diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..5aa2bd4e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.settings +.classpath +.project +target +annotations/src/main/java/org +*/bin +*.ipr +*.iml +*.iws +.idea +*.aj diff --git a/LICENSE.TXT b/LICENSE.TXT new file mode 100644 index 000000000..fbcc6f285 --- /dev/null +++ b/LICENSE.TXT @@ -0,0 +1,21 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +Roo is copyright (C) 2008-2011 SpringSource Inc. All Rights Reserved. + +Roo is licensed under the Apache License, Version 2.0: + + http://www.apache.org/licenses/LICENSE-2.0.html. + +In connection with the annotations, we further note: + + 1. The annotations are source level retention only. As such there is + no requirement for the roo-annotations-*.jar file to be present + in your classpath at runtime. The annotations are not even + referred to in your compiled .class files. + + 2. Roo itself is a development time only tool. No Roo JARs whatsoever + are required by your project at runtime. + +[end] diff --git a/README.adoc b/README.adoc index 5435c00cd..05a55f51e 100644 --- a/README.adoc +++ b/README.adoc @@ -1 +1,387 @@ +<<<<<<< HEAD # spring-roo-community-addons +======= += Spring Roo image:https://build.spring.io/plugins/servlet/buildStatusImage/ROO-BUILD["Build Status", link="https://build.spring.io/browse/ROO-BUILD"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-roo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] +Getting started with Spring Roo development +:page-layout: base +:toc-placement: manual +:Author: DISID Corporation S.L. +:Email: + +Thanks for checking out Spring Roo from Git. + +These instructions are aimed at experienced developers looking to *develop Spring Roo itself*. If you are new to Spring Roo or would simply like to try a release that has already been built, tested and distributed by the core development team, we recommend that you visit the http://projects.spring.io/spring-roo/[Spring Roo Home Page] and download an official release. + +If you are looking for *Reference Documentation* you can get it http://docs.spring.io/spring-roo/docs/current/reference/html/[here]. + +image:https://lh4.googleusercontent.com/-_DpgkWvc3bQ/UUwmwkLNdlI/AAAAAAAAAhU/kG3QSpLOhtw/s301/Logo_SpringRoo.png["Spring Roo Logo"] + +== Developer Instructions + +These instructions detail how to get started with your freshly checked-out source tree. Follow next steps to develop Spring Roo yourself. + +. <> +. <> +. <> +. <> +. <> +. <> +. <> +. <> +. <> + +[[one-time-setup-instructions]] +== One-time setup instructions + +We'll assume you typed the following to checkout Roo (if not, adjust the paths in the following instructions accordingly): + +[source, shell] +cd ~ +git clone git@github.com:spring-projects/spring-roo.git + + +In the instructions below, _$ROO_HOME_ refers to the location where you checked out Roo (in this case it would be _ROO_HOME="~/roo"_). You do NOT need to add a _$ROO_HOME_ variable. It is simply used in these docs. + +Next double-check you meet the *installation requirements*: + +* A proper installation of *Java 6 or above* +* *Maven 3.0.1+* properly installed and working with your Java 6+ +* *Internet access* so that Maven can download required dependencies +* A *Git command line* client installed (required by Roo's Maven build for inserting the current revision number into OSGi bundle manifests) + +Next you need to setup an environment variable called _MAVEN_OPTS_. If you already have a _MAVEN_OPTS_, just check it has the memory sizes shown below (or greater). + +If you're following our checkout instructions above and are on a *nix machine, you can just type: + +[source, shell] +echo export MAVEN_OPTS=\"-Xmx1024m -XX:MaxPermSize=512m\" >> ~/.bashrc +source ~/.bashrc +echo $MAVEN_OPTS + +Result: _MAVEN_OPTS=-Xmx1024m -XX:MaxPermSize=512m)_ + +You're almost finished. + +You just need to wrap up with a *symbolic link* (Windows users instead add _$ROO_HOME/bootstrap_ to your path): + +[source, shell] +sudo ln -s $ROO_HOME/bootstrap/roo-dev /usr/bin/roo-dev +sudo chmod +x /usr/bin/roo-dev + +[[gpg-pgp-setup]] +== GPG (PGP) Setup + +Roo now uses GPG to automatically sign build outputs. If you haven't installed GPG, download and install it: + +* Main site: http://www.gnupg.org/download/ +* Apple Mac option: http://macgpg.sourceforge.net/ + +Ensure you have a valid signature. Use _"gpg --list-secret-keys"_. + +You should see some output like this: + +[source, shell] +---- +$ gpg --list-secret-keys +/home/balex/.gnupg/secring.gpg +sec 1024D/00B5050F 2009-03-28 +uid Ben Alex +uid Ben Alex +uid Ben Alex +ssb 4096g/2DB6833B 2009-03-28 +---- + +If you don't see the output, it means you first need to create a key. + +It's very easy to do this, just use _"gpg --gen-key"_. + +Then verify your newly-created key was indeed created: _"gpg --list-secret-keys"_. + +Next you need to publish your key to a *public keyserver*. Take a note of the "sec" key ID shown from the _--list-secret-keys_. In my case it's key ID _"00B5050F"_. + +Push your public key to a keyserver via the command _"gpg --keyserver hkp://pgp.mit.edu --send-keys 00B5050F"_ (_of course changing the key ID at the end_). Most public key servers share keys, so you don't need to send your public key to multiple key servers. + +Finally, every time you build you will be prompted for the password of your +key. + +You have *several options*: + +* Type the password in every time +* Include a _-Dgpg.passphrase=thephrase_ argument when calling "mvn" +* Edit _~/.bashrc_ and add _-Dgpg.passphrase=thephrase_ to _MAVEN_OPTS_ +* Edit your active Maven profile to include a _"gpg.passphrase"_ property: +[source, shell] + + + + roorules + +Of course the most secure option is to type the password every time. However, *if you're doing a lot of builds* you might prefer automation. + +NOTE: _if you're new to GPG: don't lose your private key! Backup the secring.gpg file, as you'll need it to ever revoke your key or sign a replacement key (the public key servers offer no way to revoke a key unless you can sign the revocation request)._ + +[[developing-within-eclipse]] +== Developing within eclipse + +Spring Roo itself does not use *AspectJ* and therefore any *standard IDE* can be used for development. No extra plugins are needed and the team use _"mvn clean eclipse:clean eclipse:eclipse"_ to produce https://www.eclipse.org/[Eclipse] project files that can be imported via _File > Import > Existing_ Projects into Workspace. + +In theory you could use the https://www.eclipse.org/m2e/[m2eclipse plugin]. The Roo team just tends to use _eclipse:clean eclipse:eclipse_ instead. + +[[running-the-command-line-tool]] +== Running the command line tool + +Roo uses http://www.osgi.org/[OSGi] and OSGi requires compiled JARs. Therefore as you make changes in Roo, you'd normally need to _"mvn package"_ the relevant project(s), then copy the resulting JAR files to the OSGi container. + +To simplify development and OSGi-related procedures, Roo's Maven POMs have been carefully configured to emit manifests, SCR descriptors and dependencies. + +These are mostly emitted when you use _"mvn package"_. + +To try Roo out, you should type the following: + +[source, shell] +cd $ROO_HOME +mvn install +cd ~/some-directory +roo-dev + +It's important that you run *roo-dev* from a directory that you'd like to eventually contain a Roo-created project. + +IMPORTANT: _Don't try to run *roo-dev* from your $ROO_HOME directory._ + +If this fails, please review the *"OSGi Wrapping JARs"* section above. + +Notice we used _"mvn install"_ rather than _"mvn package"_. This is simply for convenience, as it will allow you to "cd" into any Roo module subdirectory and "mvn install". This saves considerable build time if changes are only being made in a single module. + +Roo ships with a command line tool called *"roo-dev"*. This is also a Windows equivalent. It copies all relevant JARs from the Roo directories into _~/roo/bootstrap/target/osgi_. This directory represents a configured Roo OSGi instance. + +*"roo-dev"* also launches the OSGi container, which is currently http://felix.apache.org/[Apache Felix]. It also activate *"development mode"*, which gives fuller exceptions, more file activity reporting, extra flash messages related to OSGi events etc. + +[[sts-integration]] +== STS integration + +Spring Roo is not longer included on http://spring.io/tools[STS] distributions. + +If you wan to include *Roo Support* on your STS, follow the next instructions: + +. Open your STS IDE + +. Open STS dashboard and search Spring Roo + +. Install *Spring IDE - Roo Extension* ++ +image:http://i.stack.imgur.com/gzsc0.jpg[Current production release] + +. Install *Spring IDE - Roo Extension* ++ +image:http://i.stack.imgur.com/MOtHu.png[Roo Extension] + +. Restarts STS IDE + +With that simple steps you will include Roo Support on your STS IDE. + +[[git-polices]] +== Git Polices + +When checking into Git, you must provide a *commit message* which begins with the relevant https://jira.spring.io/browse/ROO[Roo Jira] issue tracking number. The message should be in the form *"ROO-xxx: Title of the Jira Issue"*. For example: + +[source, shell] +ROO-1234: Name of the task as stated in Jira + +You are free to place whatever text you like after this prefix. The prefix ensures FishEye is able to correlate the commit with Jira. eg: + +[source, shell] +ROO-1234: Name of the task as stated in Jira - add extra file + +You should *not commit any IDE or Maven-generated files into Git*. + +Try to avoid _"git pull"_, as it creates lots of commit messages like _"Merge branch 'master' of git.springsource.org:roo/roo". You can avoid this with "git pull --rebase"._ + +See the "Git Tips" below for advice. + +[[git-tips]] +== Git Tips + +Setup Git correctly before you do anything else: + +[source, shell] +git config --global user.name "Kanga Roo" +git config --global user.email joeys@marsupial.com + +Perform the *initial checkout* with this: + +[source, shell] +git clone git@github.com:spring-projects/spring-roo.git + +Let's take the simple case where you just want to make a minor change against master. You don't want a new branch etc, and you only want a single commit to eventually show up in "git log". The easiest way is to start your editing session with this: + +[source, shell] +git pull + +That will give you the latest code. Go and edit files. Determine the changes with: + +[source, shell] +git status + +You can use "git add -A" if you just want to add everything you see. + +Next you need to make a commit. Do this via: + +[source, shell] +git commit -e + +The -e will cause an editor to load, allowing you to edit the message. Every commit message should reflect the "Git Policies" above. + +Now if nobody else has made any changes since your original "git pull", you can simply type this: + +[source, shell] +git push origin + +If the result is '[ok]', you're done. + +If the result is '[rejected]', someone else beat you to it. The simplest way to workaround this is: + +[source, shell] +git pull --rebase + +The --rebase option will essentially do a 'git pull', but then it will reapply your commits again as if they happened after the 'git pull'. This avoids verbose logs like "Merge branch 'master'". + +If you're doing something non-trivial, it's best to create a branch. Learn more about this at http://sysmonblog.co.uk/misc/git_by_example/. + +[[releasing]] +== Releasing + +Roo is released on a regular basis by the *Roo project team*. To perform releases and make the associated announcements you require *appropriate permissions to many systems* (as listed below). As such these notes are intended to assist developers with such permissions complete releases. + +Our release procedure may seem long, but that's because it includes many steps related to final testing and staging releases with other teams. + +=== Prerequisites: + +* *GPG setup* (probably already setup if you followed notes above) +* *Git push privileges* (if you can commit, you have this) +* *VPN access* for SSH into static.springsource.org +* *SSH keypair* for auto login into static.springsource.org +* *s3cmd setup* (so "s3cmd ls" lists spring-roo-repository.springsource.org) +* *~/.m2/settings.xml* for spring-roo-repository-release and spring-roo-repository-snapshot IDs with S3 username/password +* @SpringRoo *twitter account credentials* +* spring.io/projects/spring-roo *editor privileges*. Note you need editor + privileges for source pages at + https://github.com/spring-projects/spring-roo/tree/gh-pages +* JIRA project *administrator privileges* +* Close down your IDE before proceeding + +=== Release Procedure: + +. Complete a thorough testing build and assembly ZIP: ++ +[source, shell] +---- +cd $ROO_HOME +git pull +cd $ROO_HOME/deployment-support +./roo-deploy-dist.sh -c next -n 4.5.6.RELEASE (use -v for logging) +cd $ROO_HOME +mvn clean install +cd $ROO_HOME/deployment-support +mvn clean site +./roo-deploy-dist.sh -c assembly -tv (use -t for extra tests) +---- + +. Verify the assembly ZIP ($ROO_HOME/target/roo-deploy/dist/*.zip) looks good: + +- Assembly ZIP unzips and is of a sensible size +- Assembly ZIP runs correctly when installed on major platforms +- Create Jira Task ticket "Release Spring Roo x.y.z.aaaaaa" +- Run the "reference guide" command in the Roo shell, copy the resulting XML file into $ROO_HOME/deployment-support/src/site/docbook/reference, git commit and then git push (so the appendix is updated) + +. Tag the release (update the key ID, Jira ID and tag ID): ++ +[source, shell] +cd $ROO_HOME +git tag -a -m "ROO-XXXX: Release Spring Roo 4.5.6.RELEASE" 4.5.6.RELEASE + +. Build JARs: ++ +[source, shell] + cd $ROO_HOME + mvn clean package + +. Build the reference guide and deploy to the static staging server. You must be connected to the VPN for deployment to work. Note that http://projects.spring.io/spring-roo/ is updated bi-hourly from staging: ++ +[source, shell] +cd $ROO_HOME/deployment-support +mvn clean site site:deploy + +. Create the final assembly ZIP (must happen *after* site built). We run full tests here, even ensuring all the Maven artifacts used by user projects are available. This takes a lot of time, but it is very helpful for our users: ++ +[source, shell] +cd $ROO_HOME/deployment-support +./roo-deploy-dist.sh -c assembly -Tv (-T means Maven tests with empty repo) + +. Repeat the verification tests on the assembly ZIP (see above). See note below if coordinating a release with the STS team. ++ +Typically after this step you'll *send the tested assembly ZIP to the STS team for a concurrent release*. Allow time for them to test the ZIP before starting step 8. This allows verification of STS embeddeding. Keep your ROO_HOME intact during this time, as you need the **/target and /.git directories for steps 8 and 9 to be completed. + +. If the verifications pass, push the Git tag up to the server: ++ +[source, shell] +cd $ROO_HOME +git push --tags + +. Deploy the JARs to Maven Central ++ +[source, shell] +cd $ROO_HOME +mvn clean deploy + +. Deploy assembly ZIP (binaries) to the production download servers (it takes up to an hour for these to be made fully downloadable): ++ +[source, shell] +cd $ROO_HOME/deployment-support +./roo-deploy-dist.sh -c deploy (use -dv for a dry-run and verbose logging) + +. Increment the version number to the next BUILD-SNAPSHOT number: ++ +[source, shell] +cd $ROO_HOME/deployment-support +./roo-deploy-dist.sh -c next -n 4.5.6.BUILD-SNAPSHOT (use -v for logging) +cd $ROO_HOME +mvn clean install eclipse:clean eclipse:eclipse +cd ~/some-directory; roo-dev script clinic.roo; mvn test +cd $ROO_HOME +git diff +git commit -a -m "ROO-XXXX: Update to next version" +git push + +If any problems are detected before step 8, *simply fix*, push and start from step 1 again. You have not deployed anything substantial (ie only the reference guide) until step 8, so some corrections and re-tagging can be performed without any difficulty. The critical requirement is to defer step 8 (and beyond) until you're sure everything is fine. + +=== Pre-notification testing: + +* Visit http://projects.spring.io/spring-roo/, click "Download!" +* Ensure it unzips OK and the sha1sum matches the downloaded .sha +* `rm -rf ~/.m2/repository/org/springframework/roo` +* Use "roo script clinic.roo" to build a new Roo project +* Use "mvn clean test" to verify Roo's annotation JAR downloads + +=== Notifications and administration + +Once the release is completed (ie all steps above) you'll typically: + +* Mark the version as "released" in JIRA (_Admin > JIRA Admin_...) +* Publish a https://spring.io/blog/ entry explaining what's new +* Update http://en.wikipedia.org/wiki/Spring_Roo with the version +* Edit project page http://projects.spring.io/spring-roo/ +* Tweet from @SpringRoo (NB: ensure #SpringRoo is in the message) +* Tweet from your personal account +* Email dev list +* Resolve the "release ticket" in JIRA + +[[help]] +== Help + +http://forum.springsource.org is now a read-only archive. All commenting, posting, registration services have been turned off. + +If you have any question about Spring-roo project and its functionalities, you can check http://stackoverflow.com/questions/tagged/spring-roo + +Thanks for your interest in Spring Roo! + +>>>>>>> 9ebd4a3c54b24ffc403d204085dab971ef930241 diff --git a/addon-backup/pom.xml b/addon-backup/pom.xml new file mode 100644 index 000000000..0f5f1fdf6 --- /dev/null +++ b/addon-backup/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.backup + bundle + Spring Roo - Addon - Backup + Provides project backup facilities. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupCommands.java b/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupCommands.java new file mode 100644 index 000000000..db8df878b --- /dev/null +++ b/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupCommands.java @@ -0,0 +1,31 @@ +package org.springframework.roo.addon.backup; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'backup' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class BackupCommands implements CommandMarker { + + @Reference private BackupOperations backupOperations; + + @CliCommand(value = "backup", help = "Backup your project to a zip file") + public String backup() { + return backupOperations.backup(); + } + + @CliAvailabilityIndicator("backup") + public boolean isBackupCommandAvailable() { + return backupOperations.isBackupPossible(); + } +} \ No newline at end of file diff --git a/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupOperations.java b/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupOperations.java new file mode 100644 index 000000000..6855177a2 --- /dev/null +++ b/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupOperations.java @@ -0,0 +1,14 @@ +package org.springframework.roo.addon.backup; + +/** + * Interface to {@link BackupOperationsImpl}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface BackupOperations { + + String backup(); + + boolean isBackupPossible(); +} \ No newline at end of file diff --git a/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupOperationsImpl.java b/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupOperationsImpl.java new file mode 100644 index 000000000..dc15d6781 --- /dev/null +++ b/addon-backup/src/main/java/org/springframework/roo/addon/backup/BackupOperationsImpl.java @@ -0,0 +1,130 @@ +package org.springframework.roo.addon.backup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.FileUtils; + +/** + * Operations for the 'backup' add-on. + * + * @author Stefan Schmidt + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class BackupOperationsImpl implements BackupOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(BackupOperationsImpl.class); + + @Reference private FileManager fileManager; + @Reference private ProjectOperations projectOperations; + + public String backup() { + Validate.isTrue(isBackupPossible(), "Project metadata unavailable"); + + // For Windows, make a date format that can legally form part of a + // filename (ROO-277) + final String pattern = File.separatorChar == '\\' ? "yyyy-MM-dd_HH.mm.ss" + : "yyyy-MM-dd_HH:mm:ss"; + final DateFormat df = new SimpleDateFormat(pattern); + final long start = System.nanoTime(); + + ZipOutputStream zos = null; + try { + final File projectDirectory = new File(projectOperations + .getPathResolver().getFocusedIdentifier(Path.ROOT, ".")); + final MutableFile file = fileManager.createFile(FileUtils + .getCanonicalPath(new File(projectDirectory, + projectOperations.getFocusedProjectName() + "_" + + df.format(new Date()) + ".zip"))); + zos = new ZipOutputStream(file.getOutputStream()); + zip(projectDirectory, projectDirectory, zos); + } + catch (final FileNotFoundException e) { + LOGGER.fine("Could not determine project directory"); + } + catch (final IOException e) { + LOGGER.fine("Could not create backup archive"); + } + finally { + IOUtils.closeQuietly(zos); + } + + final long milliseconds = (System.nanoTime() - start) / 1000000; + return "Backup completed in " + milliseconds + " ms"; + } + + public boolean isBackupPossible() { + return projectOperations.isFocusedProjectAvailable(); + } + + private void zip(final File directory, final File base, + final ZipOutputStream zos) throws IOException { + final File[] files = directory.listFiles(new FilenameFilter() { + public boolean accept(final File dir, final String name) { + // Don't use this directory if it's "target" under base + if (dir.equals(base) && name.equals("target")) { + return false; + } + + // Skip existing backup files + if (dir.equals(base) && name.endsWith(".zip")) { + return false; + } + + // Skip files that start with "." + return !name.startsWith("."); + } + }); + + for (final File file : files) { + if (file.isDirectory()) { + if (file.listFiles().length == 0) { + final ZipEntry dirEntry = new ZipEntry(file.getPath() + .substring(base.getPath().length() + 1) + + File.separatorChar); + zos.putNextEntry(dirEntry); + } + zip(file, base, zos); + } + else { + InputStream inputStream = null; + try { + final ZipEntry entry = new ZipEntry(file.getPath() + .substring(base.getPath().length() + 1)); + zos.putNextEntry(entry); + + inputStream = new FileInputStream(file); + IOUtils.write(IOUtils.toByteArray(inputStream), zos); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + } + } +} diff --git a/addon-cloud/pom.xml b/addon-cloud/pom.xml new file mode 100644 index 000000000..f9f088c7f --- /dev/null +++ b/addon-cloud/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.cloud + bundle + Spring Roo - Addon - Cloud + Provides some different Cloud Providers to deploy Spring Roo applications on cloud servers + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.jpa + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-cloud/src/main/assembly/assembly.xml b/addon-cloud/src/main/assembly/assembly.xml new file mode 100644 index 000000000..940df7736 --- /dev/null +++ b/addon-cloud/src/main/assembly/assembly.xml @@ -0,0 +1,72 @@ + + + assembly + + zip + + true + + + + / + + unix + true + + readme.txt + + + + /legal + legal + unix + true + + *.txt + *.TXT + + + + /dist + target + true + + *.jar + + + *-tests.jar + *-sources.jar + + + + /src + target + true + + *-tests.jar + *-sources.jar + + + + /src + + true + + pom.xml + + + + + + + lib + false + runtime + false + false + true + + + + diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudCommands.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudCommands.java new file mode 100644 index 000000000..e3e031fad --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudCommands.java @@ -0,0 +1,59 @@ +package org.springframework.roo.addon.cloud; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.cloud.providers.CloudProviderId; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Provides commands to install Cloud Provider that provides functions to deploy + * Spring Roo Application on Cloud Servers. + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + */ +@Component +@Service +public class CloudCommands implements CommandMarker { + + /** + * Get a reference to the CloudOperations from the underlying OSGi container + */ + @Reference + private CloudOperations operations; + + @Reference + private TypeLocationService typeLocationService; + + /** + * This method checks if the setup method is available + * + * @return true (default) if the command should be visible at this stage, + * false otherwise + */ + @CliAvailabilityIndicator("cloud setup") + public boolean isSetupCommandAvailable() { + return operations.isSetupCommandAvailable(); + } + + /** + * This method registers a command with the Roo shell. It also offers two + * command attributes, a mandatory one and an optional command which has a + * default value. + * + * @param provider + * @param configuration + */ + @CliCommand(value = "cloud setup", help = "Setup Cloud Provider on Spring Roo Project") + public void setup( + @CliOption(key = "provider", mandatory = true, help = "Cloud Provider's Name") CloudProviderId provider, + @CliOption(key = "configuration", mandatory = false, help = "Plugin Configuration. Add configuration by command like 'key=value,key2=value2,key3=value3'") String configuration) { + operations.installProvider(provider, configuration); + } + +} \ No newline at end of file diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudOperations.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudOperations.java new file mode 100644 index 000000000..7e445e240 --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudOperations.java @@ -0,0 +1,48 @@ +package org.springframework.roo.addon.cloud; + +import java.util.List; + +import org.springframework.roo.addon.cloud.providers.CloudProviderId; + +/** + * Provides operations to install Cloud Provider that provides functions to + * deploy Spring Roo Application on Cloud Servers. + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + */ +public interface CloudOperations { + + /** + * This method checks if setup command is available + * + * @return (boolean) + */ + boolean isSetupCommandAvailable(); + + /** + * This method execute install provider method + * + * @param provider + * @param configuration + */ + void installProvider(CloudProviderId provider, String configuration); + + /** + * + * Get available providers on the system + * + * @return A CloudProviderId List + */ + List getProvidersId(); + + /** + * Gets the current provider by name + * + * @param name + * Provider Name + * @return CloudProviderId + */ + CloudProviderId getProviderIdByName(String name); + +} \ No newline at end of file diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudOperationsImpl.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudOperationsImpl.java new file mode 100644 index 000000000..6e1dd2656 --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/CloudOperationsImpl.java @@ -0,0 +1,104 @@ +package org.springframework.roo.addon.cloud; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.cloud.providers.CloudProvider; +import org.springframework.roo.addon.cloud.providers.CloudProviderId; +import org.springframework.roo.project.ProjectOperations; + +/** + * Provides operations implementation to install Cloud Provider that provides + * functions to deploy Spring Roo Application on Cloud Servers. + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + */ +@Component +@Service +@Reference(name = "provider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = CloudProvider.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class CloudOperationsImpl implements CloudOperations { + + private List providers = new ArrayList(); + private List providersId = null; + + @Reference + private ProjectOperations projectOperations; + + @Override + public boolean isSetupCommandAvailable() { + return projectOperations.isProjectAvailable(projectOperations + .getFocusedModuleName()); + } + + @Override + public void installProvider(CloudProviderId prov, String configuration) { + CloudProvider provider = null; + for (CloudProvider tmpProvider : providers) { + if (prov.is(tmpProvider)) { + provider = tmpProvider; + break; + } + } + if (provider == null) { + throw new RuntimeException("Provider '".concat(prov.getId()) + .concat("' not found'")); + } + provider.setup(configuration); + + } + + /** + * This method gets providerId using name + */ + @Override + public CloudProviderId getProviderIdByName(String name) { + CloudProviderId provider = null; + for (CloudProvider tmpProvider : providers) { + if (tmpProvider.getName().equals(name)) { + provider = new CloudProviderId(tmpProvider); + } + } + return provider; + } + + /** + * This method load new providers + * + * @param provider + */ + protected void bindProvider(final CloudProvider provider) { + providers.add(provider); + } + + /** + * This method remove providers + * + * @param provider + */ + protected void unbindProvider(final CloudProvider provider) { + providers.remove(provider); + } + + /** + * This method gets a List of available providers + */ + @Override + public List getProvidersId() { + if (providersId == null) { + providersId = new ArrayList(); + for (CloudProvider tmpProvider : providers) { + providersId.add(new CloudProviderId(tmpProvider)); + } + providersId = Collections.unmodifiableList(providersId); + } + return providersId; + } +} \ No newline at end of file diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/ProviderIdConverter.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/ProviderIdConverter.java new file mode 100644 index 000000000..a7a8d650d --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/ProviderIdConverter.java @@ -0,0 +1,60 @@ +package org.springframework.roo.addon.cloud; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.cloud.providers.CloudProviderId; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * + * Cloud Providers ID converter + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + * + */ +@Component +@Service +public class ProviderIdConverter implements Converter { + + @Reference + private CloudOperations operations; + + protected void bindOperations(CloudOperations operations) { + this.operations = operations; + } + + protected void unbindOperations(CloudOperations operations) { + this.operations = null; + } + + @Override + public CloudProviderId convertFromText(String value, Class targetType, + String optionContext) { + return operations.getProviderIdByName(value); + } + + @Override + public boolean getAllPossibleValues(List completions, + Class targetType, String existingData, String optionContext, + MethodTarget target) { + for (final CloudProviderId id : operations.getProvidersId()) { + if (existingData.isEmpty() || id.getId().equals(existingData) + || id.getId().startsWith(existingData)) { + completions.add(new Completion(id.getId())); + } + } + return true; + } + + @Override + public boolean supports(Class type, String optionContext) { + return CloudProviderId.class.isAssignableFrom(type); + } + +} diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/CloudProvider.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/CloudProvider.java new file mode 100644 index 000000000..780e73700 --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/CloudProvider.java @@ -0,0 +1,31 @@ +package org.springframework.roo.addon.cloud.providers; + +/** + * + * Cloud Provider Interface + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + */ +public interface CloudProvider { + + /** + * Gets provider name + * + * @return + */ + String getName(); + + /** + * + */ + String getDescription(); + + /** + * This method installs the provider that implements the interface + * + * @param configuration + */ + void setup(String configuration); + +} diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/CloudProviderId.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/CloudProviderId.java new file mode 100644 index 000000000..d99fac631 --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/CloudProviderId.java @@ -0,0 +1,34 @@ +package org.springframework.roo.addon.cloud.providers; + +/** + * Cloud Provider Class + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + */ +public class CloudProviderId { + + private String name; + private String description; + private String className; + + public CloudProviderId(CloudProvider provider) { + this.name = provider.getName(); + this.description = provider.getDescription(); + this.className = provider.getClass().getCanonicalName(); + } + + public String getId() { + return this.name; + } + + public String getDescription() { + return description; + } + + public boolean is(CloudProvider provider) { + return name.equals(provider.getName()) + && className.equals(provider.getClass().getCanonicalName()); + } + +} diff --git a/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/cloudfoundry/CloudFoundryProvider.java b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/cloudfoundry/CloudFoundryProvider.java new file mode 100644 index 000000000..67ebe7aa9 --- /dev/null +++ b/addon-cloud/src/main/java/org/springframework/roo/addon/cloud/providers/cloudfoundry/CloudFoundryProvider.java @@ -0,0 +1,232 @@ +package org.springframework.roo.addon.cloud.providers.cloudfoundry; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.cloud.providers.CloudProvider; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Configuration; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * + * This Cloud Provider provides you the functionalities to deploy Spring Roo + * application on Cloud Foundry + * + * @author Juan Carlos García del Canto + * @since 1.2.6 + */ +@Component +@Service +public class CloudFoundryProvider implements CloudProvider { + + @Reference + private FileManager fileManager; + + @Reference + private PathResolver pathResolver; + + @Reference + private TypeLocationService typeLocationService; + + @Reference + private TypeManagementService typeManagementService; + + @Reference + private ProjectOperations projectOperations; + + /** + * DECLARING CONSTANTS + */ + + public static final String NAME = "CLOUD_FOUNDRY"; + + public static final String DESCRIPTION = "Setup Cloud Foundry Maven Plugin on your Spring Roo Application"; + + private static final Logger LOGGER = Logger + .getLogger(CloudFoundryProvider.class.getName()); + + /** + * This method configure your project with Cloud Foundry Maven Plugin + * + * @param configuration + */ + @Override + public void setup(String pluginConfiguration) { + // Adding Cloud Foundry Maven Plugin + updatePlugins(pluginConfiguration, projectOperations); + // Showing INFO about how to use CF Maven Plugin + showInfo(pluginConfiguration); + } + + /** + * + * This method update plugins with the added to configuration.xml file + * + * @param configuration + * @param moduleName + * @param projectOperations + */ + public static void updatePlugins(String pluginConfiguration, + ProjectOperations projectOperations) { + + Configuration conf = null; + + // Generating configuration if necessary + if (StringUtils.isNotBlank(pluginConfiguration)) { + try { + + DocumentBuilderFactory docFactory = DocumentBuilderFactory + .newInstance(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + + Document doc = docBuilder.newDocument(); + Element configElement = doc.createElement("configuration"); + + // Getting all configurations + String[] configurationTags = pluginConfiguration.split(","); + + for (String configurationTag : configurationTags) { + String[] keyValue = configurationTag.split("="); + if (keyValue.length == 2) { + Element element = doc.createElement(keyValue[0]); + element.setTextContent(keyValue[1]); + configElement.appendChild(element); + } + } + + conf = new Configuration(configElement); + + } catch (Exception e) { + LOGGER.log(Level.WARNING, "[ERROR] " + e); + } + } + + // Generate Plugin + Plugin cloudFoundryMvnPlugin = new Plugin("org.cloudfoundry", + "cf-maven-plugin", "1.0.4", conf, null, null); + + // Adding plugin + projectOperations + .addBuildPlugin(projectOperations.getFocusedModuleName(), + cloudFoundryMvnPlugin); + } + + /** + * This method shows info about how to use Cloud Foundry + * + * @param configuration + */ + public static void showInfo(String configuration) { + // Showing congrats message + LOGGER.log( + Level.INFO, + "Congratulations! Now you can use Cloud Foundry Maven Plugin to deploy your applications!"); + LOGGER.log(Level.INFO, ""); + // Showing current config if necessary + if (StringUtils.isNotBlank(configuration)) { + LOGGER.log(Level.INFO, "This is your current configuration:"); + LOGGER.log(Level.INFO, ""); + LOGGER.log(Level.INFO, ""); + // Getting all configurations + String[] configurationTags = configuration.split(","); + + for (String configurationTag : configurationTags) { + String[] keyValue = configurationTag.split("="); + if (keyValue.length == 2) { + LOGGER.log(Level.INFO, String.format(" <%s>%s", + keyValue[0], keyValue[1], keyValue[0])); + } + } + LOGGER.log(Level.INFO, ""); + } else { + LOGGER.log(Level.INFO, + "WARNING: You don't specify any configuration."); + } + // Showing commands you can use with maven CF Plugin + LOGGER.log(Level.INFO, ""); + LOGGER.log( + Level.INFO, + "You can use the following Cloud Foundry Maven Plugin commands with \"perform command --mavenCommand\" on ROO Shell or using \"mvn\" on OS command line."); + LOGGER.log(Level.INFO, ""); + LOGGER.log(Level.INFO, "Command Description"); + LOGGER.log(Level.INFO, "----------------------------------------"); + LOGGER.log(Level.INFO, "cf:apps List deployed applications."); + LOGGER.log(Level.INFO, + "cf:app Show details of an application."); + LOGGER.log(Level.INFO, "cf:delete Delete an application."); + LOGGER.log(Level.INFO, + "cf:env Show an application's environment variables."); + LOGGER.log(Level.INFO, + "cf:help Show documentation for all available commands."); + LOGGER.log(Level.INFO, + "cf:push Push and optionally start an application."); + LOGGER.log( + Level.INFO, + "cf:push-only Push and optionally start an application, without packaging."); + LOGGER.log(Level.INFO, "cf:restart Restart an application."); + LOGGER.log(Level.INFO, "cf:start Start an application."); + LOGGER.log(Level.INFO, "cf:stop Stop an application."); + LOGGER.log( + Level.INFO, + "cf:target Show information about the target Cloud Foundry service."); + LOGGER.log(Level.INFO, + "cf:logs Tail application logs."); + LOGGER.log(Level.INFO, + "cf:recentLogs Show recent application logs."); + LOGGER.log(Level.INFO, + "cf:scale Scale the application instances up or down."); + LOGGER.log(Level.INFO, + "cf:services Show a list of provisioned services."); + LOGGER.log(Level.INFO, + "cf:service-plans Show a list of available service plans."); + LOGGER.log(Level.INFO, + "cf:create-services Create services defined in the pom."); + LOGGER.log(Level.INFO, + "cf:delete-services Delete services defined in the pom."); + LOGGER.log(Level.INFO, + "cf:bind-services Bind services to an application."); + LOGGER.log(Level.INFO, + "cf:unbind-services Unbind services from an application."); + LOGGER.log( + Level.INFO, + "cf:delete-orphaned-routes Delete all routes that are not bound to any application."); + LOGGER.log( + Level.INFO, + "cf:login Log in to the target Cloud Foundry service and save access tokens."); + LOGGER.log( + Level.INFO, + "cf:logout Log out of the target Cloud Foundry service and remove access tokens."); + } + + /** + * PROVIDER CONFIGURATION METHODS + */ + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + +} diff --git a/addon-configurable/pom.xml b/addon-configurable/pom.xml new file mode 100644 index 000000000..a763f514a --- /dev/null +++ b/addon-configurable/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.configurable + bundle + Spring Roo - Addon - @Configurable + Support for the introduction of Spring's @Configurable annotation through an AspectJ ITD. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadata.java b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadata.java new file mode 100644 index 000000000..de55b02a1 --- /dev/null +++ b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadata.java @@ -0,0 +1,97 @@ +package org.springframework.roo.addon.configurable; + +import static org.springframework.roo.model.SpringJavaType.CONFIGURABLE; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooConfigurable}. + * + * @author Ben Alex + * @since 1.0 + */ +public class ConfigurableMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = ConfigurableMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public ConfigurableMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + + if (!isValid()) { + return; + } + + builder.addAnnotation(getConfigurableAnnotation()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Adds the @org.springframework.beans.factory.annotation.Configurable + * annotation to the type, unless it already exists. + * + * @return the annotation is already exists or will be created, or null if + * it will not be created (required) + */ + private AnnotationMetadata getConfigurableAnnotation() { + return getTypeAnnotation(CONFIGURABLE); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("configurableIntroduced", + getTypeAnnotation(CONFIGURABLE) != null); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadataProvider.java b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadataProvider.java new file mode 100644 index 000000000..787784cef --- /dev/null +++ b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadataProvider.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.configurable; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link ConfigurableMetadata}. + *

+ * Generally other add-ons which depend on Spring's @Configurable annotation + * being present will add their annotation as a trigger annotation to instances + * of {@link ConfigurableMetadataProvider}. This action will guarantee any class + * with the added trigger annotation will made @Configurable. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ConfigurableMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadataProviderImpl.java b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadataProviderImpl.java new file mode 100644 index 000000000..aa31cec0f --- /dev/null +++ b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/ConfigurableMetadataProviderImpl.java @@ -0,0 +1,82 @@ +package org.springframework.roo.addon.configurable; + +import static org.springframework.roo.model.RooJavaType.ROO_CONFIGURABLE; + +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link ConfigurableMetadataProvider}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class ConfigurableMetadataProviderImpl extends + AbstractItdMetadataProvider implements ConfigurableMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ConfigurableMetadataProviderImpl.class); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_CONFIGURABLE); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return ConfigurableMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_CONFIGURABLE); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = ConfigurableMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = ConfigurableMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Configurable"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + return new ConfigurableMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata); + } + + public String getProvidesType() { + return ConfigurableMetadata.getMetadataIdentiferType(); + } +} diff --git a/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/RooConfigurable.java b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/RooConfigurable.java new file mode 100644 index 000000000..03ea18654 --- /dev/null +++ b/addon-configurable/src/main/java/org/springframework/roo/addon/configurable/RooConfigurable.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.configurable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a class should be annotated with Spring's + * {@link org.springframework.beans.factory.annotation.Configurable} annotation. + *

+ * Obviously you should just use @Configurable in normal Java code if you would + * like {@link org.springframework.beans.factory.annotation.Configurable} + * functionality (ie there is no use case for using {@link RooConfigurable} + * given it is more complex with the involvement of ITDs etc). This annotation + * exists solely for consistency with other ITD providers. + * + * @author Ben Alex + * @since 1.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooConfigurable { +} diff --git a/addon-creator/pom.xml b/addon-creator/pom.xml new file mode 100644 index 000000000..ad3c93569 --- /dev/null +++ b/addon-creator/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.creator + bundle + Spring Roo - Addon - Creator + Support for creating new Spring Roo add-ons. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.url.stream + + + \ No newline at end of file diff --git a/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorCommands.java b/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorCommands.java new file mode 100644 index 000000000..6b9ac6321 --- /dev/null +++ b/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorCommands.java @@ -0,0 +1,88 @@ +package org.springframework.roo.addon.creator; + +import static org.springframework.roo.shell.OptionContexts.UPDATE; + +import java.io.File; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'addon create' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class CreatorCommands implements CommandMarker { + + @Reference private CreatorOperations creatorOperations; + + @CliCommand(value = "addon create advanced", help = "Create a new advanced add-on for Spring Roo (commands + operations + metadata + trigger annotation + dependencies)") + public void advanced( + @CliOption(key = "topLevelPackage", mandatory = true, optionContext = UPDATE, help = "The top level package of the new addon") final JavaPackage tlp, + @CliOption(key = "description", mandatory = false, help = "Description of your addon (surround text with double quotes)") final String description, + @CliOption(key = "projectName", mandatory = false, help = "Provide a custom project name (if not provided the top level package name will be used instead)") final String projectName) { + + creatorOperations.createAdvancedAddon(tlp, description, projectName); + } + + @CliCommand(value = "addon create i18n", help = "Create a new Internationalization add-on for Spring Roo") + public void i18n( + @CliOption(key = "topLevelPackage", mandatory = true, optionContext = UPDATE, help = "The top level package of the new addon") final JavaPackage tlp, + @CliOption(key = "locale", mandatory = true, help = "The locale abbreviation (ie: en, or more specific like en_AU, or de_DE)") final Locale locale, + @CliOption(key = "messageBundle", mandatory = true, help = "Fully qualified path to the messages_xx.properties file") final File messageBundle, + @CliOption(key = "language", mandatory = false, help = "The full name of the language (used as a label for the UI)") final String language, + @CliOption(key = "flagGraphic", mandatory = false, help = "Fully qualified path to flag xx.png file") final File flagGraphic, + @CliOption(key = "description", mandatory = false, help = "Description of your addon (surround text with double quotes)") final String description, + @CliOption(key = "projectName", mandatory = false, help = "Provide a custom project name (if not provided the top level package name will be used instead)") final String projectName) { + + if (locale == null) { + throw new IllegalStateException( + "Could not read provided locale. Please use correct format (ie: en, or more specific like en_AU, or de_DE)"); + } + creatorOperations.createI18nAddon(tlp, language, locale, messageBundle, + flagGraphic, description, projectName); + } + + @CliAvailabilityIndicator({ "addon create i18n", "addon create simple", + "addon create advanced" }) + public boolean isCreateAddonAvailable() { + return creatorOperations.isAddonCreatePossible(); + } + + @CliCommand(value = "addon create simple", help = "Create a new simple add-on for Spring Roo (commands + operations)") + public void simple( + @CliOption(key = "topLevelPackage", mandatory = true, optionContext = UPDATE, help = "The top level package of the new addon") final JavaPackage tlp, + @CliOption(key = "description", mandatory = false, help = "Description of your addon (surround text with double quotes)") final String description, + @CliOption(key = "projectName", mandatory = false, help = "Provide a custom project name (if not provided the top level package name will be used instead)") final String projectName) { + + creatorOperations.createSimpleAddon(tlp, description, projectName); + } + + @CliCommand(value = "addon create wrapper", help = "Create a new add-on for Spring Roo which wraps a maven artifact to create a OSGi compliant bundle") + public void wrapper( + @CliOption(key = "topLevelPackage", mandatory = true, optionContext = UPDATE, help = "The top level package of the new wrapper bundle") final JavaPackage tlp, + @CliOption(key = "groupId", mandatory = true, help = "Dependency group id") final String groupId, + @CliOption(key = "artifactId", mandatory = true, help = "Dependency artifact id)") final String artifactId, + @CliOption(key = "version", mandatory = true, help = "Dependency version") final String version, + @CliOption(key = "vendorName", mandatory = true, help = "Dependency vendor name)") final String vendorName, + @CliOption(key = "licenseUrl", mandatory = true, help = "Dependency license URL") final String lincenseUrl, + @CliOption(key = "docUrl", mandatory = false, help = "Dependency documentation URL") final String docUrl, + @CliOption(key = "description", mandatory = false, help = "Description of the bundle (use keywords with #-tags for better search integration)") final String description, + @CliOption(key = "projectName", mandatory = false, help = "Provide a custom project name (if not provided the top level package name will be used instead)") final String projectName, + @CliOption(key = "osgiImports", mandatory = false, help = "Contents of Import-Package in OSGi manifest") final String osgiImports) { + + creatorOperations.createWrapperAddon(tlp, groupId, artifactId, version, + vendorName, lincenseUrl, docUrl, osgiImports, description, + projectName); + } +} \ No newline at end of file diff --git a/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorOperations.java b/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorOperations.java new file mode 100644 index 000000000..204e1b808 --- /dev/null +++ b/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorOperations.java @@ -0,0 +1,31 @@ +package org.springframework.roo.addon.creator; + +import java.io.File; +import java.util.Locale; + +import org.springframework.roo.model.JavaPackage; + +/** + * Provides add-on creation operations. + * + * @author Stefan Schmidt + */ +public interface CreatorOperations { + + void createAdvancedAddon(JavaPackage topLevelPackage, String description, + String projectName); + + void createI18nAddon(JavaPackage topLevelPackage, String language, + Locale locale, File messageBundle, File flagGraphic, + String description, String projectName); + + void createSimpleAddon(JavaPackage topLevelPackage, String description, + String projectName); + + void createWrapperAddon(JavaPackage topLevelPackage, String groupId, + String artifactId, String version, String vendorName, + String lincenseUrl, String docUrl, String osgiImports, + String description, String projectName); + + boolean isAddonCreatePossible(); +} \ No newline at end of file diff --git a/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorOperationsImpl.java b/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorOperationsImpl.java new file mode 100644 index 000000000..c70bca596 --- /dev/null +++ b/addon-creator/src/main/java/org/springframework/roo/addon/creator/CreatorOperationsImpl.java @@ -0,0 +1,575 @@ +package org.springframework.roo.addon.creator; + +import static java.io.File.separatorChar; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.springframework.roo.url.stream.UrlInputStreamService; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of {@link CreatorOperations}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class CreatorOperationsImpl implements CreatorOperations { + + /** + * The types of project that can be created + */ + private enum Type { + + /** + * A simple addon + */ + SIMPLE, + + /** + * An advanced addon + */ + ADVANCED, + + /** + * A language bundle + */ + I18N, + + /** + * An OSGi wrapper for a non-OSGi library + */ + WRAPPER + }; + + private static final String ICON_SET_URL = "http://www.famfamfam.com/lab/icons/flags/famfamfam_flag_icons.zip"; + private static final String POM_XML = "pom.xml"; + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private UrlInputStreamService httpService; + + private String iconSetUrl; + + protected void activate(final ComponentContext context) { + iconSetUrl = context.getBundleContext().getProperty( + "creator.i18n.iconset.url"); + if (StringUtils.isBlank(iconSetUrl)) { + iconSetUrl = ICON_SET_URL; + } + } + + public void createAdvancedAddon(final JavaPackage topLevelPackage, + final String description, final String projectName) { + Validate.notNull(topLevelPackage, "Top-level package required"); + + createProject(topLevelPackage, Type.ADVANCED, description, projectName); + + install("Commands.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.ADVANCED, projectName); + install("Operations.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.ADVANCED, projectName); + install("OperationsImpl.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.ADVANCED, projectName); + install("Metadata.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.ADVANCED, projectName); + install("MetadataProvider.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.ADVANCED, projectName); + install("RooAnnotation.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.ADVANCED, projectName); + install("assembly.xml", topLevelPackage, Path.ROOT, Type.ADVANCED, + projectName); + install("configuration.xml", topLevelPackage, Path.SRC_MAIN_RESOURCES, + Type.ADVANCED, projectName); + } + + public void createI18nAddon(final JavaPackage topLevelPackage, + String language, final Locale locale, final File messageBundle, + final File flagGraphic, String description, final String projectName) { + Validate.notNull(topLevelPackage, "Top Level Package required"); + Validate.notNull(locale, "Locale required"); + Validate.notNull(messageBundle, "Message Bundle required"); + + if (StringUtils.isBlank(language)) { + language = ""; + final InputStream inputStream = FileUtils + .getInputStream(getClass(), Type.I18N.name().toLowerCase() + + "/iso3166.txt"); + try { + for (String line : IOUtils.readLines(inputStream)) { + final String[] split = line.split(";"); + if (split[1].startsWith(locale.getCountry().toUpperCase())) { + if (split[0].contains(",")) { + split[0] = split[0].substring(0, + split[0].indexOf(",") - 1); + } + final String[] langWords = split[0].split("\\s"); + final StringBuilder b = new StringBuilder(); + for (final String word : langWords) { + b.append(StringUtils.capitalize(word.toLowerCase())) + .append(" "); + } + language = b.toString().substring(0, b.length() - 1); + } + } + } + catch (final IOException e) { + throw new IllegalStateException( + "Could not parse ISO 3166 language list, please use --language option in command"); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + final String[] langWords = language.split("\\s"); + final StringBuilder builder = new StringBuilder(); + for (final String word : langWords) { + builder.append(StringUtils.capitalize(word.toLowerCase())); + } + final String languageName = builder.toString(); + final String packagePath = topLevelPackage + .getFullyQualifiedPackageName().replace('.', separatorChar); + + if (StringUtils.isBlank(description)) { + description = languageName + + " language support for Spring Roo Web MVC JSP Scaffolding"; + } + if (!description.contains("#mvc") + || !description.contains("#localization") + || !description.contains("locale:")) { + description = description + "; #mvc,#localization,locale:" + + locale.getCountry().toLowerCase(); + } + createProject(topLevelPackage, Type.I18N, description, projectName); + + install("assembly.xml", topLevelPackage, Path.ROOT, Type.I18N, + projectName); + + OutputStream outputStream = null; + try { + outputStream = fileManager.createFile( + pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, + packagePath + separatorChar + + messageBundle.getName())) + .getOutputStream(); + org.apache.commons.io.FileUtils.copyFile(messageBundle, + outputStream); + if (flagGraphic != null) { + outputStream = fileManager + .createFile( + pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, + packagePath + separatorChar + + flagGraphic.getName())) + .getOutputStream(); + org.apache.commons.io.FileUtils.copyFile(flagGraphic, + outputStream); + } + else { + installFlagGraphic(locale, packagePath); + } + } + catch (final IOException e) { + throw new IllegalStateException( + "Could not copy addon resources into project", e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + + final String destinationFile = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_JAVA, packagePath + separatorChar + languageName + + "Language.java"); + + if (!fileManager.exists(destinationFile)) { + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), Type.I18N.name().toLowerCase() + + "/Language.java-template"); + try { + // Read template and insert the user's package + String input = IOUtils.toString(templateInputStream); + input = input.replace("__TOP_LEVEL_PACKAGE__", + topLevelPackage.getFullyQualifiedPackageName()); + input = input.replace("__APP_NAME__", languageName); + input = input.replace("__LOCALE__", locale.getLanguage()); + input = input.replace("__LANGUAGE__", + StringUtils.capitalize(language)); + if (flagGraphic != null) { + input = input.replace("__FLAG_FILE__", + flagGraphic.getName()); + } + else { + input = input.replace("__FLAG_FILE__", locale.getCountry() + .toLowerCase() + ".png"); + } + input = input.replace("__MESSAGE_BUNDLE__", + messageBundle.getName()); + + // Output the file for the user + final MutableFile mutableFile = fileManager + .createFile(destinationFile); + outputStream = mutableFile.getOutputStream(); + IOUtils.write(input, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException("Unable to create '" + + languageName + "Language.java'", ioe); + } + finally { + IOUtils.closeQuietly(templateInputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + /** + * Creates the root files for a new project, namely the: + *

    + *
  • Maven POM
  • + *
  • readme.txt
  • + *
  • + * + * @param topLevelPackage the top-level package of the project being created + * (required) + * @param type the type of project being created (required) + * @param description the description to put into the POM (can be blank) + * @param projectName if blank, a sanitised version of the given top-level + * package is used for the project name + */ + private void createProject(final JavaPackage topLevelPackage, + final Type type, final String description, String projectName) { + if (StringUtils.isBlank(projectName)) { + projectName = topLevelPackage.getFullyQualifiedPackageName() + .replace(".", "-"); + } + + // Load the POM template + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), type.name().toLowerCase() + "/roo-addon-" + + type.name().toLowerCase() + "-template.xml"); + final Document pom = XmlUtils.readXml(templateInputStream); + final Element root = pom.getDocumentElement(); + + // Populate it from the given inputs + XmlUtils.findRequiredElement("/project/artifactId", root) + .setTextContent(topLevelPackage.getFullyQualifiedPackageName()); + XmlUtils.findRequiredElement("/project/groupId", root).setTextContent( + topLevelPackage.getFullyQualifiedPackageName()); + XmlUtils.findRequiredElement("/project/name", root).setTextContent( + projectName); + XmlUtils.findRequiredElement("/project/properties/repo.folder", root) + .setTextContent( + topLevelPackage.getFullyQualifiedPackageName().replace( + ".", "/")); + if (StringUtils.isNotBlank(description)) { + XmlUtils.findRequiredElement("/project/description", root) + .setTextContent(description); + } + + // Write the new POM to disk + writePomFile(pom); + + // Write the other root files + writeTextFile("readme.txt", "Welcome to my addon!"); + writeTextFile("legal" + separatorChar + "LICENSE.TXT", + "Your license goes here"); + + fileManager.scan(); + } + + public void createSimpleAddon(final JavaPackage topLevelPackage, + final String description, final String projectName) { + Validate.notNull(topLevelPackage, "Top Level Package required"); + + createProject(topLevelPackage, Type.SIMPLE, description, projectName); + + install("Commands.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.SIMPLE, projectName); + install("Operations.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.SIMPLE, projectName); + install("OperationsImpl.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.SIMPLE, projectName); + install("PropertyName.java", topLevelPackage, Path.SRC_MAIN_JAVA, + Type.SIMPLE, projectName); + install("assembly.xml", topLevelPackage, Path.ROOT, Type.SIMPLE, + projectName); + install("info.tagx", topLevelPackage, Path.SRC_MAIN_RESOURCES, + Type.SIMPLE, projectName); + install("show.tagx", topLevelPackage, Path.SRC_MAIN_RESOURCES, + Type.SIMPLE, projectName); + } + + public void createWrapperAddon(final JavaPackage topLevelPackage, + final String groupId, final String artifactId, + final String version, final String vendorName, + final String lincenseUrl, final String docUrl, + final String osgiImports, final String description, + String projectName) { + Validate.notNull(topLevelPackage, "Top Level Package required"); + if (StringUtils.isBlank(projectName)) { + projectName = topLevelPackage.getFullyQualifiedPackageName() + .replace(".", "-"); + } + final String wrapperGroupId = topLevelPackage + .getFullyQualifiedPackageName(); + + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), "wrapper/roo-addon-wrapper-template.xml"); + final Document pom = XmlUtils.readXml(templateInputStream); + final Element root = pom.getDocumentElement(); + + XmlUtils.findRequiredElement("/project/name", root).setTextContent( + projectName); + XmlUtils.findRequiredElement("/project/groupId", root).setTextContent( + wrapperGroupId); + XmlUtils.findRequiredElement("/project/artifactId", root) + .setTextContent(wrapperGroupId + "." + artifactId); + XmlUtils.findRequiredElement("/project/version", root).setTextContent( + version + ".0001"); + XmlUtils.findRequiredElement( + "/project/dependencies/dependency/groupId", root) + .setTextContent(groupId); + XmlUtils.findRequiredElement( + "/project/dependencies/dependency/artifactId", root) + .setTextContent(artifactId); + XmlUtils.findRequiredElement( + "/project/dependencies/dependency/version", root) + .setTextContent(version); + XmlUtils.findRequiredElement("/project/properties/pkgArtifactId", root) + .setTextContent(artifactId); + XmlUtils.findRequiredElement("/project/properties/pkgVersion", root) + .setTextContent(version); + XmlUtils.findRequiredElement("/project/properties/pkgVendor", root) + .setTextContent(vendorName); + XmlUtils.findRequiredElement("/project/properties/pkgLicense", root) + .setTextContent(lincenseUrl); + XmlUtils.findRequiredElement("/project/properties/repo.folder", root) + .setTextContent( + topLevelPackage.getFullyQualifiedPackageName().replace( + ".", "/")); + if (docUrl != null && docUrl.length() > 0) { + XmlUtils.findRequiredElement("/project/properties/pkgDocUrl", root) + .setTextContent(docUrl); + } + if (osgiImports != null && osgiImports.length() > 0) { + final Element config = XmlUtils + .findRequiredElement( + "/project/build/plugins/plugin[artifactId = 'maven-bundle-plugin']/configuration/instructions", + root); + config.appendChild(new XmlElementBuilder("Import-Package", pom) + .setText(osgiImports).build()); + } + if (description != null && description.length() > 0) { + final Element descriptionE = XmlUtils.findRequiredElement( + "/project/description", root); + descriptionE.setTextContent(description + " " + + descriptionE.getTextContent()); + } + + writePomFile(pom); + } + + private String getErrorMsg(final String localeStr) { + return "Could not acquire flag icon for locale " + localeStr + + " please use --flagGraphic to specify the flag manually"; + } + + private void install(final String targetFilename, + final JavaPackage topLevelPackage, final Path path, + final Type type, String projectName) { + if (StringUtils.isBlank(projectName)) { + projectName = topLevelPackage.getFullyQualifiedPackageName() + .replace(".", "-"); + } + final String topLevelPackageName = topLevelPackage + .getFullyQualifiedPackageName(); + final String packagePath = topLevelPackageName.replace('.', + separatorChar); + String destinationFile = ""; + + if (targetFilename.endsWith(".java")) { + destinationFile = pathResolver.getFocusedIdentifier( + path, + packagePath + + separatorChar + + StringUtils.capitalize(topLevelPackageName + .substring(topLevelPackageName + .lastIndexOf(".") + 1)) + + targetFilename); + } + else { + destinationFile = pathResolver.getFocusedIdentifier(path, + packagePath + separatorChar + targetFilename); + } + + // Different destination for assembly.xml + if ("assembly.xml".equals(targetFilename)) { + destinationFile = pathResolver.getFocusedIdentifier(path, "src" + + separatorChar + "main" + separatorChar + "assembly" + + separatorChar + targetFilename); + } + // Adjust name for Roo Annotation + else if (targetFilename.startsWith("RooAnnotation")) { + destinationFile = pathResolver.getFocusedIdentifier( + path, + packagePath + + separatorChar + + "Roo" + + StringUtils.capitalize(topLevelPackageName + .substring(topLevelPackageName + .lastIndexOf(".") + 1)) + ".java"); + } + + if (!fileManager.exists(destinationFile)) { + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), type.name().toLowerCase() + "/" + + targetFilename + "-template"); + OutputStream outputStream = null; + try { + // Read template and insert the user's package + String input = IOUtils.toString(templateInputStream); + input = input.replace("__TOP_LEVEL_PACKAGE__", + topLevelPackage.getFullyQualifiedPackageName()); + input = input + .replace("__APP_NAME__", StringUtils + .capitalize(topLevelPackageName + .substring(topLevelPackageName + .lastIndexOf(".") + 1))); + input = input.replace( + "__APP_NAME_LWR_CASE__", + topLevelPackageName.substring( + topLevelPackageName.lastIndexOf(".") + 1) + .toLowerCase()); + input = input.replace("__PROJECT_NAME__", + projectName.toLowerCase()); + + // Output the file for the user + final MutableFile mutableFile = fileManager + .createFile(destinationFile); + outputStream = mutableFile.getOutputStream(); + IOUtils.write(input, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException("Unable to create '" + + targetFilename + "'", ioe); + } + finally { + IOUtils.closeQuietly(templateInputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + private void installFlagGraphic(final Locale locale, + final String packagePath) { + boolean success = false; + final String countryCode = locale.getCountry().toLowerCase(); + + // Retrieve the icon file: + BufferedInputStream bis = null; + ZipInputStream zis = null; + try { + bis = new BufferedInputStream(httpService.openConnection(new URL( + iconSetUrl))); + zis = new ZipInputStream(bis); + ZipEntry entry; + final String expectedEntryName = "png/" + countryCode + ".png"; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals(expectedEntryName)) { + final MutableFile target = fileManager + .createFile(pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, packagePath + "/" + + countryCode + ".png")); + OutputStream outputStream = null; + try { + outputStream = target.getOutputStream(); + IOUtils.copy(zis, outputStream); + success = true; + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + } + } + catch (final IOException e) { + throw new IllegalStateException(getErrorMsg(locale.getCountry()), e); + } + finally { + IOUtils.closeQuietly(bis); + IOUtils.closeQuietly(zis); + } + + if (!success) { + throw new IllegalStateException(getErrorMsg(locale.toString())); + } + } + + public boolean isAddonCreatePossible() { + return !projectOperations.isFocusedProjectAvailable(); + } + + /** + * Writes the given Maven POM to disk + * + * @param pom the POM to write (required) + */ + private void writePomFile(final Document pom) { + final LogicalPath rootPath = LogicalPath.getInstance(Path.ROOT, ""); + final MutableFile pomFile = fileManager.createFile(pathResolver + .getIdentifier(rootPath, POM_XML)); + XmlUtils.writeXml(pomFile.getOutputStream(), pom); + } + + private void writeTextFile(final String fullPathFromRoot, + final String message) { + Validate.notBlank(fullPathFromRoot, + "Text file name to write is required"); + Validate.notBlank(message, "Message required"); + final String path = pathResolver.getFocusedIdentifier(Path.ROOT, + fullPathFromRoot); + final MutableFile mutableFile = fileManager.exists(path) ? fileManager + .updateFile(path) : fileManager.createFile(path); + OutputStream outputStream = null; + try { + outputStream = mutableFile.getOutputStream(); + IOUtils.write(message, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } +} diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Commands.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Commands.java-template new file mode 100644 index 000000000..156c508ca --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Commands.java-template @@ -0,0 +1,72 @@ +package __TOP_LEVEL_PACKAGE__; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Sample of a command class. The command class is registered by the Roo shell following an + * automatic classpath scan. You can provide simple user presentation-related logic in this + * class. You can return any objects from each method, or use the logger directly if you'd + * like to emit messages of different severity (and therefore different colours on + * non-Windows systems). + * + * @since 1.1 + */ +@Component // Use these Apache Felix annotations to register your commands class in the Roo container +@Service +public class __APP_NAME__Commands implements CommandMarker { // All command types must implement the CommandMarker interface + + /** + * Get a reference to the __APP_NAME__Operations from the underlying OSGi container + */ + @Reference private __APP_NAME__Operations operations; + + /** + * This method is optional. It allows automatic command hiding in situations when the command should not be visible. + * For example the 'entity' command will not be made available before the user has defined his persistence settings + * in the Roo shell or directly in the project. + * + * You can define multiple methods annotated with {@link CliAvailabilityIndicator} if your commands have differing + * visibility requirements. + * + * @return true (default) if the command should be visible at this stage, false otherwise + */ + @CliAvailabilityIndicator({ "__APP_NAME_LWR_CASE__ setup", "__APP_NAME_LWR_CASE__ add", "__APP_NAME_LWR_CASE__ all" }) + public boolean isCommandAvailable() { + return operations.isCommandAvailable(); + } + + /** + * This method registers a command with the Roo shell. It also offers a mandatory command attribute. + * + * @param type + */ + @CliCommand(value = "__APP_NAME_LWR_CASE__ add", help = "Some helpful description") + public void add(@CliOption(key = "type", mandatory = true, help = "The java type to apply this annotation to") JavaType target) { + operations.annotateType(target); + } + + /** + * This method registers a command with the Roo shell. It has no command attribute. + * + */ + @CliCommand(value = "__APP_NAME_LWR_CASE__ all", help = "Some helpful description") + public void all() { + operations.annotateAll(); + } + + /** + * This method registers a command with the Roo shell. It has no command attribute. + * + */ + @CliCommand(value = "__APP_NAME_LWR_CASE__ setup", help = "Setup __APP_NAME__ addon") + public void setup() { + operations.setup(); + } +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Metadata.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Metadata.java-template new file mode 100644 index 000000000..25a0f6176 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Metadata.java-template @@ -0,0 +1,148 @@ +package __TOP_LEVEL_PACKAGE__; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * This type produces metadata for a new ITD. It uses an {@link ItdTypeDetailsBuilder} provided by + * {@link AbstractItdTypeDetailsProvidingMetadataItem} to register a field in the ITD and a new method. + * + * @since 1.1.0 + */ +public class __APP_NAME__Metadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + // Constants + private static final String PROVIDES_TYPE_STRING = __APP_NAME__Metadata.class.getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING); + + public static final String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static final String createIdentifier(JavaType javaType, LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path); + } + + public static final JavaType getJavaType(String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType(PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static final LogicalPath getPath(String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static boolean isValid(String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public __APP_NAME__Metadata(String identifier, JavaType aspectName, PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), "Metadata identification string '" + identifier + "' does not appear to be a valid"); + + // Adding a new sample field definition + builder.addField(getSampleField()); + + // Adding a new sample method definition + builder.addMethod(getSampleMethod()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Create metadata for a field definition. + * + * @return a FieldMetadata object + */ + private FieldMetadata getSampleField() { + // Note private fields are private to the ITD, not the target type, this is undesirable if a dependent method is pushed in to the target type + int modifier = 0; + + // Using the FieldMetadataBuilder to create the field definition. + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder(getId(), // Metadata ID provided by supertype + modifier, // Using package protection rather than private + new ArrayList(), // No annotations for this field + new JavaSymbolName("sampleField"), // Field name + JavaType.STRING); // Field type + + return fieldBuilder.build(); // Build and return a FieldMetadata instance + } + + private MethodMetadata getSampleMethod() { + // Specify the desired method name + JavaSymbolName methodName = new JavaSymbolName("sampleMethod"); + + // Check if a method with the same signature already exists in the target type + final MethodMetadata method = methodExists(methodName, new ArrayList()); + if (method != null) { + // If it already exists, just return the method and omit its generation via the ITD + return method; + } + + // Define method annotations (none in this case) + List annotations = new ArrayList(); + + // Define method throws types (none in this case) + List throwsTypes = new ArrayList(); + + // Define method parameter types (none in this case) + List parameterTypes = new ArrayList(); + + // Define method parameter names (none in this case) + List parameterNames = new ArrayList(); + + // Create the method body + InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("System.out.println(\"Hello World\");"); + + // Use the MethodMetadataBuilder for easy creation of MethodMetadata + MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + methodBuilder.setThrowsTypes(throwsTypes); + + return methodBuilder.build(); // Build and return a MethodMetadata instance + } + + private MethodMetadata methodExists(JavaSymbolName methodName, List paramTypes) { + // We have no access to method parameter information, so we scan by name alone and treat any match as authoritative + // We do not scan the superclass, as the caller is expected to know we'll only scan the current class + for (MethodMetadata method : governorTypeDetails.getDeclaredMethods()) { + if (method.getMethodName().equals(methodName) && method.getParameterTypes().equals(paramTypes)) { + // Found a method of the expected name; we won't check method parameters though + return method; + } + } + return null; + } + + // Typically, no changes are required beyond this point + + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/MetadataProvider.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/MetadataProvider.java-template new file mode 100644 index 000000000..ec49f07c0 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/MetadataProvider.java-template @@ -0,0 +1,74 @@ +package __TOP_LEVEL_PACKAGE__; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Provides {@link __APP_NAME__Metadata}. This type is called by Roo to retrieve the metadata for this add-on. + * Use this type to reference external types and services needed by the metadata type. Register metadata triggers and + * dependencies here. Also define the unique add-on ITD identifier. + * + * @since 1.1 + */ +@Component +@Service +public final class __APP_NAME__MetadataProvider extends AbstractItdMetadataProvider { + + /** + * The activate method for this OSGi component, this will be called by the OSGi container upon bundle activation + * (result of the 'addon install' command) + * + * @param context the component context can be used to get access to the OSGi container (ie find out if certain bundles are active) + */ + protected void activate(ComponentContext context) { + metadataDependencyRegistry.registerDependency(PhysicalTypeIdentifier.getMetadataIdentiferType(), getProvidesType()); + addMetadataTrigger(new JavaType(Roo__APP_NAME__.class.getName())); + } + + /** + * The deactivate method for this OSGi component, this will be called by the OSGi container upon bundle deactivation + * (result of the 'addon uninstall' command) + * + * @param context the component context can be used to get access to the OSGi container (ie find out if certain bundles are active) + */ + protected void deactivate(ComponentContext context) { + metadataDependencyRegistry.deregisterDependency(PhysicalTypeIdentifier.getMetadataIdentiferType(), getProvidesType()); + removeMetadataTrigger(new JavaType(Roo__APP_NAME__.class.getName())); + } + + /** + * Return an instance of the Metadata offered by this add-on + */ + protected ItdTypeDetailsProvidingMetadataItem getMetadata(String metadataIdentificationString, JavaType aspectName, PhysicalTypeMetadata governorPhysicalTypeMetadata, String itdFilename) { + // Pass dependencies required by the metadata in through its constructor + return new __APP_NAME__Metadata(metadataIdentificationString, aspectName, governorPhysicalTypeMetadata); + } + + /** + * Define the unique ITD file name extension, here the resulting file name will be **_ROO___APP_NAME__.aj + */ + public String getItdUniquenessFilenameSuffix() { + return "__APP_NAME__"; + } + + protected String getGovernorPhysicalTypeIdentifier(String metadataIdentificationString) { + JavaType javaType = __APP_NAME__Metadata.getJavaType(metadataIdentificationString); + LogicalPath path = __APP_NAME__Metadata.getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + protected String createLocalIdentifier(JavaType javaType, LogicalPath path) { + return __APP_NAME__Metadata.createIdentifier(javaType, path); + } + + public String getProvidesType() { + return __APP_NAME__Metadata.getMetadataIdentiferType(); + } +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Operations.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Operations.java-template new file mode 100644 index 000000000..f31f61f26 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/Operations.java-template @@ -0,0 +1,33 @@ +package __TOP_LEVEL_PACKAGE__; + +import org.springframework.roo.model.JavaType; + +/** + * Interface of operations this add-on offers. Typically used by a command type or an external add-on. + * + * @since 1.1 + */ +public interface __APP_NAME__Operations { + + /** + * Indicate commands should be available + * + * @return true if it should be available, otherwise false + */ + boolean isCommandAvailable(); + + /** + * Annotate the provided Java type with the trigger of this add-on + */ + void annotateType(JavaType type); + + /** + * Annotate all Java types with the trigger of this add-on + */ + void annotateAll(); + + /** + * Setup all add-on artifacts (dependencies in this case) + */ + void setup(); +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/OperationsImpl.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/OperationsImpl.java-template new file mode 100644 index 000000000..fd9f364cf --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/OperationsImpl.java-template @@ -0,0 +1,107 @@ +package __TOP_LEVEL_PACKAGE__; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.DependencyScope; +import org.springframework.roo.project.DependencyType; +import org.springframework.roo.project.Repository; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Implementation of operations this add-on offers. + * + * @since 1.1 + */ +@Component // Use these Apache Felix annotations to register your commands class in the Roo container +@Service +public class __APP_NAME__OperationsImpl implements __APP_NAME__Operations { + + /** + * Use ProjectOperations to install new dependencies, plugins, properties, etc into the project configuration + */ + @Reference private ProjectOperations projectOperations; + + /** + * Use TypeLocationService to find types which are annotated with a given annotation in the project + */ + @Reference private TypeLocationService typeLocationService; + + /** + * Use TypeManagementService to change types + */ + @Reference private TypeManagementService typeManagementService; + + /** {@inheritDoc} */ + public boolean isCommandAvailable() { + // Check if a project has been created + return projectOperations.isFocusedProjectAvailable(); + } + + /** {@inheritDoc} */ + public void annotateType(JavaType javaType) { + // Use Roo's Assert type for null checks + Validate.notNull(javaType, "Java type required"); + + // Obtain ClassOrInterfaceTypeDetails for this java type + ClassOrInterfaceTypeDetails existing = typeLocationService.getTypeDetails(javaType); + + // Test if the annotation already exists on the target type + if (existing != null && MemberFindingUtils.getAnnotationOfType(existing.getAnnotations(), new JavaType(Roo__APP_NAME__.class.getName())) == null) { + ClassOrInterfaceTypeDetailsBuilder classOrInterfaceTypeDetailsBuilder = new ClassOrInterfaceTypeDetailsBuilder(existing); + + // Create JavaType instance for the add-ons trigger annotation + JavaType rooRoo__APP_NAME__ = new JavaType(Roo__APP_NAME__.class.getName()); + + // Create Annotation metadata + AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder(rooRoo__APP_NAME__); + + // Add annotation to target type + classOrInterfaceTypeDetailsBuilder.addAnnotation(annotationBuilder.build()); + + // Save changes to disk + typeManagementService.createOrUpdateTypeOnDisk(classOrInterfaceTypeDetailsBuilder.build()); + } + } + + /** {@inheritDoc} */ + public void annotateAll() { + // Use the TypeLocationService to scan project for all types with a specific annotation + for (JavaType type: typeLocationService.findTypesWithAnnotation(new JavaType("org.springframework.roo.addon.javabean.RooJavaBean"))) { + annotateType(type); + } + } + + /** {@inheritDoc} */ + public void setup() { + // Install the add-on Google code repository needed to get the annotation + projectOperations.addRepository("", new Repository("__APP_NAME__ Roo add-on repository", "__APP_NAME__ Roo add-on repository", "https://__PROJECT_NAME__.googlecode.com/svn/repo")); + + List dependencies = new ArrayList(); + + // Install the dependency on the add-on jar ( + dependencies.add(new Dependency("__TOP_LEVEL_PACKAGE__", "__TOP_LEVEL_PACKAGE__", "0.1.0.BUILD-SNAPSHOT", DependencyType.JAR, DependencyScope.PROVIDED)); + + // Install dependencies defined in external XML file + for (Element dependencyElement : XmlUtils.findElements("/configuration/batch/dependencies/dependency", XmlUtils.getConfiguration(getClass()))) { + dependencies.add(new Dependency(dependencyElement)); + } + + // Add all new dependencies to pom.xml + projectOperations.addDependencies("", dependencies); + } +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/RooAnnotation.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/RooAnnotation.java-template new file mode 100644 index 000000000..10a84e008 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/RooAnnotation.java-template @@ -0,0 +1,17 @@ +package __TOP_LEVEL_PACKAGE__; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Trigger annotation for this add-on. + + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface Roo__APP_NAME__ { +} + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/assembly.xml-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/assembly.xml-template new file mode 100644 index 000000000..940df7736 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/assembly.xml-template @@ -0,0 +1,72 @@ + + + assembly + + zip + + true + + + + / + + unix + true + + readme.txt + + + + /legal + legal + unix + true + + *.txt + *.TXT + + + + /dist + target + true + + *.jar + + + *-tests.jar + *-sources.jar + + + + /src + target + true + + *-tests.jar + *-sources.jar + + + + /src + + true + + pom.xml + + + + + + + lib + false + runtime + false + false + true + + + + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/configuration.xml-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/configuration.xml-template new file mode 100644 index 000000000..f13deeb5b --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/configuration.xml-template @@ -0,0 +1,16 @@ + + + + + + + + + + org.springframework.batch + spring-batch-admin-manager + 1.0.0.RELEASE + + + + \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/roo-addon-advanced-template.xml b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/roo-addon-advanced-template.xml new file mode 100644 index 000000000..8d5f08d60 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/advanced/roo-addon-advanced-template.xml @@ -0,0 +1,238 @@ + + + 4.0.0 + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + bundle + 0.1.0.BUILD-SNAPSHOT + TO_BE_CHANGED_BY_ADDON + + Your project/company name goes here (used in copyright and vendor information in the manifest) + + + + >GNU General Public License (GPL), Version 3.0 + http://www.gnu.org/copyleft/gpl.html + repo + + + An add-on created by Spring Roo's addon creator feature. + http://www.some.company + + 2.0.0.BUILD-SNAPSHOT + UTF-8 + ${project.name} + TO_BE_CHANGED_BY_ADDON + 5.0.0 + 1.20.0 + + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + + org.apache.felix + org.apache.felix.scr.annotations + 1.6.0 + + + + org.springframework.roo + org.springframework.roo.metadata + ${roo.version} + + + org.springframework.roo + org.springframework.roo.process.manager + ${roo.version} + + + org.springframework.roo + org.springframework.roo.project + ${roo.version} + + + org.springframework.roo + org.springframework.roo.support + ${roo.version} + + + org.springframework.roo + org.springframework.roo.shell + ${roo.version} + + + org.springframework.roo + org.springframework.roo.bootstrap + ${roo.version} + + + org.springframework.roo + org.springframework.roo.model + ${roo.version} + + + org.springframework.roo + org.springframework.roo.classpath + ${roo.version} + + + + org.apache.commons + commons-lang3 + 3.1 + + + + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + http://code.google.com/p/${google.code.project.name}/source/browse + + + + Google Code + dav:https://${google.code.project.name}.googlecode.com/svn/repo + + + + + + org.apache.maven.wagon + wagon-webdav-jackrabbit + 1.0-beta-6 + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.2 + + + copy-dependencies + package + + copy-dependencies + + + + + ${project.build.directory}/all + true + compile + org.apache.felix.scr.annotations + org.osgi + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2.1 + + + src/main/assembly/assembly.xml + + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + attach-sources + package + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.2 + + forked-path + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.3 + + + sign-artifacts + verify + + sign + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + ${project.artifactId} + Copyright ${project.organization.name}. All Rights Reserved. + ${project.url} + + true + httppgp://${google.code.project.name}.googlecode.com/svn/repo/${repo.folder}/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.6 + 1.6 + + + + org.apache.felix + maven-scr-plugin + ${scr.plugin.version} + + + generate-scr-scrdescriptor + + scr + + + + + false + + + + + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/Language.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/Language.java-template new file mode 100644 index 000000000..2dddf69ac --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/Language.java-template @@ -0,0 +1,34 @@ +package __TOP_LEVEL_PACKAGE__; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * __APP_NAME__ language support. + * + */ +@Component(immediate = true) +@Service +public class __APP_NAME__Language extends AbstractLanguage { + + public Locale getLocale() { + return new Locale("__LOCALE__"); + } + + public String getLanguage() { + return "__LANGUAGE__"; + } + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "__FLAG_FILE__"); + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "__MESSAGE_BUNDLE__"); + } +} diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/assembly.xml-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/assembly.xml-template new file mode 100644 index 000000000..8b85e1ae0 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/assembly.xml-template @@ -0,0 +1,74 @@ + + + + zip + + true + + + + / + + unix + true + + readme.txt + + + + /legal + legal + unix + true + + *.txt + *.TXT + + + + /dist + target + true + + *.jar + + + *-tests.jar + *-sources.jar + + + + /src + target + true + + *-tests.jar + *-sources.jar + + + + /src + + true + + pom.xml + + + + + + + lib + false + runtime + false + true + false + true + + + + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/iso3166.txt b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/iso3166.txt new file mode 100644 index 000000000..2a5a55722 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/iso3166.txt @@ -0,0 +1,246 @@ +AFGHANISTAN;AF +ÅLAND ISLANDS;AX +ALBANIA;AL +ALGERIA;DZ +AMERICAN SAMOA;AS +ANDORRA;AD +ANGOLA;AO +ANGUILLA;AI +ANTARCTICA;AQ +ANTIGUA AND BARBUDA;AG +ARGENTINA;AR +ARMENIA;AM +ARUBA;AW +AUSTRALIA;AU +AUSTRIA;AT +AZERBAIJAN;AZ +BAHAMAS;BS +BAHRAIN;BH +BANGLADESH;BD +BARBADOS;BB +BELARUS;BY +BELGIUM;BE +BELIZE;BZ +BENIN;BJ +BERMUDA;BM +BHUTAN;BT +BOLIVIA, PLURINATIONAL STATE OF;BO +BOSNIA AND HERZEGOVINA;BA +BOTSWANA;BW +BOUVET ISLAND;BV +BRAZIL;BR +BRITISH INDIAN OCEAN TERRITORY;IO +BRUNEI DARUSSALAM;BN +BULGARIA;BG +BURKINA FASO;BF +BURUNDI;BI +CAMBODIA;KH +CAMEROON;CM +CANADA;CA +CAPE VERDE;CV +CAYMAN ISLANDS;KY +CENTRAL AFRICAN REPUBLIC;CF +CHAD;TD +CHILE;CL +CHINA;CN +CHRISTMAS ISLAND;CX +COCOS (KEELING) ISLANDS;CC +COLOMBIA;CO +COMOROS;KM +CONGO;CG +CONGO, THE DEMOCRATIC REPUBLIC OF THE;CD +COOK ISLANDS;CK +COSTA RICA;CR +CÔTE D'IVOIRE;CI +CROATIA;HR +CUBA;CU +CYPRUS;CY +CZECH REPUBLIC;CZ +DENMARK;DK +DJIBOUTI;DJ +DOMINICA;DM +DOMINICAN REPUBLIC;DO +ECUADOR;EC +EGYPT;EG +EL SALVADOR;SV +EQUATORIAL GUINEA;GQ +ERITREA;ER +ESTONIA;EE +ETHIOPIA;ET +FALKLAND ISLANDS (MALVINAS);FK +FAROE ISLANDS;FO +FIJI;FJ +FINLAND;FI +FRANCE;FR +FRENCH GUIANA;GF +FRENCH POLYNESIA;PF +FRENCH SOUTHERN TERRITORIES;TF +GABON;GA +GAMBIA;GM +GEORGIA;GE +GERMANY;DE +GHANA;GH +GIBRALTAR;GI +GREECE;GR +GREENLAND;GL +GRENADA;GD +GUADELOUPE;GP +GUAM;GU +GUATEMALA;GT +GUERNSEY;GG +GUINEA;GN +GUINEA-BISSAU;GW +GUYANA;GY +HAITI;HT +HEARD ISLAND AND MCDONALD ISLANDS;HM +HOLY SEE (VATICAN CITY STATE);VA +HONDURAS;HN +HONG KONG;HK +HUNGARY;HU +ICELAND;IS +INDIA;IN +INDONESIA;ID +IRAN, ISLAMIC REPUBLIC OF;IR +IRAQ;IQ +IRELAND;IE +ISLE OF MAN;IM +ISRAEL;IL +ITALY;IT +JAMAICA;JM +JAPAN;JP +JERSEY;JE +JORDAN;JO +KAZAKHSTAN;KZ +KENYA;KE +KIRIBATI;KI +KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF;KP +KOREA, REPUBLIC OF;KR +KUWAIT;KW +KYRGYZSTAN;KG +LAO PEOPLE'S DEMOCRATIC REPUBLIC;LA +LATVIA;LV +LEBANON;LB +LESOTHO;LS +LIBERIA;LR +LIBYAN ARAB JAMAHIRIYA;LY +LIECHTENSTEIN;LI +LITHUANIA;LT +LUXEMBOURG;LU +MACAO;MO +MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF;MK +MADAGASCAR;MG +MALAWI;MW +MALAYSIA;MY +MALDIVES;MV +MALI;ML +MALTA;MT +MARSHALL ISLANDS;MH +MARTINIQUE;MQ +MAURITANIA;MR +MAURITIUS;MU +MAYOTTE;YT +MEXICO;MX +MICRONESIA, FEDERATED STATES OF;FM +MOLDOVA, REPUBLIC OF;MD +MONACO;MC +MONGOLIA;MN +MONTENEGRO;ME +MONTSERRAT;MS +MOROCCO;MA +MOZAMBIQUE;MZ +MYANMAR;MM +NAMIBIA;NA +NAURU;NR +NEPAL;NP +NETHERLANDS;NL +NETHERLANDS ANTILLES;AN +NEW CALEDONIA;NC +NEW ZEALAND;NZ +NICARAGUA;NI +NIGER;NE +NIGERIA;NG +NIUE;NU +NORFOLK ISLAND;NF +NORTHERN MARIANA ISLANDS;MP +NORWAY;NO +OMAN;OM +PAKISTAN;PK +PALAU;PW +PALESTINIAN TERRITORY, OCCUPIED;PS +PANAMA;PA +PAPUA NEW GUINEA;PG +PARAGUAY;PY +PERU;PE +PHILIPPINES;PH +PITCAIRN;PN +POLAND;PL +PORTUGAL;PT +PUERTO RICO;PR +QATAR;QA +RÉUNION;RE +ROMANIA;RO +RUSSIAN FEDERATION;RU +RWANDA;RW +SAINT BARTHÉLEMY;BL +SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA;SH +SAINT KITTS AND NEVIS;KN +SAINT LUCIA;LC +SAINT MARTIN;MF +SAINT PIERRE AND MIQUELON;PM +SAINT VINCENT AND THE GRENADINES;VC +SAMOA;WS +SAN MARINO;SM +SAO TOME AND PRINCIPE;ST +SAUDI ARABIA;SA +SENEGAL;SN +SERBIA;RS +SEYCHELLES;SC +SIERRA LEONE;SL +SINGAPORE;SG +SLOVAKIA;SK +SLOVENIA;SI +SOLOMON ISLANDS;SB +SOMALIA;SO +SOUTH AFRICA;ZA +SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS;GS +SPAIN;ES +SRI LANKA;LK +SUDAN;SD +SURINAME;SR +SVALBARD AND JAN MAYEN;SJ +SWAZILAND;SZ +SWEDEN;SE +SWITZERLAND;CH +SYRIAN ARAB REPUBLIC;SY +TAIWAN, PROVINCE OF CHINA;TW +TAJIKISTAN;TJ +TANZANIA, UNITED REPUBLIC OF;TZ +THAILAND;TH +TIMOR-LESTE;TL +TOGO;TG +TOKELAU;TK +TONGA;TO +TRINIDAD AND TOBAGO;TT +TUNISIA;TN +TURKEY;TR +TURKMENISTAN;TM +TURKS AND CAICOS ISLANDS;TC +TUVALU;TV +UGANDA;UG +UKRAINE;UA +UNITED ARAB EMIRATES;AE +UNITED KINGDOM;GB +UNITED STATES;US +UNITED STATES MINOR OUTLYING ISLANDS;UM +URUGUAY;UY +UZBEKISTAN;UZ +VANUATU;VU +VENEZUELA, BOLIVARIAN REPUBLIC OF;VE +VIET NAM;VN +VIRGIN ISLANDS, BRITISH;VG +VIRGIN ISLANDS, U.S.;VI +WALLIS AND FUTUNA;WF +WESTERN SAHARA;EH +YEMEN;YE +ZAMBIA;ZM +ZIMBABWE;ZW \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/roo-addon-i18n-template.xml b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/roo-addon-i18n-template.xml new file mode 100644 index 000000000..f263d0cb6 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/i18n/roo-addon-i18n-template.xml @@ -0,0 +1,188 @@ + + + 4.0.0 + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + bundle + 0.1.0.BUILD-SNAPSHOT + TO_BE_CHANGED_BY_ADDON + + Your project/company name goes here (used in copyright and vendor information in the manifest) + + + + >GNU General Public License (GPL), Version 3.0 + http://www.gnu.org/copyleft/gpl.html + repo + + + An add-on created by Spring Roo's addon creator feature. + http://www.some.company + + 2.0.0.BUILD-SNAPSHOT + UTF-8 + ${project.name} + TO_BE_CHANGED_BY_ADDON + 5.0.0 + 1.20.0 + + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + + org.apache.felix + org.apache.felix.scr.annotations + 1.6.0 + + + + org.springframework.roo + org.springframework.roo.support + ${roo.version} + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + ${roo.version} + + + + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + http://code.google.com/p/${google.code.project.name}/source/browse + + + + Google Code + dav:https://${google.code.project.name}.googlecode.com/svn/repo + + + + + + org.apache.maven.wagon + wagon-webdav-jackrabbit + 1.0-beta-6 + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.2 + + + copy-dependencies + package + + copy-dependencies + + + + + ${project.build.directory}/all + true + compile + org.apache.felix.scr.annotations + org.osgi + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2.1 + + + src/main/assembly/assembly.xml + + + + + org.apache.maven.plugins + maven-release-plugin + 2.2 + + forked-path + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.3 + + + sign-artifacts + verify + + sign + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + ${project.artifactId} + Copyright ${project.organization.name}. All Rights Reserved. + ${project.url} + + true + httppgp://${google.code.project.name}.googlecode.com/svn/repo/${repo.folder}/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.6 + 1.6 + + + + org.apache.felix + maven-scr-plugin + ${scr.plugin.version} + + + generate-scr-scrdescriptor + + scr + + + + + false + + + + + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/Commands.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/Commands.java-template new file mode 100644 index 000000000..5a34e881b --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/Commands.java-template @@ -0,0 +1,123 @@ +package __TOP_LEVEL_PACKAGE__; + +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Example of a command class. The command class is registered by the Roo shell following an + * automatic classpath scan. You can provide simple user presentation-related logic in this + * class. You can return any objects from each method, or use the logger directly if you'd + * like to emit messages of different severity (and therefore different colors on + * non-Windows systems). + * + * @since 1.1.1 + */ +@Component // Use these Apache Felix annotations to register your commands class in the Roo container +@Service +public class __APP_NAME__Commands implements CommandMarker { // All command types must implement the CommandMarker interface + + /** + * Get hold of a JDK Logger + */ + private Logger log = Logger.getLogger(getClass().getName()); + + /** + * Get a reference to the __APP_NAME__Operations from the underlying OSGi container + */ + @Reference private __APP_NAME__Operations operations; + + /** + * Get a reference to the StaticFieldConverter from the underlying OSGi container; + * this is useful for 'type save' command tab completions in the Roo shell + */ + @Reference private StaticFieldConverter staticFieldConverter; + + /** + * The activate method for this OSGi component, this will be called by the OSGi container upon bundle activation + * (result of the 'addon install' command) + * + * @param context the component context can be used to get access to the OSGi container (ie find out if certain bundles are active) + */ + protected void activate(ComponentContext context) { + staticFieldConverter.add(__APP_NAME__PropertyName.class); + } + + /** + * The deactivate method for this OSGi component, this will be called by the OSGi container upon bundle deactivation + * (result of the 'addon remove' command) + * + * @param context the component context can be used to get access to the OSGi container (ie find out if certain bundles are active) + */ + protected void deactivate(ComponentContext context) { + staticFieldConverter.remove(__APP_NAME__PropertyName.class); + } + + // ************************************************************************* + // Example 1 Printing colored messages to the shell + // ************************************************************************* + + /** + * This method is optional. It allows automatic command hiding in situations when the command should not be visible. + * For example the 'entity' command will not be made available before the user has defined his persistence settings + * in the Roo shell or directly in the project. + * + * You can define multiple methods annotated with {@link CliAvailabilityIndicator} if your commands have differing + * visibility requirements. + * + * @return true (default) if the command should be visible at this stage, false otherwise + */ + @CliAvailabilityIndicator("say hello") + public boolean isSayHelloAvailable() { + return true; // This command is always available! + } + + /** + * This method registers a command with the Roo shell. It also offers two command attributes, a mandatory one and an + * optional command which has a default value. + * + * @param name + * @param country + */ + @CliCommand(value = "say hello", help = "Prints welcome message to the Roo shell") + public void sayHello( + @CliOption(key = "name", mandatory = true, help = "State your name") String name, // A mandatory command attribute + @CliOption(key = "countryOfOrigin", mandatory = false, help = "Country of orgin") __APP_NAME__PropertyName country) { + + log.info("Welcome " + name + "!"); + log.warning("Country of origin: " + (country == null ? __APP_NAME__PropertyName.NOT_SPECIFIED.getPropertyName() : country.getPropertyName())); + log.info("It seems you are a running JDK " + operations.getProperty("java.version")); + log.info("You can use the default JDK logger anywhere in your add-on to send messages to the Roo shell"); + } + + // ************************************************************************* + // Example 2 Installing and replacing tagx files in the target project + // ************************************************************************* + + /** + * Define when "web mvc install tags" command should be visible in the Roo shell. + * In this case we want to hide the command until the WEB-INF/tags folder is present. + * + * @return true (default) if the command should be visible at this stage, false otherwise + */ + @CliAvailabilityIndicator("web mvc install tags") // Define the exact command name + public boolean isInstallTagsCommandAvailable() { + return operations.isInstallTagsCommandAvailable(); + } + + /** + * Replace existing MVC tagx files in the target project + */ + @CliCommand(value = "web mvc install tags", help="Replace default Roo MVC tags used for scaffolding") + public void installTags() { + operations.installTags(); + } +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/Operations.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/Operations.java-template new file mode 100644 index 000000000..89456797b --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/Operations.java-template @@ -0,0 +1,27 @@ +package __TOP_LEVEL_PACKAGE__; + +/** + * Interface of commands that are available via the Roo shell. + * + * @since 1.1.1 + */ +public interface __APP_NAME__Operations { + + /** + * Indicate of the install tags command should be available + * + * @return true if it should be available, otherwise false + */ + boolean isInstallTagsCommandAvailable(); + + /** + * @param propertyName to obtain (required) + * @return a message that will ultimately be displayed on the shell + */ + String getProperty(String propertyName); + + /** + * Install tags used for MVC scaffolded apps into the target project. + */ + void installTags(); +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/OperationsImpl.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/OperationsImpl.java-template new file mode 100644 index 000000000..58d87fbad --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/OperationsImpl.java-template @@ -0,0 +1,92 @@ +package __TOP_LEVEL_PACKAGE__; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; + +/** + * Implementation of {@link __APP_NAME__Operations} interface. + * + * @since 1.1.1 + */ +@Component +@Service +public class __APP_NAME__OperationsImpl implements __APP_NAME__Operations { + + private static final char SEPARATOR = File.separatorChar; + + /** + * Get a reference to the FileManager from the underlying OSGi container. Make sure you + * are referencing the Roo bundle which contains this service in your add-on pom.xml. + * + * Using the Roo file manager instead if java.io.File gives you automatic rollback in case + * an Exception is thrown. + */ + @Reference private FileManager fileManager; + + /** + * Get a reference to the ProjectOperations from the underlying OSGi container. Make sure you + * are referencing the Roo bundle which contains this service in your add-on pom.xml. + */ + @Reference private ProjectOperations projectOperations; + + /** {@inheritDoc} */ + public boolean isInstallTagsCommandAvailable() { + return projectOperations.isFocusedProjectAvailable() && fileManager.exists(projectOperations.getPathResolver().getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, "WEB-INF" + SEPARATOR + "tags")); + } + + /** {@inheritDoc} */ + public String getProperty(String propertyName) { + Validate.notBlank(propertyName, "Property name required"); + return System.getProperty(propertyName); + } + + /** {@inheritDoc} */ + public void installTags() { + // Use PathResolver to get canonical resource names for a given artifact + PathResolver pathResolver = projectOperations.getPathResolver(); + createOrReplaceFile(pathResolver.getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, "WEB-INF" + SEPARATOR + "tags" + SEPARATOR + "util"), "info.tagx"); + createOrReplaceFile(pathResolver.getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, "WEB-INF" + SEPARATOR + "tags" + SEPARATOR + "form"), "show.tagx"); + } + + /** + * A private method which illustrates how to reference and manipulate resources + * in the target project as well as the bundle classpath. + * + * @param path + * @param fileName + */ + private void createOrReplaceFile(String path, String fileName) { + String targetFile = path + SEPARATOR + fileName; + + // Use MutableFile in combination with FileManager to take advantage of Roo's transactional file handling which + // offers automatic rollback if an exception occurs + MutableFile mutableFile = fileManager.exists(targetFile) ? fileManager.updateFile(targetFile) : fileManager.createFile(targetFile); + InputStream inputStream = null; + OutputStream outputStream = null; + try { + // Use FileUtils to open an InputStream to a resource located in your bundle + inputStream = FileUtils.getInputStream(getClass(), fileName); + outputStream = mutableFile.getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/PropertyName.java-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/PropertyName.java-template new file mode 100644 index 000000000..bf235b6c0 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/PropertyName.java-template @@ -0,0 +1,33 @@ +package __TOP_LEVEL_PACKAGE__; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Example of an enum used for tab-completion of properties. + * + * @since 1.1.1 + */ +public enum __APP_NAME__PropertyName { + AUSTRALIA("Australia"), + UNITED_STATES("United States"), + GERMANY("Germany"), + NOT_SPECIFIED("None of your business!"); + + private String propertyName; + + private __APP_NAME__PropertyName(String propertyName) { + Validate.notBlank(propertyName, "Property name required"); + this.propertyName = propertyName; + } + + public String getPropertyName() { + return propertyName; + } + + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("propertyName", propertyName); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/assembly.xml-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/assembly.xml-template new file mode 100644 index 000000000..940df7736 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/assembly.xml-template @@ -0,0 +1,72 @@ + + + assembly + + zip + + true + + + + / + + unix + true + + readme.txt + + + + /legal + legal + unix + true + + *.txt + *.TXT + + + + /dist + target + true + + *.jar + + + *-tests.jar + *-sources.jar + + + + /src + target + true + + *-tests.jar + *-sources.jar + + + + /src + + true + + pom.xml + + + + + + + lib + false + runtime + false + false + true + + + + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/info.tagx-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/info.tagx-template new file mode 100644 index 000000000..7d2d23854 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/info.tagx-template @@ -0,0 +1,8 @@ + + + + + Thanks for submitting this document! + + + \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/roo-addon-simple-template.xml b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/roo-addon-simple-template.xml new file mode 100644 index 000000000..2f4b78e5a --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/roo-addon-simple-template.xml @@ -0,0 +1,243 @@ + + + 4.0.0 + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + bundle + 0.1.0.BUILD-SNAPSHOT + TO_BE_CHANGED_BY_ADDON + + Your project/company name goes here (used in copyright and vendor information in the manifest) + + + + >GNU General Public License (GPL), Version 3.0 + http://www.gnu.org/copyleft/gpl.html + repo + + + An add-on created by Spring Roo's addon creator feature. + http://www.some.company + + 2.0.0.BUILD-SNAPSHOT + UTF-8 + ${project.name} + TO_BE_CHANGED_BY_ADDON + 5.0.0 + 1.20.0 + + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + + org.apache.felix + org.apache.felix.scr.annotations + 1.6.0 + + + + org.springframework.roo + org.springframework.roo.metadata + ${roo.version} + + + org.springframework.roo + org.springframework.roo.process.manager + ${roo.version} + + + org.springframework.roo + org.springframework.roo.project + ${roo.version} + + + org.springframework.roo + org.springframework.roo.support + ${roo.version} + + + org.springframework.roo + org.springframework.roo.shell + ${roo.version} + + + org.springframework.roo + org.springframework.roo.bootstrap + ${roo.version} + + + org.springframework.roo + org.springframework.roo.model + ${roo.version} + + + org.springframework.roo + org.springframework.roo.classpath + ${roo.version} + + + + org.apache.commons + commons-lang3 + 3.1 + + + commons-io + commons-io + 2.1 + + + + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + http://code.google.com/p/${google.code.project.name}/source/browse + + + + Google Code + dav:https://${google.code.project.name}.googlecode.com/svn/repo + + + + + + org.apache.maven.wagon + wagon-webdav-jackrabbit + 1.0-beta-6 + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.1 + + + copy-dependencies + package + + copy-dependencies + + + + + ${project.build.directory}/all + true + compile + org.apache.felix.scr.annotations + org.osgi + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2.1 + + + src/main/assembly/assembly.xml + + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + attach-sources + package + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.2 + + forked-path + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.3 + + + sign-artifacts + verify + + sign + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + ${project.artifactId} + Copyright ${project.organization.name}. All Rights Reserved. + ${project.url} + + true + httppgp://${google.code.project.name}.googlecode.com/svn/repo/${repo.folder}/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.6 + 1.6 + + + + org.apache.felix + maven-scr-plugin + ${scr.plugin.version} + + + generate-scr-scrdescriptor + + scr + + + + + false + + + + + diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/show.tagx-template b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/show.tagx-template new file mode 100644 index 000000000..ac56f8485 --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/simple/show.tagx-template @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-creator/src/main/resources/org/springframework/roo/addon/creator/wrapper/roo-addon-wrapper-template.xml b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/wrapper/roo-addon-wrapper-template.xml new file mode 100644 index 000000000..2ef5525fe --- /dev/null +++ b/addon-creator/src/main/resources/org/springframework/roo/addon/creator/wrapper/roo-addon-wrapper-template.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + bundle + TO_BE_CHANGED_BY_ADDON + Spring Roo - Wrapping - ${pkgArtifactId} + This bundle wraps the standard Maven artifact: ${pkgArtifactId}-${pkgVersion}. + + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + ${pkgVersion}.0001 + TO_BE_CHANGED_BY_ADDON + + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + ${project.name} + + + + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + TO_BE_CHANGED_BY_ADDON + true + + + + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + scm:svn:https://${google.code.project.name}.googlecode.com/svn/trunk + http://code.google.com/p/${google.code.project.name}/source/browse + + + + Google Code + dav:https://${google.code.project.name}.googlecode.com/svn/repo + + + + + + org.apache.maven.wagon + wagon-webdav-jackrabbit + 1.0-beta-6 + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.3 + + + sign-artifacts + verify + + sign + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + true + httppgp://${google.code.project.name}.googlecode.com/svn/repo/${repo.folder}/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar + + ${project.artifactId} + *;version=${project.version} + ${pkgVendor} (wrapped into an OSGi bundle by the Spring Roo project build system) + ${pkgDocUrl} + ${pkgLicense} + + + + + + \ No newline at end of file diff --git a/addon-dbre/pom.xml b/addon-dbre/pom.xml new file mode 100644 index 000000000..2b182031e --- /dev/null +++ b/addon-dbre/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.dbre + bundle + Spring Roo - Addon - Database Reverse Engineering + Support for the incremental reverse engineering of existing databases. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.jdbc + + + org.springframework.roo + org.springframework.roo.addon.jpa + + + org.springframework.roo + org.springframework.roo.addon.layers.repository.jpa + + + org.springframework.roo + org.springframework.roo.addon.layers.service + + + org.springframework.roo + org.springframework.roo.addon.test + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.inflector + + + \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbManagedAnnotationValues.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbManagedAnnotationValues.java new file mode 100644 index 000000000..b0f72a043 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbManagedAnnotationValues.java @@ -0,0 +1,34 @@ +package org.springframework.roo.addon.dbre; + +import static org.springframework.roo.model.RooJavaType.ROO_DB_MANAGED; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; + +/** + * Represents a parsed {@link RooDbManaged} annotation. + * + * @author Alan Stewart + * @since 1.1.4 + */ +public class DbManagedAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private boolean automaticallyDelete = true; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public DbManagedAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, ROO_DB_MANAGED); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public boolean isAutomaticallyDelete() { + return automaticallyDelete; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreCommands.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreCommands.java new file mode 100644 index 000000000..a90121d19 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreCommands.java @@ -0,0 +1,63 @@ +package org.springframework.roo.addon.dbre; + +import java.io.File; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dbre.model.Schema; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Shell commands for {@link DbreOperations}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class DbreCommands implements CommandMarker { + + @Reference private DbreOperations dbreOperations; + + @CliCommand(value = "database introspect", help = "Displays database metadata") + public void displayDatabaseMetadata( + @CliOption(key = "schema", mandatory = true, optionContext = "schema", help = "The database schema names. Multiple schema names must be a double-quoted list separated by spaces") final Set schemas, + @CliOption(key = "file", mandatory = false, help = "The file to save the metadata to") final File file, + @CliOption(key = "enableViews", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Display database views") final boolean view) { + + dbreOperations.displayDatabaseMetadata(schemas, file, view); + } + + @CliAvailabilityIndicator({ "database introspect", + "database reverse engineer" }) + public boolean isDbreAvailable() { + return dbreOperations.isDbreInstallationPossible(); + } + + @CliCommand(value = "database reverse engineer", help = "Create and update entities based on database metadata") + public void serializeDatabaseMetadata( + @CliOption(key = "schema", mandatory = true, optionContext = "schema", help = "The database schema names. Multiple schema names must be a double-quoted list separated by spaces") final Set schemas, + @CliOption(key = "package", mandatory = false, help = "The package in which new entities will be placed") final JavaPackage destinationPackage, + @CliOption(key = "testAutomatically", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Create automatic integration tests for entities") final boolean testAutomatically, + @CliOption(key = "enableViews", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Reverse engineer database views") final boolean view, + @CliOption(key = "includeTables", mandatory = false, specifiedDefaultValue = "", optionContext = "include-tables", help = "The tables to include in reverse engineering. Multiple table names must be a double-quoted list separated by spaces") final Set includeTables, + @CliOption(key = "excludeTables", mandatory = false, specifiedDefaultValue = "", optionContext = "exclude-tables", help = "The tables to exclude from reverse engineering. Multiple table names must be a double-quoted list separated by spaces") final Set excludeTables, + @CliOption(key = "includeNonPortableAttributes", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Include non-portable JPA @Column attributes such as 'columnDefinition'") final boolean includeNonPortableAttributes, + @CliOption(key = "disableVersionFields", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Disable 'version' field") final boolean disableVersionFields, + @CliOption(key = "disableGeneratedIdentifiers", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Disable identifier auto generation") final boolean disableGeneratedIdentifiers, + @CliOption(key = "activeRecord", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "true", help = "Generate CRUD active record methods for each entity") final boolean activeRecord, + @CliOption(key = "repository", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Generate a repository for each entity") final boolean repository, + @CliOption(key = "service", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Generate a service for each entity") final boolean service) { + + dbreOperations.reverseEngineerDatabase(schemas, destinationPackage, + testAutomatically, view, includeTables, excludeTables, + includeNonPortableAttributes, disableVersionFields, + disableGeneratedIdentifiers, activeRecord, repository, service); + } +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreDatabaseListener.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreDatabaseListener.java new file mode 100644 index 000000000..931866274 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreDatabaseListener.java @@ -0,0 +1,11 @@ +package org.springframework.roo.addon.dbre; + +/** + * Responds to discovery of database structural information from the DBRE XML + * file and creates and manages entities based on this information. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface DbreDatabaseListener { +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreDatabaseListenerImpl.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreDatabaseListenerImpl.java new file mode 100644 index 000000000..c1712b365 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreDatabaseListenerImpl.java @@ -0,0 +1,771 @@ +package org.springframework.roo.addon.dbre; + +import static org.springframework.roo.addon.dbre.model.DbreModelService.DBRE_XML; +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.RooJavaType.ROO_DB_MANAGED; +import static org.springframework.roo.model.RooJavaType.ROO_IDENTIFIER; +import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; +import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dbre.model.Column; +import org.springframework.roo.addon.dbre.model.Database; +import org.springframework.roo.addon.dbre.model.DbreModelService; +import org.springframework.roo.addon.dbre.model.Table; +import org.springframework.roo.addon.jpa.identifier.Identifier; +import org.springframework.roo.addon.jpa.identifier.IdentifierService; +import org.springframework.roo.addon.layers.repository.jpa.RepositoryJpaOperations; +import org.springframework.roo.addon.layers.service.ServiceOperations; +import org.springframework.roo.addon.test.IntegrationTestOperations; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.file.monitor.event.FileEvent; +import org.springframework.roo.file.monitor.event.FileEventListener; +import org.springframework.roo.file.monitor.event.FileOperation; +import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Implementation of {@link DbreDatabaseListener}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class DbreDatabaseListenerImpl extends + AbstractHashCodeTrackingMetadataNotifier implements IdentifierService, + FileEventListener { + + private static final JavaSymbolName DB_MANAGED = new JavaSymbolName( + "dbManaged"); + private static final String IDENTIFIER_TYPE = "identifierType"; + private static final String PRIMARY_KEY_SUFFIX = "PK"; + private static final String SEQUENCE_NAME_FIELD = "sequenceName"; + private static final String VERSION = "version"; + private static final String VERSION_FIELD = "versionField"; + + @Reference private DbreModelService dbreModelService; + @Reference private FileManager fileManager; + @Reference private IntegrationTestOperations integrationTestOperations; + @Reference private ProjectOperations projectOperations; + @Reference private RepositoryJpaOperations repositoryJpaOperations; + @Reference private ServiceOperations serviceOperations; + @Reference private Shell shell; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + private Map> identifierResults; + + private void createIdentifierClass(final JavaType identifierType) { + final List identifierAnnotations = new ArrayList(); + + final AnnotationMetadataBuilder identifierBuilder = new AnnotationMetadataBuilder( + ROO_IDENTIFIER); + identifierBuilder.addBooleanAttribute(DB_MANAGED.getSymbolName(), true); + identifierAnnotations.add(identifierBuilder); + + // Produce identifier itself + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(identifierType, projectOperations + .getPathResolver().getFocusedPath(Path.SRC_MAIN_JAVA)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC | Modifier.FINAL, + identifierType, PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(identifierAnnotations); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + + shell.flash(Level.FINE, + "Created " + identifierType.getFullyQualifiedTypeName(), + DbreDatabaseListenerImpl.class.getName()); + shell.flash(Level.FINE, "", DbreDatabaseListenerImpl.class.getName()); + } + + /** + * Creates a new DBRE-managed entity from the given table + * + * @param javaType the name of the entity to be created (required) + * @param table the table from which to create the entity (required) + * @param activeRecord whether to create "active record" CRUD methods in the + * new entity + * @return the newly created entity + */ + private ClassOrInterfaceTypeDetails createNewManagedEntityFromTable( + final JavaType javaType, final Table table, + final boolean activeRecord) { + // Create type annotations for new entity + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(ROO_JAVA_BEAN)); + annotations.add(new AnnotationMetadataBuilder(ROO_TO_STRING)); + + // Find primary key from db metadata and add identifier attributes to + // @RooJpaEntity + final AnnotationMetadataBuilder jpaAnnotationBuilder = new AnnotationMetadataBuilder( + activeRecord ? ROO_JPA_ACTIVE_RECORD : ROO_JPA_ENTITY); + manageIdentifier(javaType, jpaAnnotationBuilder, + new HashSet(), table); + + if (!hasVersionField(table)) { + jpaAnnotationBuilder.addStringAttribute(VERSION_FIELD, ""); + } + if (table.isDisableGeneratedIdentifiers()) { + jpaAnnotationBuilder.addStringAttribute(SEQUENCE_NAME_FIELD, ""); + } + + jpaAnnotationBuilder.addStringAttribute("table", table.getName()); + if (!DbreModelService.NO_SCHEMA_REQUIRED.equals(table.getSchema() + .getName())) { + jpaAnnotationBuilder.addStringAttribute("schema", table.getSchema() + .getName()); + } + + annotations.add(jpaAnnotationBuilder); + + // Add @RooDbManaged + annotations.add(getRooDbManagedAnnotation()); + + final JavaType superclass = OBJECT; + final List extendsTypes = new ArrayList(); + extendsTypes.add(superclass); + + // Create entity class + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(javaType, projectOperations.getPathResolver() + .getFocusedPath(Path.SRC_MAIN_JAVA)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, javaType, + PhysicalTypeCategory.CLASS); + cidBuilder.setExtendsTypes(extendsTypes); + cidBuilder.setAnnotations(annotations); + + final ClassOrInterfaceTypeDetails entity = cidBuilder.build(); + typeManagementService.createOrUpdateTypeOnDisk(entity); + + shell.flash(Level.FINE, + "Created " + javaType.getFullyQualifiedTypeName(), + DbreDatabaseListenerImpl.class.getName()); + shell.flash(Level.FINE, "", DbreDatabaseListenerImpl.class.getName()); + + return entity; + } + + /** + * Deletes the given {@link JavaType} for the given reason + * + * @param javaType the type to be deleted (required) + * @param reason the reason for deletion (can be blank) + */ + private void deleteJavaType(final JavaType javaType, final String reason) { + final PhysicalTypeMetadata governorPhysicalTypeMetadata = getPhysicalTypeMetadata(javaType); + if (governorPhysicalTypeMetadata != null) { + final String filePath = governorPhysicalTypeMetadata + .getPhysicalLocationCanonicalPath(); + if (fileManager.exists(filePath)) { + fileManager.delete(filePath, reason); + shell.flash(Level.FINE, + "Deleted " + javaType.getFullyQualifiedTypeName(), + DbreDatabaseListenerImpl.class.getName()); + } + + shell.flash(Level.FINE, "", + DbreDatabaseListenerImpl.class.getName()); + } + } + + private void deleteManagedType( + final ClassOrInterfaceTypeDetails managedEntity, final String reason) { + if (!isEntityDeletable(managedEntity)) { + return; + } + deleteJavaType(managedEntity.getName(), reason); + + final JavaType identifierType = getIdentifierType(managedEntity + .getName()); + for (final ClassOrInterfaceTypeDetails managedIdentifier : getManagedIdentifiers()) { + if (managedIdentifier.getName().equals(identifierType)) { + deleteJavaType( + identifierType, + "managed identifier of deleted type " + + managedEntity.getName()); + break; + } + } + } + + private void deserializeDatabase() { + final Database database = dbreModelService.getDatabase(true); + if (database != null) { + identifierResults = new LinkedHashMap>(); + reverseEngineer(database); + } + } + + private JavaPackage getDestinationPackage(final Database database, + final Set managedEntities) { + JavaPackage destinationPackage = database.getDestinationPackage(); + if (destinationPackage == null) { + if (!managedEntities.isEmpty() && !database.hasMultipleSchemas()) { + // Take the package of the first one + destinationPackage = managedEntities.iterator().next() + .getName().getPackage(); + } + } + + // Fall back to project's top level package + if (destinationPackage == null) { + destinationPackage = projectOperations.getFocusedTopLevelPackage(); + } + return destinationPackage; + } + + public List getIdentifiers(final JavaType pkType) { + if (identifierResults == null) { + // Need to populate the identifier results before returning from + // this method + deserializeDatabase(); + } + if (identifierResults == null) { + // It's still null, so maybe the DBRE XML file isn't available at + // this time or similar + return null; + } + return identifierResults.get(pkType); + } + + private List getIdentifiers(final Table table, + final boolean usePrimaryKeys) { + final List result = new ArrayList(); + + // Add fields to the identifier class + final Set columns = usePrimaryKeys ? table.getPrimaryKeys() + : table.getColumns(); + for (final Column column : columns) { + final String columnName = column.getName(); + JavaSymbolName fieldName; + try { + fieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(columnName)); + } + catch (final RuntimeException e) { + throw new IllegalArgumentException( + "Failed to create field name for column '" + columnName + + "' in table '" + table.getName() + "': " + + e.getMessage()); + } + final JavaType fieldType = column.getJavaType(); + final String columnDefinition = table + .isIncludeNonPortableAttributes() ? column.getTypeName() + : ""; + result.add(new Identifier(fieldName, fieldType, columnName, column + .getColumnSize(), column.getScale(), columnDefinition)); + } + return result; + } + + private List getIdentifiersFromColumns(final Table table) { + return getIdentifiers(table, false); + } + + private List getIdentifiersFromPrimaryKeys(final Table table) { + return getIdentifiers(table, true); + } + + /** + * Returns the type of ID that DBRE should use for the given entity + * + * @param entity the entity for which to get the ID type (required) + * @return a non-null ID type + */ + private JavaType getIdentifierType(final JavaType entity) { + final PhysicalTypeMetadata governorPhysicalTypeMetadata = getPhysicalTypeMetadata(entity); + if (governorPhysicalTypeMetadata != null) { + final ClassOrInterfaceTypeDetails governorTypeDetails = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + final AnnotationMetadata jpaAnnotation = getJpaAnnotation(governorTypeDetails); + if (jpaAnnotation != null) { + final AnnotationAttributeValue identifierTypeAttribute = jpaAnnotation + .getAttribute(new JavaSymbolName(IDENTIFIER_TYPE)); + if (identifierTypeAttribute != null) { + // The identifierType attribute exists, so get its value + final JavaType identifierType = (JavaType) identifierTypeAttribute + .getValue(); + if (identifierType != null + && !JdkJavaType.isPartOfJavaLang(identifierType)) { + return identifierType; + } + } + } + } + + // The JPA annotation's "identifierType" attribute does not exist or is + // not a simple type, so return a default + return new JavaType(entity.getFullyQualifiedTypeName() + + PRIMARY_KEY_SUFFIX); + } + + /** + * Returns the JPA-related annotation on the given managed entity + * + * @param managedEntity an existing DBRE-managed entity (required) + * @return null if there isn't one + */ + private AnnotationMetadata getJpaAnnotation( + final ClassOrInterfaceTypeDetails managedEntity) { + // The @RooJpaEntity annotation takes precedence if present + final AnnotationMetadata rooJpaEntity = managedEntity + .getAnnotation(ROO_JPA_ENTITY); + if (rooJpaEntity != null) { + return rooJpaEntity; + } + return managedEntity.getAnnotation(ROO_JPA_ACTIVE_RECORD); + } + + private Set getManagedIdentifiers() { + final Set managedIdentifierTypes = new LinkedHashSet(); + + final Set identifierTypes = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_IDENTIFIER); + for (final ClassOrInterfaceTypeDetails managedIdentifierType : identifierTypes) { + final AnnotationMetadata identifierAnnotation = managedIdentifierType + .getTypeAnnotation(ROO_IDENTIFIER); + final AnnotationAttributeValue attrValue = identifierAnnotation + .getAttribute(DB_MANAGED); + if (attrValue != null && (Boolean) attrValue.getValue()) { + managedIdentifierTypes.add(managedIdentifierType); + } + } + return managedIdentifierTypes; + } + + private PhysicalTypeMetadata getPhysicalTypeMetadata(final JavaType javaType) { + final String declaredByMetadataId = typeLocationService + .getPhysicalTypeIdentifier(javaType); + if (StringUtils.isBlank(declaredByMetadataId)) { + return null; + } + return (PhysicalTypeMetadata) getMetadataService().get(declaredByMetadataId); + } + + private AnnotationMetadataBuilder getRooDbManagedAnnotation() { + final AnnotationMetadataBuilder rooDbManagedBuilder = new AnnotationMetadataBuilder( + ROO_DB_MANAGED); + rooDbManagedBuilder.addBooleanAttribute("automaticallyDelete", true); + return rooDbManagedBuilder; + } + + /** + * Indicates whether the given entity has the standard annotations applied + * by Roo, and no others. + * + * @param entity the entity to check (required) + * @return false if any of the standard ones are missing or any + * extra ones have been added + */ + private boolean hasStandardEntityAnnotations( + final ClassOrInterfaceTypeDetails entity) { + final List typeAnnotations = entity + .getAnnotations(); + // We expect four: RooDbManaged, RooJavaBean, RooToString, and either + // RooEntity or RooJpaEntity + if (typeAnnotations.size() != 4) { + return false; + } + // There are exactly four - check for any non-standard ones + for (final AnnotationMetadata annotation : typeAnnotations) { + final JavaType annotationType = annotation.getAnnotationType(); + final boolean entityAnnotation = ROO_JPA_ACTIVE_RECORD + .equals(annotationType) + || ROO_JPA_ENTITY.equals(annotationType); + if (!entityAnnotation && !ROO_DB_MANAGED.equals(annotationType) + && !ROO_JAVA_BEAN.equals(annotationType) + && !ROO_TO_STRING.equals(annotationType)) { + return false; + } + } + return true; + } + + private boolean hasVersionField(final Table table) { + if (!table.isDisableVersionFields()) { + for (final Column column : table.getColumns()) { + if (VERSION.equalsIgnoreCase(column.getName())) { + return true; + } + } + } + return false; + } + + /** + * Indicates whether active record CRUD methods should be generated for the + * given entities (this being an all or nothing decision) + * + * @param database the database being reverse-engineered (required) + * @param managedEntities any existing DB-managed entities in the user + * project (can be null or empty) + * @return see above + */ + private boolean isActiveRecord(final Database database, + final Collection managedEntities) { + if (CollectionUtils.isEmpty(managedEntities)) { + // There are no existing entities; use the given setting + return database.isActiveRecord(); + } + /* + * There are one or more existing entities; preserve the existing + * decision, based on the first such entity. This saves the user from + * having to enter the same value for the "activeRecord" option each + * time they run the database reverse engineer command. + */ + return managedEntities.iterator().next() + .getAnnotation(ROO_JPA_ACTIVE_RECORD) != null; + } + + private boolean isEntityDeletable( + final ClassOrInterfaceTypeDetails managedEntity) { + final String declaredByMetadataId = DbreMetadata.createIdentifier( + managedEntity.getName(), PhysicalTypeIdentifier + .getPath(managedEntity.getDeclaredByMetadataId())); + final DbreMetadata dbreMetadata = (DbreMetadata) getMetadataService() + .get(declaredByMetadataId); + if (dbreMetadata == null || !dbreMetadata.isAutomaticallyDelete()) { + return false; + } + + // Check whether the type's annotations have been customised + if (!hasStandardEntityAnnotations(managedEntity)) { + return false; + } + + // Finally, check for added constructors, fields and methods + return managedEntity.getDeclaredConstructors().isEmpty() + && managedEntity.getDeclaredFields().isEmpty() + && managedEntity.getDeclaredMethods().isEmpty(); + } + + private boolean isIdentifierDeletable(final JavaType identifierType) { + final PhysicalTypeMetadata governorPhysicalTypeMetadata = getPhysicalTypeMetadata(identifierType); + if (governorPhysicalTypeMetadata == null) { + return false; + } + + // Check for added constructors, fields and methods + final ClassOrInterfaceTypeDetails managedIdentifier = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + return managedIdentifier.getDeclaredConstructors().isEmpty() + && managedIdentifier.getDeclaredFields().isEmpty() + && managedIdentifier.getDeclaredMethods().isEmpty(); + } + + private void manageIdentifier(final JavaType javaType, + final AnnotationMetadataBuilder jpaAnnotationBuilder, + final Set attributesToDeleteIfPresent, + final Table table) { + final JavaType identifierType = getIdentifierType(javaType); + final PhysicalTypeMetadata identifierPhysicalTypeMetadata = getPhysicalTypeMetadata(identifierType); + + // Process primary keys and add 'identifierType' attribute + final int pkCount = table.getPrimaryKeyCount(); + if (pkCount == 1) { + // Table has one primary key + // Check for redundant, managed identifier class and delete if found + if (isIdentifierDeletable(identifierType)) { + deleteJavaType(identifierType, "the " + table.getName() + + " table has only one primary key"); + } + + attributesToDeleteIfPresent + .add(new JavaSymbolName(IDENTIFIER_TYPE)); + + // We don't need a PK class, so we just tell the + // JpaActiveRecordProvider via IdentifierService the column name, + // field type and field name to use + final List identifiers = getIdentifiersFromPrimaryKeys(table); + identifierResults.put(javaType, identifiers); + } + else if (pkCount == 0 || pkCount > 1) { + // Table has either no primary keys or more than one primary key so + // create a composite key + + // Check if identifier class already exists and if not, create it + if (identifierPhysicalTypeMetadata == null + || !identifierPhysicalTypeMetadata.isValid() + || identifierPhysicalTypeMetadata + .getMemberHoldingTypeDetails() == null) { + createIdentifierClass(identifierType); + } + + jpaAnnotationBuilder.addClassAttribute(IDENTIFIER_TYPE, + identifierType); + + // We need a PK class, so we tell the IdentifierMetadataProvider via + // IdentifierService the various column names, field types and field + // names to use + // For tables with no primary keys, create a composite key using all + // the table's columns + final List identifiers = pkCount == 0 ? getIdentifiersFromColumns(table) + : getIdentifiersFromPrimaryKeys(table); + identifierResults.put(identifierType, identifiers); + } + } + + private void notify(final List entities) { + for (final ClassOrInterfaceTypeDetails managedIdentifierType : getManagedIdentifiers()) { + final MetadataItem metadataItem = getMetadataService() + .evictAndGet(managedIdentifierType + .getDeclaredByMetadataId()); + if (metadataItem != null) { + notifyIfRequired(metadataItem); + } + } + + for (final ClassOrInterfaceTypeDetails entity : entities) { + final MetadataItem metadataItem = getMetadataService() + .evictAndGet(entity.getDeclaredByMetadataId()); + if (metadataItem != null) { + notifyIfRequired(metadataItem); + } + } + } + + public void onFileEvent(final FileEvent fileEvent) { + if (fileEvent.getFileDetails().getCanonicalPath().endsWith(DBRE_XML)) { + final FileOperation operation = fileEvent.getOperation(); + if (operation == FileOperation.UPDATED + || operation == FileOperation.CREATED) { + deserializeDatabase(); + } + } + } + + private void reverseEngineer(final Database database) { + final Set managedEntities = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_DB_MANAGED); + // Determine whether to create "active record" CRUD methods + database.setActiveRecord(isActiveRecord(database, managedEntities)); + + // Lookup the relevant destination package if not explicitly given + final JavaPackage destinationPackage = getDestinationPackage(database, + managedEntities); + + // Set the destination package in the database + database.setDestinationPackage(destinationPackage); + + // Get tables from database + final Set tables = new LinkedHashSet
    (database.getTables()); + + // Manage existing entities with @RooDbManaged annotation + for (final ClassOrInterfaceTypeDetails managedEntity : managedEntities) { + // Remove table from set as each managed entity is processed. + // The tables that remain in the set will be used for creation of + // new entities later + final Table table = updateOrDeleteManagedEntity(managedEntity, + database); + if (table != null) { + tables.remove(table); + } + } + + // Create new entities from tables + final List newEntities = new ArrayList(); + for (final Table table : tables) { + // Don't create types from join tables in many-to-many associations + if (!table.isJoinTable()) { + JavaPackage schemaPackage = destinationPackage; + if (database.hasMultipleSchemas()) { + schemaPackage = new JavaPackage( + destinationPackage.getFullyQualifiedPackageName() + + "." + + DbreTypeUtils.suggestPackageName(table + .getSchema().getName())); + } + final JavaType javaType = DbreTypeUtils + .suggestTypeNameForNewTable(table.getName(), + schemaPackage); + final boolean activeRecord = database.isActiveRecord() + && !database.isRepository(); + if (typeLocationService.getTypeDetails(javaType) == null) { + table.setIncludeNonPortableAttributes(database + .isIncludeNonPortableAttributes()); + table.setDisableVersionFields(database + .isDisableVersionFields()); + table.setDisableGeneratedIdentifiers(database + .isDisableGeneratedIdentifiers()); + newEntities.add(createNewManagedEntityFromTable(javaType, + table, activeRecord)); + } + } + } + + // Create repositories if required + if (database.isRepository()) { + for (final ClassOrInterfaceTypeDetails entity : newEntities) { + final JavaType type = entity.getType(); + repositoryJpaOperations.setupRepository( + new JavaType(type.getFullyQualifiedTypeName() + + "Repository"), type); + } + } + + // Create services if required + if (database.isService()) { + for (final ClassOrInterfaceTypeDetails entity : newEntities) { + final JavaType type = entity.getType(); + final String typeName = type.getFullyQualifiedTypeName(); + serviceOperations.setupService(new JavaType(typeName + + "Service"), new JavaType(typeName + "ServiceImpl"), + type, false, "", false, false); + } + } + + // Create integration tests if required + if (database.isTestAutomatically()) { + for (final ClassOrInterfaceTypeDetails entity : newEntities) { + integrationTestOperations.newIntegrationTest(entity.getType()); + } + } + + // Notify + final List allEntities = new ArrayList(); + allEntities.addAll(newEntities); + allEntities.addAll(managedEntities); + notify(allEntities); + } + + private Table updateOrDeleteManagedEntity( + final ClassOrInterfaceTypeDetails managedEntity, + final Database database) { + // Update the attributes of the existing JPA-related annotation + final AnnotationMetadata jpaAnnotation = getJpaAnnotation(managedEntity); + Validate.validState(jpaAnnotation != null, + "Neither @%s nor @%s found on existing DBRE-managed entity %s", + ROO_JPA_ACTIVE_RECORD.getSimpleTypeName(), ROO_JPA_ENTITY + .getSimpleTypeName(), managedEntity.getName() + .getFullyQualifiedTypeName()); + + // Find table in database using 'table' and 'schema' attributes from the + // JPA annotation + final AnnotationAttributeValue tableAttribute = jpaAnnotation + .getAttribute(new JavaSymbolName("table")); + final String errMsg = "Unable to maintain database-managed entity " + + managedEntity.getName().getFullyQualifiedTypeName() + + " because its associated table could not be found"; + Validate.notNull(tableAttribute, errMsg); + final String tableName = (String) tableAttribute.getValue(); + Validate.notBlank(tableName, errMsg); + + final AnnotationAttributeValue schemaAttribute = jpaAnnotation + .getAttribute(new JavaSymbolName("schema")); + final String schemaName = schemaAttribute != null ? (String) schemaAttribute + .getValue() : null; + + final Table table = database.getTable(tableName, schemaName); + if (table == null) { + // Table is missing and probably has been dropped so delete managed + // type and its identifier if applicable + deleteManagedType(managedEntity, "no database table called '" + + tableName + "'"); + return null; + } + + table.setIncludeNonPortableAttributes(database + .isIncludeNonPortableAttributes()); + table.setDisableVersionFields(database.isDisableVersionFields()); + table.setDisableGeneratedIdentifiers(database + .isDisableGeneratedIdentifiers()); + + // Update the @RooJpaEntity/@RooJpaActiveRecord attributes + final AnnotationMetadataBuilder jpaAnnotationBuilder = new AnnotationMetadataBuilder( + jpaAnnotation); + final Set attributesToDeleteIfPresent = new LinkedHashSet(); + manageIdentifier(managedEntity.getName(), jpaAnnotationBuilder, + attributesToDeleteIfPresent, table); + + // Manage versionField attribute + final AnnotationAttributeValue versionFieldAttribute = jpaAnnotation + .getAttribute(new JavaSymbolName(VERSION_FIELD)); + if (versionFieldAttribute == null) { + if (hasVersionField(table)) { + attributesToDeleteIfPresent.add(new JavaSymbolName( + VERSION_FIELD)); + } + else { + jpaAnnotationBuilder.addStringAttribute(VERSION_FIELD, ""); + } + } + else { + final String versionFieldValue = (String) versionFieldAttribute + .getValue(); + if (hasVersionField(table) + && (StringUtils.isBlank(versionFieldValue) || VERSION + .equals(versionFieldValue))) { + attributesToDeleteIfPresent.add(new JavaSymbolName( + VERSION_FIELD)); + } + } + + final AnnotationAttributeValue sequenceNameFieldAttribute = jpaAnnotation + .getAttribute(new JavaSymbolName(SEQUENCE_NAME_FIELD)); + if (sequenceNameFieldAttribute == null) { + if (!table.isDisableGeneratedIdentifiers()) { + attributesToDeleteIfPresent.add(new JavaSymbolName( + SEQUENCE_NAME_FIELD)); + } + else { + jpaAnnotationBuilder + .addStringAttribute(SEQUENCE_NAME_FIELD, ""); + } + } + else { + final String sequenceNameFieldValue = (String) sequenceNameFieldAttribute + .getValue(); + if (!table.isDisableGeneratedIdentifiers() + && ("".equals(sequenceNameFieldValue))) { + attributesToDeleteIfPresent.add(new JavaSymbolName( + SEQUENCE_NAME_FIELD)); + } + } + + // Update the annotation on disk + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + managedEntity); + cidBuilder.updateTypeAnnotation(jpaAnnotationBuilder.build(), + attributesToDeleteIfPresent); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + return table; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadata.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadata.java new file mode 100644 index 000000000..7edec676b --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadata.java @@ -0,0 +1,1178 @@ +package org.springframework.roo.addon.dbre; + +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JdkJavaType.SET; +import static org.springframework.roo.model.JpaJavaType.CASCADE_TYPE; +import static org.springframework.roo.model.JpaJavaType.COLUMN; +import static org.springframework.roo.model.JpaJavaType.JOIN_COLUMN; +import static org.springframework.roo.model.JpaJavaType.JOIN_COLUMNS; +import static org.springframework.roo.model.JpaJavaType.JOIN_TABLE; +import static org.springframework.roo.model.JpaJavaType.LOB; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.TEMPORAL; +import static org.springframework.roo.model.JpaJavaType.TEMPORAL_TYPE; +import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; +import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.jvnet.inflector.Noun; +import org.springframework.roo.addon.dbre.model.CascadeAction; +import org.springframework.roo.addon.dbre.model.Column; +import org.springframework.roo.addon.dbre.model.Database; +import org.springframework.roo.addon.dbre.model.ForeignKey; +import org.springframework.roo.addon.dbre.model.Reference; +import org.springframework.roo.addon.dbre.model.Table; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooDbManaged}. + *

    + * Creates and manages entity relationships, such as many-valued and + * single-valued associations. + *

    + * One-to-many and one-to-one associations are created based on the following + * laws: + *

      + *
    • Primary Key (PK) - Foreign Key (FK) LAW #1: If the foreign key column is + * part of the primary key (or part of an index) then the relationship between + * the tables will be one to many (1:M). + *
    • Primary Key (PK) - Foreign Key (FK) LAW #2: If the foreign key column + * represents the entire primary key (or the entire index) then the relationship + * between the tables will be one to one (1:1). + *
    + *

    + * Many-to-many associations are created if a join table is detected. To be + * identified as a many-to-many join table, the table must have have exactly two + * primary keys and have exactly two foreign-keys pointing to other entity + * tables and have no other columns. + * + * @author Alan Stewart + * @since 1.1 + */ +public class DbreMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String CREATED = "created"; + private static final String MAPPED_BY = "mappedBy"; + private static final String NAME = "name"; + private static final String PROVIDES_TYPE_STRING = DbreMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final String VALUE = "value"; + + private DbManagedAnnotationValues annotationValues; + private Database database; + private IdentifierHolder identifierHolder; + private Iterable managedEntities; + private ClassOrInterfaceTypeDetailsBuilder updatedGovernorBuilder; + private FieldMetadata versionField; + + // XXX DiSiD: Move var from method to class (store successive modifications) + // http://projects.disid.com/issues/7455 + private AnnotationMetadata toStringAnnotation = governorTypeDetails + .getAnnotation(ROO_TO_STRING); + + public DbreMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final DbManagedAnnotationValues annotationValues, + final IdentifierHolder identifierHolder, + final FieldMetadata versionField, + final Iterable managedEntities, + final Database database) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(identifierHolder, "Identifier holder required"); + Validate.notNull(managedEntities, "Managed entities required"); + Validate.notNull(database, "Database required"); + + this.annotationValues = annotationValues; + this.identifierHolder = identifierHolder; + this.versionField = versionField; + this.managedEntities = managedEntities; + this.database = database; + + final Table table = this.database.getTable( + DbreTypeUtils.getTableName(governorTypeDetails), + DbreTypeUtils.getSchemaName(governorTypeDetails)); + if (table == null) { + valid = false; + return; + } + + // Add fields for many-valued associations with many-to-many + // multiplicity + addManyToManyFields(table); + + // Add fields for single-valued associations to other entities that have + // one-to-one multiplicity + addOneToOneFields(table); + + // Add fields for many-valued associations with one-to-many multiplicity + addOneToManyFields(table); + + // Add fields for single-valued associations to other entities that have + // many-to-one multiplicity + addManyToOneFields(table); + + // Add remaining fields from columns + addOtherFields(table); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public ClassOrInterfaceTypeDetails getUpdatedGovernor() { + return updatedGovernorBuilder == null ? null : updatedGovernorBuilder + .build(); + } + + public boolean isAutomaticallyDelete() { + return annotationValues.isAutomaticallyDelete(); + } + + private void addCascadeType( + final AnnotationMetadataBuilder annotationBuilder, + final CascadeAction onUpdate, final CascadeAction onDelete) { + final String attributeName = "cascade"; + boolean hasCascadeType = true; + if (onUpdate == CascadeAction.CASCADE + && onDelete == CascadeAction.CASCADE) { + annotationBuilder.addEnumAttribute(attributeName, CASCADE_TYPE, + "ALL"); + } + else if (onUpdate == CascadeAction.CASCADE + && onDelete != CascadeAction.CASCADE) { + final List arrayValues = new ArrayList(); + arrayValues.add(new EnumAttributeValue(new JavaSymbolName( + attributeName), new EnumDetails(CASCADE_TYPE, + new JavaSymbolName("PERSIST")))); + arrayValues.add(new EnumAttributeValue(new JavaSymbolName( + attributeName), new EnumDetails(CASCADE_TYPE, + new JavaSymbolName("MERGE")))); + annotationBuilder + .addAttribute(new ArrayAttributeValue( + new JavaSymbolName(attributeName), arrayValues)); + } + else if (onUpdate != CascadeAction.CASCADE + && onDelete == CascadeAction.CASCADE) { + annotationBuilder.addEnumAttribute(attributeName, + CASCADE_TYPE.getSimpleTypeName(), "REMOVE"); + } + else { + hasCascadeType = false; + } + if (hasCascadeType) { + builder.getImportRegistrationResolver().addImport(CASCADE_TYPE); + } + } + + private void addManyToManyFields(final Table table) { + final Map owningSideTables = new LinkedHashMap(); + final Map uniqueOwningSideFields = new LinkedHashMap(); + final Map uniqueInverseSideFields = new LinkedHashMap(); + + for (final Table joinTable : database.getTables()) { + if (!joinTable.isJoinTable()) { + continue; + } + + final String errMsg = "table in join table '" + + joinTable.getName() + + "' for many-to-many relationship could not be found. Note that table names are case sensitive in some databases such as MySQL."; + final Iterator iter = joinTable.getImportedKeys() + .iterator(); + + // First foreign key in set + final ForeignKey foreignKey1 = iter.next(); + + // Second and last foreign key in set + final ForeignKey foreignKey2 = iter.next(); + + final Table owningSideTable = foreignKey1.getForeignTable(); + Validate.notNull(owningSideTable, "Owning-side %s", errMsg); + + final Table inverseSideTable = foreignKey2.getForeignTable(); + Validate.notNull(inverseSideTable, "Inverse-side %s", errMsg); + + final Integer tableCount = owningSideTables + .containsKey(owningSideTable) ? owningSideTables + .get(owningSideTable) + 1 : 0; + owningSideTables.put(owningSideTable, tableCount); + final String fieldSuffix = owningSideTables.get(owningSideTable) > 0 ? String + .valueOf(owningSideTables.get(owningSideTable)) : ""; + + final boolean sameTable = owningSideTable.equals(inverseSideTable); + + if (owningSideTable.equals(table)) { + final JavaSymbolName fieldName = new JavaSymbolName( + getInflectorPlural(DbreTypeUtils + .suggestFieldName(inverseSideTable)) + + (sameTable ? "1" : fieldSuffix)); + final FieldMetadataBuilder fieldBuilder = getManyToManyOwningSideField( + fieldName, joinTable, inverseSideTable, + foreignKey1.getOnUpdate(), foreignKey1.getOnDelete()); + uniqueOwningSideFields.put(fieldName, fieldBuilder); + } + + if (inverseSideTable.equals(table)) { + final JavaSymbolName fieldName = new JavaSymbolName( + getInflectorPlural(DbreTypeUtils + .suggestFieldName(owningSideTable)) + + (sameTable ? "2" : fieldSuffix)); + final JavaSymbolName mappedByFieldName = new JavaSymbolName( + getInflectorPlural(DbreTypeUtils + .suggestFieldName(inverseSideTable)) + + (sameTable ? "1" : fieldSuffix)); + final FieldMetadataBuilder fieldBuilder = getManyToManyInverseSideField( + fieldName, mappedByFieldName, owningSideTable, + foreignKey2.getOnUpdate(), foreignKey2.getOnDelete()); + uniqueInverseSideFields.put(fieldName, fieldBuilder); + } + } + + // Add unique owning-side many-to-one fields + for (final FieldMetadataBuilder fieldBuilder : uniqueOwningSideFields + .values()) { + addToBuilder(fieldBuilder); + + // Exclude these fields in @RooToString to avoid circular references + // - ROO-1399 + excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() + .getSymbolName()); + } + // Add unique inverse-side many-to-one fields + for (final FieldMetadataBuilder fieldBuilder : uniqueInverseSideFields + .values()) { + addToBuilder(fieldBuilder); + + // Exclude these fields in @RooToString to avoid circular references + // - ROO-1399 + excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() + .getSymbolName()); + } + } + + private void addManyToOneFields(final Table table) { + // Add unique many-to-one fields + final Map uniqueFields = new LinkedHashMap(); + + for (final ForeignKey foreignKey : table.getImportedKeys()) { + final Table foreignTable = foreignKey.getForeignTable(); + if (foreignTable == null || isOneToOne(table, foreignKey)) { + continue; + } + + // Assume many-to-one multiplicity + JavaSymbolName fieldName = null; + final String foreignTableName = foreignTable.getName(); + final String foreignSchemaName = foreignTable.getSchema().getName(); + if (foreignKey.getReferenceCount() == 1) { + final Reference reference = foreignKey.getReferences() + .iterator().next(); + fieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(reference + .getLocalColumnName())); + } + else { + final Short keySequence = foreignKey.getKeySequence(); + final String fieldSuffix = keySequence != null + && keySequence > 0 ? String.valueOf(keySequence) : ""; + fieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(foreignTableName) + + fieldSuffix); + } + final JavaType fieldType = DbreTypeUtils.findTypeForTableName( + managedEntities, foreignTableName, foreignSchemaName); + Validate.notNull( + fieldType, + "Attempted to create many-to-one field '%s' in '%s' %s", + fieldName, + destination.getFullyQualifiedTypeName(), + getErrorMsg(foreignTable.getFullyQualifiedTableName(), + table.getFullyQualifiedTableName())); + + // Fields are stored in a field-keyed map first before adding them + // to the builder. + // This ensures the fields from foreign keys with multiple columns + // will only get created once. + final FieldMetadataBuilder fieldBuilder = getOneToOneOrManyToOneField( + fieldName, fieldType, foreignKey, MANY_TO_ONE, true); + uniqueFields.put(fieldName, fieldBuilder); + } + + for (final FieldMetadataBuilder fieldBuilder : uniqueFields.values()) { + addToBuilder(fieldBuilder); + + // Exclude these fields in @RooToString to avoid circular references + // - ROO-1399 + excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() + .getSymbolName()); + } + } + + private void addOneToManyFields(final Table table) { + Validate.notNull(table, "Table required"); + if (table.isJoinTable()) { + return; + } + + for (final ForeignKey exportedKey : table.getExportedKeys()) { + final Table exportedKeyForeignTable = exportedKey.getForeignTable(); + Validate.notNull( + exportedKeyForeignTable, + "Foreign key table for foreign key '%s' in table '%s' does not exist. One-to-many relationship not created", + exportedKey.getName(), table.getFullyQualifiedTableName()); + if (exportedKeyForeignTable.isJoinTable()) { + continue; + } + + final String foreignTableName = exportedKeyForeignTable.getName(); + final String foreignSchemaName = exportedKeyForeignTable + .getSchema().getName(); + final Table foreignTable = database.getTable(foreignTableName, + foreignSchemaName); + Validate.notNull( + foreignTable, + "Related table '%s' could not be found but was referenced by table '%s'", + exportedKeyForeignTable.getFullyQualifiedTableName(), + table.getFullyQualifiedTableName()); + + if (isOneToOne(foreignTable, + foreignTable.getImportedKey(exportedKey.getName()))) { + continue; + } + + final Short keySequence = exportedKey.getKeySequence(); + final String fieldSuffix = keySequence != null && keySequence > 0 ? String + .valueOf(keySequence) : ""; + JavaSymbolName fieldName = new JavaSymbolName( + getInflectorPlural(DbreTypeUtils + .suggestFieldName(foreignTableName)) + fieldSuffix); + JavaSymbolName mappedByFieldName = null; + if (exportedKey.getReferenceCount() == 1) { + final Reference reference = exportedKey.getReferences() + .iterator().next(); + mappedByFieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(reference + .getForeignColumnName())); + } + else { + mappedByFieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(table) + fieldSuffix); + } + + // Check for existence of same field - ROO-1691 + while (true) { + if (!hasFieldInItd(fieldName)) { + break; + } + fieldName = new JavaSymbolName(fieldName.getSymbolName() + "_"); + } + + final FieldMetadataBuilder fieldBuilder = getOneToManyMappedByField( + fieldName, mappedByFieldName, foreignTableName, + foreignSchemaName, exportedKey.getOnUpdate(), + exportedKey.getOnDelete()); + addToBuilder(fieldBuilder); + + // Exclude these fields in @RooToString to avoid circular references + // - ROO-1399 + excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() + .getSymbolName()); + } + } + + private void addOneToOneFields(final Table table) { + // Add unique one-to-one fields + final Map uniqueFields = new LinkedHashMap(); + + for (final ForeignKey foreignKey : table.getImportedKeys()) { + if (!isOneToOne(table, foreignKey)) { + continue; + } + + final Table importedKeyForeignTable = foreignKey.getForeignTable(); + Validate.notNull( + importedKeyForeignTable, + "Foreign key table for foreign key '%s' in table '%s' does not exist. One-to-one relationship not created", + foreignKey.getName(), table.getFullyQualifiedTableName()); + + final String foreignTableName = importedKeyForeignTable.getName(); + final String foreignSchemaName = importedKeyForeignTable + .getSchema().getName(); + final Short keySequence = foreignKey.getKeySequence(); + final String fieldSuffix = keySequence != null && keySequence > 0 ? String + .valueOf(keySequence) : ""; + final JavaSymbolName fieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(foreignTableName) + + fieldSuffix); + final JavaType fieldType = DbreTypeUtils.findTypeForTableName( + managedEntities, foreignTableName, foreignSchemaName); + Validate.notNull( + fieldType, + "Attempted to create one-to-one field '%s' in '%s' %s", + fieldName, + destination.getFullyQualifiedTypeName(), + getErrorMsg(importedKeyForeignTable + .getFullyQualifiedTableName(), table + .getFullyQualifiedTableName())); + + // Fields are stored in a field-keyed map first before adding them + // to the builder. + // This ensures the fields from foreign keys with multiple columns + // will only get created once. + final FieldMetadataBuilder fieldBuilder = getOneToOneOrManyToOneField( + fieldName, fieldType, foreignKey, ONE_TO_ONE, false); + uniqueFields.put(fieldName, fieldBuilder); + } + + for (final FieldMetadataBuilder fieldBuilder : uniqueFields.values()) { + addToBuilder(fieldBuilder); + + // Exclude these fields in @RooToString to avoid circular references + // - ROO-1399 + excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() + .getSymbolName()); + } + + // Add one-to-one mapped-by fields + if (table.isJoinTable()) { + return; + } + + for (final ForeignKey exportedKey : table.getExportedKeys()) { + final Table exportedKeyForeignTable = exportedKey.getForeignTable(); + Validate.notNull( + exportedKeyForeignTable, + "Foreign key table for foreign key '%s' in table '%s' does not exist. One-to-one relationship not created", + exportedKey.getName(), table.getFullyQualifiedTableName()); + if (exportedKeyForeignTable.isJoinTable()) { + continue; + } + + final String foreignTableName = exportedKeyForeignTable.getName(); + final String foreignSchemaName = exportedKeyForeignTable + .getSchema().getName(); + final Table foreignTable = database.getTable(foreignTableName, + foreignSchemaName); + Validate.notNull( + foreignTable, + "Related table '%s' could not be found but has a foreign-key reference to table '%s'", + exportedKeyForeignTable.getFullyQualifiedTableName(), + table.getFullyQualifiedTableName()); + if (!isOneToOne(foreignTable, + foreignTable.getImportedKey(exportedKey.getName()))) { + continue; + } + final Short keySequence = exportedKey.getKeySequence(); + final String fieldSuffix = keySequence != null && keySequence > 0 ? String + .valueOf(keySequence) : ""; + JavaSymbolName fieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(foreignTableName) + + fieldSuffix); + + final JavaType fieldType = DbreTypeUtils.findTypeForTableName( + managedEntities, foreignTableName, foreignSchemaName); + Validate.notNull( + fieldType, + "Attempted to create one-to-one mapped-by field '%s' in '%s' %s", + fieldName, destination.getFullyQualifiedTypeName(), + getErrorMsg(foreignTable.getFullyQualifiedTableName())); + + // Check for existence of same field - ROO-1691 + while (true) { + if (!hasFieldInItd(fieldName)) { + break; + } + fieldName = new JavaSymbolName(fieldName.getSymbolName() + "_"); + } + + final JavaSymbolName mappedByFieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(table.getName()) + + fieldSuffix); + + final FieldMetadataBuilder fieldBuilder = getOneToOneMappedByField( + fieldName, fieldType, mappedByFieldName, + exportedKey.getOnUpdate(), exportedKey.getOnDelete()); + addToBuilder(fieldBuilder); + + // Exclude these fields in @RooToString to avoid circular references + // - ROO-1399 + excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() + .getSymbolName()); + } + } + + private void addOtherFields(final Table table) { + final Map uniqueFields = new LinkedHashMap(); + + for (final Column column : table.getColumns()) { + final String columnName = column.getName(); + JavaSymbolName fieldName = new JavaSymbolName( + DbreTypeUtils.suggestFieldName(columnName)); + + final boolean isIdField = isIdField(fieldName) + || column.isPrimaryKey(); + final boolean isVersionField = isVersionField(fieldName) + || (columnName.equals("version") && !database + .isDisableVersionFields()); + final boolean isCompositeKeyField = isCompositeKeyField(fieldName); + final boolean isForeignKey = table + .findImportedKeyByLocalColumnName(columnName) != null; + if (isIdField || isVersionField || isCompositeKeyField + || isForeignKey) { + continue; + } + + final boolean hasEmbeddedIdField = isEmbeddedIdField(fieldName) + && !isCompositeKeyField; + if (hasEmbeddedIdField) { + fieldName = governorTypeDetails.getUniqueFieldName(fieldName + .getSymbolName()); + } + final FieldMetadataBuilder fieldBuilder = getField(fieldName, + column, table.getName(), + table.isIncludeNonPortableAttributes()); + if (fieldBuilder.getFieldType().equals(DATE) + && fieldName.getSymbolName().equals(CREATED)) { + fieldBuilder.setFieldInitializer("new Date()"); + } + uniqueFields.put(fieldName, fieldBuilder); + } + + for (final FieldMetadataBuilder fieldBuilder : uniqueFields.values()) { + addToBuilder(fieldBuilder); + } + } + + private void addToBuilder(final FieldMetadataBuilder fieldBuilder) { + final JavaSymbolName fieldName = fieldBuilder.getFieldName(); + if (hasField(fieldName, fieldBuilder.buildAnnotations()) + || hasFieldInItd(fieldName)) { + return; + } + + builder.addField(fieldBuilder); + + // Check for an existing accessor in the governor + final JavaType fieldType = fieldBuilder.getFieldType(); + builder.addMethod(getAccessorMethod(fieldName, fieldType)); + + // Check for an existing mutator in the governor + builder.addMethod(getMutatorMethod(fieldName, fieldType)); + } + + // XXX DiSiD: Invoke this method when add non other fields to builder + // http://projects.disid.com/issues/7455 + private void excludeFieldsInToStringAnnotation(final String fieldName) { + if (toStringAnnotation == null) { + return; + } + + final List> attributes = new ArrayList>(); + final List ignoreFields = new ArrayList(); + + // Copy the existing attributes, excluding the "ignoreFields" attribute + boolean alreadyAdded = false; + AnnotationAttributeValue value = toStringAnnotation + .getAttribute(new JavaSymbolName("excludeFields")); + if (value == null) { + value = new ArrayAttributeValue( + new JavaSymbolName("excludeFields"), + new ArrayList()); + } + + // Ensure we have an array of strings + final String errMsg = "@RooToString attribute 'excludeFields' must be an array of strings"; + Validate.isInstanceOf(ArrayAttributeValue.class, value, errMsg); + final ArrayAttributeValue arrayVal = (ArrayAttributeValue) value; + for (final Object obj : arrayVal.getValue()) { + Validate.isInstanceOf(StringAttributeValue.class, obj, errMsg); + final StringAttributeValue sv = (StringAttributeValue) obj; + if (sv.getValue().equals(fieldName)) { + alreadyAdded = true; + } + ignoreFields.add(sv); + } + + // Add the desired field to ignore to the end + if (!alreadyAdded) { + ignoreFields.add(new StringAttributeValue(new JavaSymbolName( + "ignored"), fieldName)); + } + + attributes.add(new ArrayAttributeValue( + new JavaSymbolName("excludeFields"), ignoreFields)); + final AnnotationMetadataBuilder toStringAnnotationBuilder = new AnnotationMetadataBuilder( + ROO_TO_STRING, attributes); + updatedGovernorBuilder = new ClassOrInterfaceTypeDetailsBuilder( + governorTypeDetails); + toStringAnnotation = toStringAnnotationBuilder.build(); + updatedGovernorBuilder.updateTypeAnnotation(toStringAnnotation, + new HashSet()); + } + + private String getErrorMsg(final String tableName) { + return String + .format(" but type for table '%s' could not be found or is not database managed (not annotated with @RooDbManaged)", + tableName); + } + + private String getErrorMsg(final String foreignTableName, + final String tableName) { + return getErrorMsg(foreignTableName) + + String.format( + " and table '%s' has a foreign-key reference to table '%s'", + tableName, foreignTableName); + } + + private FieldMetadataBuilder getField(final JavaSymbolName fieldName, + final Column column, final String tableName, + final boolean includeNonPortable) { + JavaType fieldType = column.getJavaType(); + Validate.notNull(fieldType, + "Field type for column '%s' in table '%s' is null", + column.getName(), tableName); + + // Check if field is a Boolean object and is required, then change to + // boolean primitive + if (fieldType.equals(JavaType.BOOLEAN_OBJECT) && column.isRequired()) { + fieldType = JavaType.BOOLEAN_PRIMITIVE; + } + + // Add annotations to field + final List annotations = new ArrayList(); + + // Add @Column annotation + final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( + COLUMN); + columnBuilder.addStringAttribute(NAME, column.getEscapedName()); + if (includeNonPortable) { + columnBuilder.addStringAttribute("columnDefinition", + column.getTypeName()); + } + + // Add length attribute for Strings + int columnSize = column.getColumnSize(); + if (columnSize < 4000 && fieldType.equals(JavaType.STRING)) { + columnBuilder.addIntegerAttribute("length", columnSize); + } + + // Add precision and scale attributes for numeric fields + if (columnSize > 0 && JdkJavaType.isDecimalType(fieldType)) { + columnBuilder.addIntegerAttribute("precision", columnSize); + int scale = column.getScale(); + if (scale > 0) { + columnBuilder.addIntegerAttribute("scale", scale); + } + } + + // Add unique = true to @Column if applicable + if (column.isUnique()) { + columnBuilder.addBooleanAttribute("unique", true); + } + + annotations.add(columnBuilder); + + // Add @NotNull if applicable + if (column.isRequired()) { + annotations.add(new AnnotationMetadataBuilder(NOT_NULL)); + } + + // Add JSR 220 @Temporal annotation to date fields + if (fieldType.equals(DATE) || fieldType.equals(CALENDAR)) { + final AnnotationMetadataBuilder temporalBuilder = new AnnotationMetadataBuilder( + TEMPORAL); + temporalBuilder.addEnumAttribute(VALUE, new EnumDetails( + TEMPORAL_TYPE, new JavaSymbolName(column.getJdbcType()))); + annotations.add(temporalBuilder); + + final AnnotationMetadataBuilder dateTimeFormatBuilder = new AnnotationMetadataBuilder( + DATE_TIME_FORMAT); + if (fieldType.equals(DATE)) { + dateTimeFormatBuilder.addStringAttribute("style", "M-"); + } + else { + dateTimeFormatBuilder.addStringAttribute("style", "MM"); + } + + if (fieldName.getSymbolName().equals(CREATED)) { + columnBuilder.addBooleanAttribute("updatable", false); + } + annotations.add(dateTimeFormatBuilder); + } + + // Add @Lob for CLOB fields if applicable + if (column.getJdbcType().equals("CLOB")) { + annotations.add(new AnnotationMetadataBuilder(LOB)); + } + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); + if (fieldName.getSymbolName().equals(CREATED)) { + if (fieldType.equals(DATE)) { + fieldBuilder.setFieldInitializer("new java.util.Date()"); + } + else { + fieldBuilder + .setFieldInitializer("java.util.Calendar.getInstance()"); + } + } + return fieldBuilder; + } + + private String getInflectorPlural(final String term) { + try { + return Noun.pluralOf(term, Locale.ENGLISH); + } + catch (final RuntimeException e) { + // Inflector failed (see for example ROO-305), so don't pluralize it + return term; + } + } + + private AnnotationMetadataBuilder getJoinColumnAnnotation( + final Reference reference, final boolean referencedColumn) { + return getJoinColumnAnnotation(reference, referencedColumn, null); + } + + private AnnotationMetadataBuilder getJoinColumnAnnotation( + final Reference reference, final boolean referencedColumn, + final JavaType fieldType) { + return getJoinColumnAnnotation(reference, referencedColumn, fieldType, + null); + } + + private AnnotationMetadataBuilder getJoinColumnAnnotation( + final Reference reference, final boolean referencedColumn, + final JavaType fieldType, final Boolean nullable) { + final Column localColumn = reference.getLocalColumn(); + Validate.notNull(localColumn, "Foreign-key reference local column '" + + reference.getLocalColumnName() + "' must not be null"); + final AnnotationMetadataBuilder joinColumnBuilder = new AnnotationMetadataBuilder( + JOIN_COLUMN); + joinColumnBuilder + .addStringAttribute(NAME, localColumn.getEscapedName()); + + if (referencedColumn) { + final Column foreignColumn = reference.getForeignColumn(); + Validate.notNull( + foreignColumn, + "Foreign-key reference foreign column '%s' must not be null", + reference.getForeignColumnName()); + joinColumnBuilder.addStringAttribute("referencedColumnName", + foreignColumn.getEscapedName()); + } + + if (nullable == null) { + if (localColumn.isRequired()) { + joinColumnBuilder.addBooleanAttribute("nullable", false); + } + } + else { + joinColumnBuilder.addBooleanAttribute("nullable", nullable); + } + + if (fieldType != null) { + if (isCompositeKeyColumn(localColumn) || localColumn.isPrimaryKey() + || !reference.isInsertableOrUpdatable()) { + joinColumnBuilder.addBooleanAttribute("insertable", false); + joinColumnBuilder.addBooleanAttribute("updatable", false); + } + } + + return joinColumnBuilder; + } + + private AnnotationMetadataBuilder getJoinColumnsAnnotation( + final Set references, final JavaType fieldType) { + final List arrayValues = new ArrayList(); + + // Nullable attribute will have same value for each + // If some column not required, all JoinColumn will be nullable + boolean nullable = false; + for (final Reference reference : references) { + if (!reference.getLocalColumn().isRequired()) { + nullable = true; + } + } + + for (final Reference reference : references) { + final AnnotationMetadataBuilder joinColumnAnnotation = getJoinColumnAnnotation( + reference, true, fieldType, nullable); + arrayValues.add(new NestedAnnotationAttributeValue( + new JavaSymbolName(VALUE), joinColumnAnnotation.build())); + } + final List> attributes = new ArrayList>(); + attributes.add(new ArrayAttributeValue( + new JavaSymbolName(VALUE), arrayValues)); + return new AnnotationMetadataBuilder(JOIN_COLUMNS, attributes); + } + + private FieldMetadataBuilder getManyToManyInverseSideField( + final JavaSymbolName fieldName, + final JavaSymbolName mappedByFieldName, + final Table owningSideTable, final CascadeAction onUpdate, + final CascadeAction onDelete) { + final JavaType element = DbreTypeUtils.findTypeForTable( + managedEntities, owningSideTable); + Validate.notNull( + element, + "Attempted to create many-to-many inverse-side field '%s' in '%s' %s", + fieldName, destination.getFullyQualifiedTypeName(), + getErrorMsg(owningSideTable.getFullyQualifiedTableName())); + + final List params = Arrays.asList(element); + final JavaType fieldType = new JavaType( + SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, params); + + // Add annotations to field + final List annotations = new ArrayList(); + final AnnotationMetadataBuilder manyToManyBuilder = new AnnotationMetadataBuilder( + MANY_TO_MANY); + manyToManyBuilder.addStringAttribute(MAPPED_BY, + mappedByFieldName.getSymbolName()); + addCascadeType(manyToManyBuilder, onUpdate, onDelete); + annotations.add(manyToManyBuilder); + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + fieldName, fieldType); + } + + private FieldMetadataBuilder getManyToManyOwningSideField( + final JavaSymbolName fieldName, final Table joinTable, + final Table inverseSideTable, final CascadeAction onUpdate, + final CascadeAction onDelete) { + final JavaType element = DbreTypeUtils.findTypeForTable( + managedEntities, inverseSideTable); + Validate.notNull( + element, + "Attempted to create many-to-many owning-side field '%s' in '%s' %s", + fieldName, destination.getFullyQualifiedTypeName(), + getErrorMsg(inverseSideTable.getFullyQualifiedTableName())); + + final List params = Arrays.asList(element); + final JavaType fieldType = new JavaType( + SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, params); + + // Add annotations to field + final List annotations = new ArrayList(); + + // Add @ManyToMany annotation + final AnnotationMetadataBuilder manyToManyBuilder = new AnnotationMetadataBuilder( + MANY_TO_MANY); + annotations.add(manyToManyBuilder); + + // Add @JoinTable annotation + final AnnotationMetadataBuilder joinTableBuilder = new AnnotationMetadataBuilder( + JOIN_TABLE); + final List> joinTableAnnotationAttributes = new ArrayList>(); + joinTableAnnotationAttributes.add(new StringAttributeValue( + new JavaSymbolName(NAME), joinTable.getName())); + + final Iterator iter = joinTable.getImportedKeys() + .iterator(); + + // Add "joinColumns" attribute containing nested @JoinColumn annotations + final List joinColumnArrayValues = new ArrayList(); + final Set firstKeyReferences = iter.next().getReferences(); + for (final Reference reference : firstKeyReferences) { + final AnnotationMetadataBuilder joinColumnBuilder = getJoinColumnAnnotation( + reference, firstKeyReferences.size() > 1); + joinColumnArrayValues.add(new NestedAnnotationAttributeValue( + new JavaSymbolName(VALUE), joinColumnBuilder.build())); + } + joinTableAnnotationAttributes + .add(new ArrayAttributeValue( + new JavaSymbolName("joinColumns"), + joinColumnArrayValues)); + + // Add "inverseJoinColumns" attribute containing nested @JoinColumn + // annotations + final List inverseJoinColumnArrayValues = new ArrayList(); + final Set lastKeyReferences = iter.next().getReferences(); + for (final Reference reference : lastKeyReferences) { + final AnnotationMetadataBuilder joinColumnBuilder = getJoinColumnAnnotation( + reference, lastKeyReferences.size() > 1); + inverseJoinColumnArrayValues + .add(new NestedAnnotationAttributeValue(new JavaSymbolName( + VALUE), joinColumnBuilder.build())); + } + joinTableAnnotationAttributes + .add(new ArrayAttributeValue( + new JavaSymbolName("inverseJoinColumns"), + inverseJoinColumnArrayValues)); + + // Add attributes to a @JoinTable annotation builder + joinTableBuilder.setAttributes(joinTableAnnotationAttributes); + annotations.add(joinTableBuilder); + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + fieldName, fieldType); + } + + private FieldMetadataBuilder getOneToManyMappedByField( + final JavaSymbolName fieldName, + final JavaSymbolName mappedByFieldName, + final String foreignTableName, final String foreignSchemaName, + final CascadeAction onUpdate, final CascadeAction onDelete) { + final JavaType element = DbreTypeUtils.findTypeForTableName( + managedEntities, foreignTableName, foreignSchemaName); + Validate.notNull( + element, + "Attempted to create one-to-many mapped-by field '%s' in '%s' %s", + fieldName, destination.getFullyQualifiedTypeName(), + getErrorMsg(foreignTableName + "." + foreignSchemaName)); + + final JavaType fieldType = new JavaType( + SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(element)); + // Add @OneToMany annotation + final List annotations = new ArrayList(); + final AnnotationMetadataBuilder oneToManyBuilder = new AnnotationMetadataBuilder( + ONE_TO_MANY); + oneToManyBuilder.addStringAttribute(MAPPED_BY, + mappedByFieldName.getSymbolName()); + addCascadeType(oneToManyBuilder, onUpdate, onDelete); + annotations.add(oneToManyBuilder); + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + fieldName, fieldType); + } + + private FieldMetadataBuilder getOneToOneMappedByField( + final JavaSymbolName fieldName, final JavaType fieldType, + final JavaSymbolName mappedByFieldName, + final CascadeAction onUpdate, final CascadeAction onDelete) { + final List annotations = new ArrayList(); + final AnnotationMetadataBuilder oneToOneBuilder = new AnnotationMetadataBuilder( + ONE_TO_ONE); + oneToOneBuilder.addStringAttribute(MAPPED_BY, + mappedByFieldName.getSymbolName()); + addCascadeType(oneToOneBuilder, onUpdate, onDelete); + annotations.add(oneToOneBuilder); + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + fieldName, fieldType); + } + + private FieldMetadataBuilder getOneToOneOrManyToOneField( + final JavaSymbolName fieldName, final JavaType fieldType, + final ForeignKey foreignKey, final JavaType annotationType, + final boolean referencedColumn) { + // Add annotations to field + final List annotations = new ArrayList(); + + // Add annotation + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + annotationType); + if (foreignKey.isExported()) { + addCascadeType(annotationBuilder, foreignKey.getOnUpdate(), + foreignKey.getOnDelete()); + } + annotations.add(annotationBuilder); + + final Set references = foreignKey.getReferences(); + if (references.size() == 1) { + // Add @JoinColumn annotation + annotations.add(getJoinColumnAnnotation(references.iterator() + .next(), referencedColumn, fieldType)); + } + else { + // Add @JoinColumns annotation + annotations.add(getJoinColumnsAnnotation(references, fieldType)); + } + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + fieldName, fieldType); + } + + private boolean hasField(final JavaSymbolName fieldName, + final List annotations) { + // Check governor for field + if (governorTypeDetails.getField(fieldName) != null) { + return true; + } + + // Check @Column and @JoinColumn annotations on fields in governor with + // the same 'name' as the generated field + final List governorFields = governorTypeDetails + .getFieldsWithAnnotation(COLUMN); + governorFields.addAll(governorTypeDetails + .getFieldsWithAnnotation(JOIN_COLUMN)); + for (final FieldMetadata governorField : governorFields) { + governorFieldAnnotations: for (final AnnotationMetadata governorFieldAnnotation : governorField + .getAnnotations()) { + if (governorFieldAnnotation.getAnnotationType().equals(COLUMN) + || governorFieldAnnotation.getAnnotationType().equals( + JOIN_COLUMN)) { + final AnnotationAttributeValue name = governorFieldAnnotation + .getAttribute(new JavaSymbolName(NAME)); + if (name == null) { + continue governorFieldAnnotations; + } + for (final AnnotationMetadata annotationMetadata : annotations) { + final AnnotationAttributeValue columnName = annotationMetadata + .getAttribute(new JavaSymbolName(NAME)); + if (columnName != null && columnName.equals(name)) { + return true; + } + } + } + } + } + + return false; + } + + /** + * Indicates whether the ITD being built has a field of the given name + * + * @param fieldName + * @return true if the field exists in the builder, otherwise false + */ + private boolean hasFieldInItd(final JavaSymbolName fieldName) { + for (final FieldMetadataBuilder declaredField : builder + .getDeclaredFields()) { + if (declaredField.getFieldName().equals(fieldName)) { + return true; + } + } + return false; + } + + private boolean isCompositeKeyColumn(final Column column) { + if (!identifierHolder.isEmbeddedIdField()) { + return false; + } + + for (final FieldMetadata field : identifierHolder + .getEmbeddedIdentifierFields()) { + for (final AnnotationMetadata annotation : field.getAnnotations()) { + if (!annotation.getAnnotationType().equals(COLUMN)) { + continue; + } + final AnnotationAttributeValue nameAttribute = annotation + .getAttribute(new JavaSymbolName(NAME)); + if (nameAttribute != null) { + final String name = (String) nameAttribute.getValue(); + if (column.getName().equals(name)) { + return true; + } + } + } + } + return false; + } + + private boolean isCompositeKeyField(final JavaSymbolName fieldName) { + if (!identifierHolder.isEmbeddedIdField()) { + return false; + } + + for (final FieldMetadata field : identifierHolder + .getEmbeddedIdentifierFields()) { + if (field.getFieldName().equals(fieldName)) { + return true; + } + } + return false; + } + + private boolean isEmbeddedIdField(final JavaSymbolName fieldName) { + return identifierHolder.isEmbeddedIdField() + && identifierHolder.getIdentifierField().getFieldName() + .equals(fieldName); + } + + private boolean isIdField(final JavaSymbolName fieldName) { + return !identifierHolder.isEmbeddedIdField() + && identifierHolder.getIdentifierField().getFieldName() + .equals(fieldName); + } + + private boolean isOneToOne(final Table table, final ForeignKey foreignKey) { + Validate.notNull(table, + "Table must not be null in determining a one-to-one relationship"); + Validate.notNull(foreignKey, + "Foreign key must not be null in determining a one-to-one relationship"); + boolean equals = table.getPrimaryKeyCount() == foreignKey + .getReferenceCount(); + final Iterator primaryKeyIterator = table.getPrimaryKeys() + .iterator(); + while (equals && primaryKeyIterator.hasNext()) { + equals &= foreignKey.hasLocalColumn(primaryKeyIterator.next()); + } + return equals; + } + + private boolean isVersionField(final JavaSymbolName fieldName) { + return versionField != null + && versionField.getFieldName().equals(fieldName); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadataProvider.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadataProvider.java new file mode 100644 index 000000000..db7c44ad9 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadataProvider.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.dbre; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link DbreMetadata}. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface DbreMetadataProvider extends ItdTriggerBasedMetadataProvider { +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadataProviderImpl.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadataProviderImpl.java new file mode 100644 index 000000000..0d98c6ecc --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreMetadataProviderImpl.java @@ -0,0 +1,209 @@ +package org.springframework.roo.addon.dbre; + +import static org.springframework.roo.model.RooJavaType.ROO_DB_MANAGED; + +import java.util.List; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.dbre.model.Database; +import org.springframework.roo.addon.dbre.model.DbreModelService; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link DbreMetadataProvider}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class DbreMetadataProviderImpl extends AbstractItdMetadataProvider + implements DbreMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(DbreMetadataProviderImpl.class); + + private DbreModelService dbreModelService; + private TypeManagementService typeManagementService; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_DB_MANAGED); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return DbreMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_DB_MANAGED); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = DbreMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = DbreMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + private IdentifierHolder getIdentifierHolder(final JavaType javaType) { + final List identifierFields = getPersistenceMemberLocator() + .getIdentifierFields(javaType); + if (identifierFields.isEmpty()) { + return null; + } + + final FieldMetadata identifierField = identifierFields.get(0); + final boolean embeddedIdField = identifierField.getCustomData().get( + CustomDataKeys.EMBEDDED_ID_FIELD) != null; + final List embeddedIdFields = getPersistenceMemberLocator() + .getEmbeddedIdentifierFields(javaType); + return new IdentifierHolder(identifierField, embeddedIdField, + embeddedIdFields); + } + + public String getItdUniquenessFilenameSuffix() { + return "DbManaged"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // We need to parse the annotation, which we expect to be present + final DbManagedAnnotationValues annotationValues = new DbManagedAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound()) { + return null; + } + + // Abort if the database couldn't be deserialized. This can occur if the + // DBRE XML file has been deleted or is empty. + final Database database = getDbreModelService().getDatabase(false); + if (database == null) { + return null; + } + + // We know governor type details are non-null and can be safely cast + final JavaType javaType = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails().getName(); + final IdentifierHolder identifierHolder = getIdentifierHolder(javaType); + if (identifierHolder == null) { + return null; + } + + final FieldMetadata versionField = getVersionField(javaType, + metadataIdentificationString); + + // Search for database-managed entities + final Iterable managedEntities = getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(ROO_DB_MANAGED); + + boolean found = false; + for (final ClassOrInterfaceTypeDetails managedEntity : managedEntities) { + if (managedEntity.getName().equals(javaType)) { + found = true; + break; + } + } + if (!found) { + final String mid = getTypeLocationService() + .getPhysicalTypeIdentifier(javaType); + getMetadataDependencyRegistry().registerDependency(mid, + metadataIdentificationString); + return null; + } + + final DbreMetadata dbreMetadata = new DbreMetadata( + metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues, + identifierHolder, versionField, managedEntities, database); + final ClassOrInterfaceTypeDetails updatedGovernor = dbreMetadata + .getUpdatedGovernor(); + if (updatedGovernor != null) { + getTypeManagementService().createOrUpdateTypeOnDisk(updatedGovernor); + } + return dbreMetadata; + } + + public String getProvidesType() { + return DbreMetadata.getMetadataIdentiferType(); + } + + private FieldMetadata getVersionField(final JavaType domainType, + final String metadataIdentificationString) { + return getPersistenceMemberLocator().getVersionField(domainType); + } + + public DbreModelService getDbreModelService(){ + if(dbreModelService == null){ + // Get all Services implement DbreModelService interface + try { + ServiceReference[] references = context.getAllServiceReferences(DbreModelService.class.getName(), null); + + for(ServiceReference ref : references){ + return (DbreModelService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load DbreModelService on DbreMetadataProviderImpl."); + return null; + } + }else{ + return dbreModelService; + } + } + + public TypeManagementService getTypeManagementService(){ + if(typeManagementService == null){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on DbreMetadataProviderImpl."); + return null; + } + }else{ + return typeManagementService; + } + } +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreOperations.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreOperations.java new file mode 100644 index 000000000..8e3f9597d --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreOperations.java @@ -0,0 +1,65 @@ +package org.springframework.roo.addon.dbre; + +import java.io.File; +import java.util.Set; + +import org.springframework.roo.addon.dbre.model.Schema; +import org.springframework.roo.model.JavaPackage; + +/** + * Provides database reverse engineering operations. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface DbreOperations { + + /** + * Displays the metadata for the indicated schema on the screen, or writes + * it to the given file if a filename is specified. + * + * @param schemas the schema(s) to introspect (required) + * @param file to write to (can be null, in which case the output will + * appear on-screen) + * @param view true if database views are displayed, otherwise false + */ + void displayDatabaseMetadata(Set schemas, File file, boolean view); + + /** + * Returns whether or not the DBRE commands can be executed. + * + * @return true if the DBRE commands are available to use, otherwise false + */ + boolean isDbreInstallationPossible(); + + /** + * Introspects the database schema and causes the related entities on disk + * to be created, updated and deleted. + * + * @param schemas the schema(s) to reverse engineer (required) + * @param destinationPackage the package in which all entities will be + * stored (if not given, the project's top level package) + * @param testAutomatically whether to create automatic integration tests + * for generated entities + * @param view true if database views are displayed, otherwise false + * @param includeTables the set of tables to include in reverse engineering. + * @param excludeTables the set of tables to exclude from reverse + * engineering + * @param includeNonPortableAttributes whether or not to include + * non-portable JPA @Column attributes such as 'columnDefinition' + * @param disableVersionFields whether or not to disable a table's version + * column + * @param disableGeneratedIdentifiers whether or not to disable the + * identifier auto generation value + * @param activeRecord whether to generate CRUD active record methods for + * each entity + * @param repository whether to generate a service layer for each entity + * @param service whether to generate a repository layer for each entity + */ + void reverseEngineerDatabase(Set schemas, + JavaPackage destinationPackage, boolean testAutomatically, + boolean view, Set includeTables, Set excludeTables, + boolean includeNonPortableAttributes, boolean disableVersionFields, + boolean disableGeneratedIdentifiers, boolean activeRecord, + boolean repository, boolean service); +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreOperationsImpl.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreOperationsImpl.java new file mode 100644 index 000000000..1501073da --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreOperationsImpl.java @@ -0,0 +1,264 @@ +package org.springframework.roo.addon.dbre; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dbre.model.Database; +import org.springframework.roo.addon.dbre.model.DatabaseXmlUtils; +import org.springframework.roo.addon.dbre.model.DbreModelService; +import org.springframework.roo.addon.dbre.model.Schema; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of {@link DbreOperations}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class DbreOperationsImpl implements DbreOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(DbreOperationsImpl.class); + + @Reference private DbreModelService dbreModelService; + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + + public void displayDatabaseMetadata(final Set schemas, + final File file, final boolean view) { + Validate.notNull(schemas, "Schemas required"); + + // Force it to refresh the database from the actual JDBC connection + final Database database = dbreModelService.refreshDatabase(schemas, + view, Collections. emptySet(), + Collections. emptySet()); + database.setIncludeNonPortableAttributes(true); + database.setDisableVersionFields(true); + database.setDisableGeneratedIdentifiers(true); + outputSchemaXml(database, schemas, file, true); + } + + public boolean isDbreInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JPA); + } + + private void outputSchemaXml(final Database database, + final Set schemas, final File file, + final boolean displayOnly) { + if (database == null) { + LOGGER.warning("Cannot obtain database information for schema(s) '" + + StringUtils.join(schemas, ",") + "'"); + } + else if (!database.hasTables()) { + LOGGER.warning("Schema(s) '" + + StringUtils.join(schemas, ",") + + "' do not exist or does not have any tables. Note that the schema names of some databases are case-sensitive"); + } + else { + try { + if (displayOnly) { + final Document document = DatabaseXmlUtils + .getDatabaseDocument(database); + final OutputStream outputStream = file != null ? new FileOutputStream( + file) : new ByteArrayOutputStream(); + XmlUtils.writeXml(outputStream, document); + LOGGER.info(file != null ? "Database metadata written to file " + + file.getAbsolutePath() + : outputStream.toString()); + } + else { + dbreModelService.writeDatabase(database); + } + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + } + + public void reverseEngineerDatabase(final Set schemas, + final JavaPackage destinationPackage, + final boolean testAutomatically, final boolean view, + final Set includeTables, final Set excludeTables, + final boolean includeNonPortableAttributes, + final boolean disableVersionFields, + final boolean disableGeneratedIdentifiers, + final boolean activeRecord, final boolean repository, + final boolean service) { + // Force it to refresh the database from the actual JDBC connection + final Database database = dbreModelService.refreshDatabase(schemas, + view, includeTables, excludeTables); + database.setModuleName(projectOperations.getFocusedModuleName()); + database.setActiveRecord(activeRecord); + database.setRepository(repository); + database.setService(service); + database.setDestinationPackage(destinationPackage); + database.setIncludeNonPortableAttributes(includeNonPortableAttributes); + database.setDisableVersionFields(disableVersionFields); + database.setDisableGeneratedIdentifiers(disableGeneratedIdentifiers); + database.setTestAutomatically(testAutomatically); + outputSchemaXml(database, schemas, null, false); + + // Update the pom.xml to add an exclusion for the DBRE XML file in the + // maven-war-plugin + updatePom(); + + // Change the persistence.xml file to prevent tables being created and + // dropped. + updatePersistenceXml(includeNonPortableAttributes); + } + + private boolean setPropertyValue(final Element root, + Element propertyElement, final String name, final String value) { + boolean changed = false; + propertyElement = XmlUtils.findFirstElement( + "/persistence/persistence-unit/properties/property[@name = '" + + name + "']", root); + if (propertyElement != null + && !propertyElement.getAttribute("value").equals(value)) { + propertyElement.setAttribute("value", value); + changed = true; + } + return changed; + } + + // XXX DiSiD: Added includeNonPortableAttributes. + // If false then no validation + // http://projects.disid.com/issues/7456 + private void updatePersistenceXml(final boolean includeNonPortableAttributes) { + final String persistencePath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, "META-INF/persistence.xml"); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(persistencePath)); + final Element root = document.getDocumentElement(); + + final Element providerElement = XmlUtils + .findFirstElement( + "/persistence/persistence-unit[@transaction-type = 'RESOURCE_LOCAL']/provider", + root); + Validate.notNull(providerElement, + "/persistence/persistence-unit/provider is null"); + final String provider = providerElement.getTextContent(); + final Element propertyElement = null; + boolean changed = false; + if (provider.contains("hibernate")) { + if (includeNonPortableAttributes) { + changed = setPropertyValue(root, propertyElement, + "hibernate.hbm2ddl.auto", "validate"); + } + else { + changed = setPropertyValue(root, propertyElement, + "hibernate.hbm2ddl.auto", "none"); + } + changed |= setPropertyValue(root, propertyElement, + "hibernate.ejb.naming_strategy", + "org.hibernate.cfg.DefaultNamingStrategy"); + } + else if (provider.contains("openjpa")) { + changed = setPropertyValue(root, propertyElement, + "openjpa.jdbc.SynchronizeMappings", "validate"); + } + else if (provider.contains("eclipse")) { + changed = setPropertyValue(root, propertyElement, + "eclipselink.ddl-generation", "none"); + } + else if (provider.contains("datanucleus")) { + changed = setPropertyValue(root, propertyElement, + "datanucleus.autoCreateSchema", "false"); + changed |= setPropertyValue(root, propertyElement, + "datanucleus.autoCreateTables", "false"); + changed |= setPropertyValue(root, propertyElement, + "datanucleus.autoCreateColumns", "false"); + changed |= setPropertyValue(root, propertyElement, + "datanucleus.autoCreateConstraints", "false"); + changed |= setPropertyValue(root, propertyElement, + "datanucleus.validateTables", "false"); + changed |= setPropertyValue(root, propertyElement, + "datanucleus.validateConstraints", "false"); + } + else { + throw new IllegalStateException("Persistence provider " + provider + + " is not supported"); + } + + if (changed) { + fileManager.createOrUpdateTextFileIfRequired(persistencePath, + XmlUtils.nodeToString(document), false); + } + } + + private void updatePom() { + final String pom = pathResolver.getFocusedIdentifier(Path.ROOT, + "pom.xml"); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom)); + final Element root = document.getDocumentElement(); + + final String warPluginXPath = "/project/build/plugins/plugin[artifactId = 'maven-war-plugin']"; + final Element warPluginElement = XmlUtils.findFirstElement( + warPluginXPath, root); + if (warPluginElement == null) { + // Project may not be a web project, so just exit + return; + } + Element excludeElement = XmlUtils + .findFirstElement( + warPluginXPath + + "/configuration/webResources/resource/excludes/exclude[text() = '" + + DbreModelService.DBRE_XML + "']", root); + if (excludeElement != null) { + // element is already there, so just exit + return; + } + + // Create the required elements + final Element configurationElement = DomUtils.createChildIfNotExists( + "configuration", warPluginElement, document); + final Element webResourcesElement = DomUtils.createChildIfNotExists( + "webResources", configurationElement, document); + final Element resourceElement = DomUtils.createChildIfNotExists( + "resource", webResourcesElement, document); + final Element excludesElement = DomUtils.createChildIfNotExists( + "excludes", resourceElement, document); + excludeElement = DomUtils.createChildIfNotExists("exclude", + excludesElement, document); + final Element directoryElement = DomUtils.createChildIfNotExists( + "directory", resourceElement, document); + + // Populate them with the required text + excludeElement.setTextContent(DbreModelService.DBRE_XML); + directoryElement.setTextContent("src/main/resources"); + + // Clean up the XML + DomUtils.removeTextNodes(warPluginElement); + + // Write out the updated POM + fileManager.createOrUpdateTextFileIfRequired(pom, + XmlUtils.nodeToString(document), false); + } +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreTypeUtils.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreTypeUtils.java new file mode 100644 index 000000000..946ab10eb --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/DbreTypeUtils.java @@ -0,0 +1,294 @@ +package org.springframework.roo.addon.dbre; + +import static org.springframework.roo.model.JpaJavaType.TABLE; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.dbre.model.DbreModelService; +import org.springframework.roo.addon.dbre.model.Table; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.ReservedWords; + +/** + * Provides methods to find types based on table names and to suggest type and + * field names from table and column names respectively. + * + * @author Alan Stewart + * @since 1.1 + */ +public abstract class DbreTypeUtils { + + private static final JavaSymbolName NAME_ATTRIBUTE = new JavaSymbolName( + "name"); + private static final JavaSymbolName SCHEMA_ATTRIBUTE = new JavaSymbolName( + "schema"); + // The annotation attributes from which to read the db schema name + // Linked to preserve the iteration order below + private static final Map SCHEMA_ATTRIBUTES = new LinkedHashMap(); + + private static final JavaSymbolName TABLE_ATTRIBUTE = new JavaSymbolName( + "table"); + // The annotation attributes from which to read the db table name + // Linked to preserve the iteration order below + private static final Map TABLE_ATTRIBUTES = new LinkedHashMap(); + + static { + TABLE_ATTRIBUTES.put(TABLE, NAME_ATTRIBUTE); + TABLE_ATTRIBUTES.put(ROO_JPA_ENTITY, TABLE_ATTRIBUTE); + TABLE_ATTRIBUTES.put(ROO_JPA_ACTIVE_RECORD, TABLE_ATTRIBUTE); + } + static { + SCHEMA_ATTRIBUTES.put(TABLE, SCHEMA_ATTRIBUTE); + SCHEMA_ATTRIBUTES.put(ROO_JPA_ENTITY, SCHEMA_ATTRIBUTE); + SCHEMA_ATTRIBUTES.put(ROO_JPA_ACTIVE_RECORD, SCHEMA_ATTRIBUTE); + } + + /** + * Locates the type associated with the presented table. + * + * @param managedEntities a set of database-managed entities to search + * (required) + * @param table the table to locate (required) + * @return the type (if known) or null (if not found) + */ + public static JavaType findTypeForTable( + final Iterable managedEntities, + final Table table) { + Validate.notNull(managedEntities, "Set of managed entities required"); + Validate.notNull(table, "Table required"); + return findTypeForTableName(managedEntities, table.getName(), table + .getSchema().getName()); + } + + /** + * Locates the type associated with the presented table name. + * + * @param managedEntities a set of database-managed entities to search + * (required) + * @param tableName the table to locate (required) + * @param schemaName the table's schema name + * @return the type (if known) or null (if not found) + */ + public static JavaType findTypeForTableName( + final Iterable managedEntities, + final String tableName, final String schemaName) { + Validate.notNull(managedEntities, "Set of managed entities required"); + Validate.notBlank(tableName, "Table name required"); + + for (final ClassOrInterfaceTypeDetails managedEntity : managedEntities) { + final String managedSchemaName = getSchemaName(managedEntity); + if (tableName.equals(getTableName(managedEntity)) + && (!DbreModelService.NO_SCHEMA_REQUIRED + .equals(managedSchemaName) || schemaName + .equals(managedSchemaName))) { + return managedEntity.getName(); + } + } + + return null; + } + + /** + * Returns the value of the given attribute of the given annotation on the + * given type + * + * @param the expected annotation value type + * @param type the type whose annotations to read (required) + * @param annotationType the annotation to read (required) + * @param attributeName the annotation attribute to read (required) + * @return + */ + @SuppressWarnings("unchecked") + private static T getAnnotationAttribute( + final MemberHoldingTypeDetails type, final JavaType annotationType, + final JavaSymbolName attributeName) { + final AnnotationMetadata typeAnnotation = type + .getTypeAnnotation(annotationType); + if (typeAnnotation == null) { + return null; + } + final AnnotationAttributeValue attributeValue = typeAnnotation + .getAttribute(attributeName); + if (attributeValue == null) { + return null; + } + return (T) attributeValue.getValue(); + } + + /** + * Reads the given attributes of the given annotations on the given type, + * returning the first non-blank one found. + * + * @param annotatedType the type for which to read the annotations + * (required) + * @param annotationAttributes the annotation/attribute pairs to read for + * that type + * @return null if none of those annotations provide a + * non-blank schema name + */ + private static String getFirstNonBlankAttributeValue( + final MemberHoldingTypeDetails annotatedType, + final Map annotationAttributes) { + for (final Entry entry : annotationAttributes + .entrySet()) { + final String attributeValue = getAnnotationAttribute(annotatedType, + entry.getKey(), entry.getValue()); + if (StringUtils.isNotBlank(attributeValue)) { + return attributeValue; + } + } + return null; + } + + private static String getName(final String str, final boolean isField) { + final StringBuilder result = new StringBuilder(); + boolean isDelimChar = false; + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + if (i == 0) { + if (c == '0' || c == '1' || c == '2' || c == '3' || c == '4' + || c == '5' || c == '6' || c == '7' || c == '8' + || c == '9') { + result.append(isField ? "f" : "T"); + result.append(c); + } + else { + result.append(isField ? Character.toLowerCase(c) + : Character.toUpperCase(c)); + } + continue; + } + else if (i > 0 && (c == '_' || c == '-' || c == '\\' || c == '/') + || c == '.' || c == ' ') { + isDelimChar = true; + continue; + } + + if (isDelimChar) { + result.append(Character.toUpperCase(c)); + isDelimChar = false; + } + else { + if (i > 1 && Character.isLowerCase(str.charAt(i - 1)) + && Character.isUpperCase(c)) { + result.append(c); + } + else { + result.append(Character.toLowerCase(c)); + } + } + } + if (ReservedWords.RESERVED_JAVA_KEYWORDS.contains(result.toString())) { + result.append("1"); + } + return result.toString(); + } + + /** + * Returns the database schema for the given entity. + * + * @param entityDetails the type to search (required) + * @return the schema name (if known) or null (if not found) + */ + public static String getSchemaName( + final MemberHoldingTypeDetails entityDetails) { + Validate.notNull(entityDetails, + "MemberHoldingTypeDetails type required"); + return getFirstNonBlankAttributeValue(entityDetails, SCHEMA_ATTRIBUTES); + } + + /** + * Returns the database table for the given entity. + * + * @param entityDetails the type to search (required) + * @return the table (if known) or null (if not found) + */ + public static String getTableName( + final MemberHoldingTypeDetails entityDetails) { + Validate.notNull(entityDetails, + "MemberHoldingTypeDetails type required"); + return getFirstNonBlankAttributeValue(entityDetails, TABLE_ATTRIBUTES); + } + + /** + * Returns a field name for a given database table or column name; + * + * @param name the name of the table or column (required) + * @return a String representing the table or column + */ + public static String suggestFieldName(final String name) { + Validate.notBlank(name, "Table or column name required"); + return getName(name, true); + } + + /** + * Returns a field name for a given database table; + * + * @param table the the table (required) + * @return a String representing the table or column. + */ + public static String suggestFieldName(final Table table) { + Validate.notNull(table, "Table required"); + return getName(table.getName(), true); + } + + public static String suggestPackageName(final String str) { + final StringBuilder result = new StringBuilder(); + final char[] value = str.toCharArray(); + for (int i = 0; i < value.length; i++) { + final char c = value[i]; + if (i == 0 + && ('1' == c || '2' == c || '3' == c || '4' == c + || '5' == c || '6' == c || '7' == c || '8' == c + || '9' == c || '0' == c)) { + result.append("p"); + result.append(c); + } + else if ('.' == c || '/' == c || ' ' == c || '*' == c || '>' == c + || '<' == c || '!' == c || '@' == c || '%' == c || '^' == c + || '?' == c || '(' == c || ')' == c || '~' == c || '`' == c + || '{' == c || '}' == c || '[' == c || ']' == c || '|' == c + || '\\' == c || '\'' == c || '+' == c || '-' == c) { + result.append(""); + } + else { + result.append(Character.toLowerCase(c)); + } + } + return result.toString(); + } + + /** + * Returns a JavaType given a table identity. + * + * @param tableName the table name to convert (required) + * @param javaPackage the Java package to use for the type + * @return a new JavaType + */ + public static JavaType suggestTypeNameForNewTable(final String tableName, + final JavaPackage javaPackage) { + Validate.notBlank(tableName, "Table name required"); + + final StringBuilder result = new StringBuilder(); + if (javaPackage != null + && StringUtils.isNotBlank(javaPackage + .getFullyQualifiedPackageName())) { + result.append(javaPackage.getFullyQualifiedPackageName()); + result.append("."); + } + result.append(getName(tableName, false)); + return new JavaType(result.toString()); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/IdentifierHolder.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/IdentifierHolder.java new file mode 100644 index 000000000..47009411b --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/IdentifierHolder.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.dbre; + +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; + +/** + * Holder for identifier and embedded identifier fields + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class IdentifierHolder { + + private final List embeddedIdentifierFields; + private final boolean embeddedIdField; + private final FieldMetadata identifierField; + + public IdentifierHolder(final FieldMetadata identifierField, + final boolean embeddedIdField, + final List embeddedIdentifierFields) { + Validate.notNull(identifierField, "Identifier field required"); + Validate.notNull(embeddedIdentifierFields, "Fields for " + + identifierField.getFieldType().getFullyQualifiedTypeName() + + " required"); + this.identifierField = identifierField; + this.embeddedIdField = embeddedIdField; + this.embeddedIdentifierFields = embeddedIdentifierFields; + } + + public List getEmbeddedIdentifierFields() { + return embeddedIdentifierFields; + } + + public FieldMetadata getIdentifierField() { + return identifierField; + } + + public boolean isEmbeddedIdField() { + return embeddedIdField; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/RooDbManaged.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/RooDbManaged.java new file mode 100644 index 000000000..eece0f6a7 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/RooDbManaged.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.dbre; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates the lifecycle of the entity is managed by the database reverse + * engineering process. + * + * @author Alan Stewart + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooDbManaged { + + /** + * @return whether to delete the database-managed entity (defaults to true). + */ + boolean automaticallyDelete() default true; +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/converter/IncludeExcludeTablesConverter.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/converter/IncludeExcludeTablesConverter.java new file mode 100644 index 000000000..80650775d --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/converter/IncludeExcludeTablesConverter.java @@ -0,0 +1,47 @@ +package org.springframework.roo.addon.dbre.converter; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion between a space-separated list of table names to a set of + * table names. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class IncludeExcludeTablesConverter implements Converter> { + + public Set convertFromText(final String value, + final Class requiredType, final String optionContext) { + final Set tables = new LinkedHashSet(); + final StringTokenizer st = new StringTokenizer(value, " "); + while (st.hasMoreTokens()) { + tables.add(st.nextToken()); + } + return tables; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Set.class.isAssignableFrom(requiredType) + && (optionContext.contains("include-tables") || optionContext + .contains("exclude-tables")); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/converter/SchemaConverter.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/converter/SchemaConverter.java new file mode 100644 index 000000000..b91ba4e85 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/converter/SchemaConverter.java @@ -0,0 +1,65 @@ +package org.springframework.roo.addon.dbre.converter; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dbre.model.DbreModelService; +import org.springframework.roo.addon.dbre.model.Schema; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion to and from database schemas. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class SchemaConverter implements Converter> { + + @Reference private DbreModelService dbreModelService; + + public Set convertFromText(final String value, + final Class requiredType, final String optionContext) { + final Set schemas = new HashSet(); + for (final String schemaName : StringUtils.split(value, " ")) { + schemas.add(new Schema(schemaName)); + } + return schemas; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + try { + if (dbreModelService.supportsSchema(false)) { + final Set schemas = dbreModelService.getSchemas(false); + for (final Schema schema : schemas) { + completions.add(new Completion(schema.getName())); + } + } + else { + completions.add(new Completion( + DbreModelService.NO_SCHEMA_REQUIRED)); + } + } + catch (final Exception e) { + completions.add(new Completion("unable-to-obtain-connection")); + } + + return true; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Set.class.isAssignableFrom(requiredType) + && optionContext.contains("schema"); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/jdbc/ConnectionProvider.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/jdbc/ConnectionProvider.java new file mode 100644 index 000000000..5f6088171 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/jdbc/ConnectionProvider.java @@ -0,0 +1,73 @@ +package org.springframework.roo.addon.dbre.jdbc; + +import java.sql.Connection; +import java.util.Map; +import java.util.Properties; + +/** + * Provides JDBC (@link Connection}s. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface ConnectionProvider { + + /** + * Closes the given {@link Connection}. + *

    + * An exception will NOT be thrown if the connection cannot be closed. + * + * @param connection the connection to close (may be null). + */ + void closeConnection(Connection connection); + + /** + * Returns a JDBC {@link Connection} configured with the specified + * connection properties map. + *

    + * The properties "user" and "password" are required for the driver to make + * a connection. If the map does not contain these properties, the + * implementing method will need to provide them. + * + * @param map the database connection properties contained in a map + * (required) + * @param displayAddOns displays add-on availability if the JDBC driver + * isn't available (required) + * @return a new connection + * @throws RuntimeException if there is a problem acquiring a connection + */ + Connection getConnection(Map map, boolean displayAddOns) + throws RuntimeException; + + /** + * Returns a JDBC {@link Connection} configured with the specified + * connection properties. + *

    + * The properties "user" and "password" are required for the driver to make + * a connection. If these properties are not supplied, the implementing + * method will need to provide them. + * + * @param props the database connection properties (required) + * @param displayAddOns displays add-on availability if the JDBC driver + * isn't available (required) + * @return a new connection + * @throws RuntimeException if there is a problem acquiring a connection + */ + Connection getConnection(Properties props, boolean displayAddOns) + throws RuntimeException; + + /** + * Returns a JDBC {@link Connection} configured with the specified JNDI + * {@link DataSource} name. + * + * @param jndiDataSource the data source name (required) + * @param map the JNDI properties (required) + * @param displayAddOns displays add-on availability if the JDBC driver + * isn't available (required) + * @return a new connection + * @throws RuntimeException if there is a problem acquiring a connection + */ + Connection getConnectionViaJndiDataSource(String jndiDataSource, + Map map, boolean displayAddOns) + throws RuntimeException; +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/jdbc/ConnectionProviderImpl.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/jdbc/ConnectionProviderImpl.java new file mode 100644 index 000000000..5f1dd6621 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/jdbc/ConnectionProviderImpl.java @@ -0,0 +1,102 @@ +package org.springframework.roo.addon.dbre.jdbc; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.Map; +import java.util.Properties; + +import javax.naming.InitialContext; +import javax.sql.DataSource; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.jdbc.JdbcDriverManager; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Implementation of {@link ConnectionProvider}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class ConnectionProviderImpl implements ConnectionProvider { + + private static final String PASSWORD = "password"; + private static final String USER = "user"; + + @Reference private JdbcDriverManager jdbcDriverManager; + + public void closeConnection(final Connection connection) { + if (connection != null) { + try { + connection.close(); + } + catch (final SQLException ignored) { + } + } + } + + public Connection getConnection(final Map map, + final boolean displayAddOns) throws RuntimeException { + return getConnection(getProps(map), displayAddOns); + } + + public Connection getConnection(final Properties props, + final boolean displayAddOns) throws RuntimeException { + Validate.notEmpty(props, + "Connection properties must not be null or empty"); + + // The properties "user" and "password" are required to make a + // connection + if (props.getProperty(USER) == null) { + props.put(USER, props.getProperty("database.username")); + } + if (props.getProperty(PASSWORD) == null) { + props.put(PASSWORD, props.getProperty("database.password")); + } + + final String driverClassName = props + .getProperty("database.driverClassName"); + final Driver driver = jdbcDriverManager.loadDriver(driverClassName, + displayAddOns); + Validate.notNull(driver, "JDBC driver not available for '%s'", + driverClassName); + try { + return driver.connect(props.getProperty("database.url"), props); + } + catch (final SQLException e) { + throw new IllegalStateException( + "Unable to get connection from driver: " + e.getMessage(), + e); + } + } + + public Connection getConnectionViaJndiDataSource( + final String jndiDataSource, final Map map, + final boolean displayAddOns) throws RuntimeException { + try { + final InitialContext context = new InitialContext(getProps(map)); + final DataSource dataSource = (DataSource) context + .lookup(jndiDataSource); + return dataSource.getConnection(); + } + catch (final Exception e) { + throw new IllegalStateException( + "Unable to get connection from driver: " + e.getMessage(), + e); + } + } + + private Properties getProps(final Map map) { + Validate.isTrue(!CollectionUtils.isEmpty(map), + "Connection properties map must not be null or empty"); + final Properties props = new Properties(); + props.putAll(map); + return props; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/AbstractIntrospector.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/AbstractIntrospector.java new file mode 100644 index 000000000..b6c2e8b5f --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/AbstractIntrospector.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.dbre.model; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.apache.commons.lang3.Validate; + +/** + * Abstract base class for obtaining {@link DatabaseMetaData}. + * + * @author Alan Stewart + * @since 1.1.2 + */ +public abstract class AbstractIntrospector { + protected DatabaseMetaData databaseMetaData; + + AbstractIntrospector(final Connection connection) throws SQLException { + Validate.notNull(connection, "Connection required"); + databaseMetaData = connection.getMetaData(); + Validate.notNull(databaseMetaData, "Database metadata is null"); + } +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/CascadeAction.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/CascadeAction.java new file mode 100644 index 000000000..677906154 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/CascadeAction.java @@ -0,0 +1,31 @@ +package org.springframework.roo.addon.dbre.model; + +/** + * Represents the different cascade actions for the onDelete and + * onUpdate properties of {@link ForeignKey}. + * + * @author Alan Stewart + * @since 1.1 + */ +public enum CascadeAction { + CASCADE("cascade"), NONE("none"), RESTRICT("restrict"), SET_DEFAULT( + "setdefault"), SET_NULL("setnull"); + public static CascadeAction getCascadeAction(final String code) { + for (final CascadeAction cascadeAction : CascadeAction.values()) { + if (cascadeAction.getCode().equals(code)) { + return cascadeAction; + } + } + return NONE; + } + + private String code; + + private CascadeAction(final String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Column.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Column.java new file mode 100644 index 000000000..9f0db9135 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Column.java @@ -0,0 +1,308 @@ +package org.springframework.roo.addon.dbre.model; + +import static org.springframework.roo.model.JavaType.BOOLEAN_OBJECT; +import static org.springframework.roo.model.JavaType.BYTE_ARRAY_PRIMITIVE; +import static org.springframework.roo.model.JavaType.CHAR_OBJECT; +import static org.springframework.roo.model.JavaType.DOUBLE_OBJECT; +import static org.springframework.roo.model.JavaType.FLOAT_OBJECT; +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.JavaType.SHORT_OBJECT; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JdkJavaType.ARRAY; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.BLOB; +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.CLOB; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JdkJavaType.REF; +import static org.springframework.roo.model.JdkJavaType.STRUCT; + +import java.sql.Types; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaType; + +/** + * Represents a column in the database model. + * + * @author Alan Stewart. + * @since 1.1 + */ +public class Column { + private boolean autoIncrement; + private final int columnSize; + private final int dataType; + private String defaultValue; + private String description; + private JavaType javaType; + private String jdbcType; + private final String name; + private boolean primaryKey; + private boolean required; + private int scale = 0; + private final String typeName; + private boolean unique; + + Column(final String name, final int dataType, final String typeName, + final int columnSize, final int scale) { + Validate.notBlank(name, "Column name required"); + this.name = name; + this.dataType = dataType; + this.typeName = typeName; + this.columnSize = columnSize; + this.scale = scale; + init(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Column other = (Column) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + public int getColumnSize() { + return columnSize; + } + + public int getDataType() { + return dataType; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getDescription() { + return description; + } + + public String getEscapedName() { + return name.replaceAll("\\\\", "\\\\\\\\"); + } + + public JavaType getJavaType() { + return javaType; + } + + public String getJdbcType() { + return jdbcType; + } + + public String getName() { + return name; + } + + public int getScale() { + return scale; + } + + public String getTypeName() { + return typeName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + private void init() { + switch (dataType) { + case Types.CHAR: + if (columnSize > 1) { + jdbcType = "VARCHAR"; + javaType = STRING; + } + else { + jdbcType = "CHAR"; + javaType = CHAR_OBJECT; + } + break; + case Types.VARCHAR: + jdbcType = "VARCHAR"; + javaType = STRING; + break; + case Types.LONGVARCHAR: + jdbcType = "LONGVARCHAR"; + javaType = STRING; + break; + case Types.NUMERIC: + jdbcType = "NUMERIC"; + javaType = BIG_DECIMAL; + break; + case Types.DECIMAL: + jdbcType = "DECIMAL"; + javaType = BIG_DECIMAL; + break; + case Types.BOOLEAN: + jdbcType = "BOOLEAN"; + javaType = BOOLEAN_OBJECT; + break; + case Types.BIT: + jdbcType = "BIT"; + javaType = BOOLEAN_OBJECT; + break; + case Types.TINYINT: + jdbcType = "TINYINT"; + javaType = columnSize > 1 ? SHORT_OBJECT : BOOLEAN_OBJECT; // ROO-1860 + break; + case Types.SMALLINT: + jdbcType = "SMALLINT"; + javaType = SHORT_OBJECT; + break; + case Types.INTEGER: + jdbcType = "INTEGER"; + javaType = INT_OBJECT; + break; + case Types.BIGINT: + jdbcType = "BIGINT"; + javaType = LONG_OBJECT; + break; + case Types.REAL: + jdbcType = "REAL"; + javaType = FLOAT_OBJECT; + break; + case Types.FLOAT: + jdbcType = "FLOAT"; + javaType = DOUBLE_OBJECT; + break; + case Types.DOUBLE: + jdbcType = "DOUBLE"; + javaType = DOUBLE_OBJECT; + break; + case Types.BINARY: + jdbcType = "BINARY"; + javaType = BYTE_ARRAY_PRIMITIVE; + break; + case Types.VARBINARY: + jdbcType = "VARBINARY"; + javaType = BYTE_ARRAY_PRIMITIVE; + break; + case Types.LONGVARBINARY: + jdbcType = "LONGVARBINARY"; + javaType = BYTE_ARRAY_PRIMITIVE; + break; + case Types.DATE: + jdbcType = "DATE"; + javaType = DATE; + break; + case Types.TIME: + jdbcType = "TIME"; + javaType = DATE; + break; + case Types.TIMESTAMP: + jdbcType = "TIMESTAMP"; + javaType = CALENDAR; + break; + case Types.CLOB: + jdbcType = "CLOB"; + javaType = CLOB; + break; + case Types.BLOB: + jdbcType = "BLOB"; + javaType = BLOB; + break; + case Types.ARRAY: + jdbcType = "ARRAY"; + javaType = ARRAY; + break; + case Types.DISTINCT: + jdbcType = "DISTINCT"; + javaType = STRING; + break; + case Types.REF: + jdbcType = "REF"; + javaType = REF; + break; + case Types.STRUCT: + jdbcType = "STRUCT"; + javaType = STRUCT; + break; + case Types.NULL: + jdbcType = "NULL"; + break; + case Types.JAVA_OBJECT: + jdbcType = "JAVA_OBJECT"; + javaType = OBJECT; + break; + case Types.OTHER: + jdbcType = "OTHER"; + javaType = STRING; + break; + default: + jdbcType = "VARCHAR"; + javaType = STRING; + break; + } + } + + public boolean isAutoIncrement() { + return autoIncrement; + } + + public boolean isPrimaryKey() { + return primaryKey; + } + + public boolean isRequired() { + return required; + } + + public boolean isUnique() { + return unique; + } + + public void setAutoIncrement(final boolean autoIncrement) { + this.autoIncrement = autoIncrement; + } + + public void setDefaultValue(final String defaultValue) { + this.defaultValue = defaultValue; + } + + public void setDescription(final String description) { + this.description = description; + } + + public void setPrimaryKey(final boolean primaryKey) { + this.primaryKey = primaryKey; + } + + public void setRequired(final boolean required) { + this.required = required; + } + + public void setUnique(final boolean unique) { + this.unique = unique; + } + + @Override + public String toString() { + return String + .format("Column [name=%s, dataType=%s, typeName=%s, columnSize=%s, scale=%s, description=%s, primaryKey=%s, required=%s, unique=%s, autoIncrement=%s, jdbcType=%s, javaType=%s, defaultValue=%s]", + name, dataType, typeName, columnSize, scale, + description, primaryKey, required, unique, + autoIncrement, jdbcType, javaType, defaultValue); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Database.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Database.java new file mode 100644 index 000000000..14a7caf1e --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Database.java @@ -0,0 +1,392 @@ +package org.springframework.roo.addon.dbre.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaPackage; + +/** + * Represents the database model, ie. the tables in the database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Database { + + /** Whether or not to generate active record CRUD methods for each entity */ + private boolean activeRecord; + + /** The JavaPackage where entities are created */ + private JavaPackage destinationPackage; + + /** Whether or not to disable identifier auto generation */ + private boolean disableGeneratedIdentifiers; + + /** Whether or not to disable version fields */ + private boolean disableVersionFields; + + /** + * Whether or not to included non-portable JPA attributes in the @Column + * annotation + */ + private boolean includeNonPortableAttributes; + + /** The module where the entities are created */ + private String moduleName; + + /** Whether or not this database has multiple schemas */ + private boolean multipleSchemas; + + /** Whether or not to generate a repository layer for each entity */ + private boolean repository; + + /** Whether or not to generate a service layer for each entity */ + private boolean service; + + /** All tables. */ + private final Set

    tables; + + /** Whether to create integration tests */ + private boolean testAutomatically; + + /** + * Constructor + * + * @param tables (required) + */ + Database(final Set
    tables) { + Validate.notNull(tables, "Tables required"); + this.tables = tables; + init(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Database other = (Database) obj; + if (tables == null) { + if (other.tables != null) { + return false; + } + } + else if (!tables.equals(other.tables)) { + return false; + } + return true; + } + + public JavaPackage getDestinationPackage() { + return destinationPackage; + } + + public String getModuleName() { + return moduleName; + } + + public Table getTable(final String name, final String schemaName) { + for (final Table table : tables) { + if (table.getName().equals(name)) { + if (StringUtils.isBlank(schemaName) + || DbreModelService.NO_SCHEMA_REQUIRED + .equals(schemaName) + || table.getSchema().getName().equals(schemaName)) { + return table; + } + } + } + return null; + } + + public Set
    getTables() { + return Collections.unmodifiableSet(tables); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (tables == null ? 0 : tables.hashCode()); + return result; + } + + public boolean hasMultipleSchemas() { + return multipleSchemas; + } + + public boolean hasTables() { + return !tables.isEmpty(); + } + + /** + * Indicates whether active record CRUD methods should be generated for each + * entity + * + * @return see above + * @since 1.2.0 + */ + public boolean isActiveRecord() { + return activeRecord; + } + + public boolean isDisableGeneratedIdentifiers() { + return disableGeneratedIdentifiers; + } + + public boolean isDisableVersionFields() { + return disableVersionFields; + } + + public boolean isIncludeNonPortableAttributes() { + return includeNonPortableAttributes; + } + + public boolean isRepository() { + return repository; + } + + public boolean isService() { + return service; + } + + public boolean isTestAutomatically() { + return testAutomatically; + } + + /** + * Sets whether active record CRUD methods should be generated for each + * entity + * + * @param activeRecord + * @since 1.2.0 + */ + public void setActiveRecord(final boolean activeRecord) { + this.activeRecord = activeRecord; + } + + public void setDestinationPackage(final JavaPackage destinationPackage) { + this.destinationPackage = destinationPackage; + } + + public void setDisableGeneratedIdentifiers( + final boolean disableGeneratedIdentifiers) { + this.disableGeneratedIdentifiers = disableGeneratedIdentifiers; + } + + public void setDisableVersionFields(final boolean disableVersionFields) { + this.disableVersionFields = disableVersionFields; + } + + public void setIncludeNonPortableAttributes( + final boolean includeNonPortableAttributes) { + this.includeNonPortableAttributes = includeNonPortableAttributes; + } + + public void setModuleName(final String moduleName) { + this.moduleName = moduleName; + } + + public void setRepository(final boolean repository) { + this.repository = repository; + } + + public void setService(final boolean service) { + this.service = service; + } + + public void setTestAutomatically(final boolean testAutomatically) { + this.testAutomatically = testAutomatically; + } + + @Override + public String toString() { + return String + .format("Database [activeRecord=%s, destinationPackage=%s, disableGeneratedIdentifiers=%s, disableVersionFields=%s, includeNonPortableAttributes=%s, moduleName=%s, multipleSchemas=%s, repository=%s, service=%s, tables=%s, testAutomatically=%s]", + activeRecord, destinationPackage, + disableGeneratedIdentifiers, disableVersionFields, + includeNonPortableAttributes, moduleName, + multipleSchemas, repository, service, tables, + testAutomatically); + } + + /** + * Initializes the model by establishing the relationships between elements + * in this model eg. in foreign keys etc. + */ + private void init() { + final Set schemas = new HashSet(); + for (final Table table : tables) { + schemas.add(table.getSchema()); + initializeImportedKeys(table); + initializeExportedKeys(table); + initializeIndices(table); + initializeJoinTable(table); + } + multipleSchemas = schemas.size() > 1; + } + + private void initializeExportedKeys(final Table table) { + final Map keySequenceMap = new LinkedHashMap(); + Short keySequence = null; + + for (final ForeignKey exportedKey : table.getExportedKeys()) { + if (exportedKey.getForeignTable() != null) { + continue; + } + final String foreignTableName = exportedKey.getForeignTableName(); + final String foreignSchemaName = exportedKey.getForeignSchemaName(); + final Table targetTable = getTable(foreignTableName, + foreignSchemaName); + if (targetTable != null) { + exportedKey.setForeignTable(targetTable); + keySequence = keySequenceMap.get(foreignTableName); + if (keySequence == null) { + keySequence = 0; + keySequenceMap.put(foreignTableName, keySequence); + } + if (table + .getExportedKeyCountByForeignTableName(foreignTableName) > 1) { + keySequenceMap.put(foreignTableName, + (short) (keySequence.shortValue() + 1)); + } + exportedKey.setKeySequence(keySequence); + } + + for (final Reference reference : exportedKey.getReferences()) { + if (reference.getLocalColumn() == null) { + final Column localColumn = table.findColumn(reference + .getLocalColumnName()); + if (localColumn != null) { + reference.setLocalColumn(localColumn); + } + } + if (reference.getForeignColumn() == null + && exportedKey.getForeignTable() != null) { + final Column foreignColumn = exportedKey.getForeignTable() + .findColumn(reference.getForeignColumnName()); + if (foreignColumn != null) { + reference.setForeignColumn(foreignColumn); + } + } + } + } + } + + private void initializeImportedKeys(final Table table) { + final Map keySequenceMap = new LinkedHashMap(); + Short keySequence = null; + final Map> repeatedColumns = new LinkedHashMap>(); + + for (final ForeignKey foreignKey : table.getImportedKeys()) { + if (foreignKey.getForeignTable() != null) { + continue; + } + final String foreignTableName = foreignKey.getForeignTableName(); + final String foreignSchemaName = foreignKey.getForeignSchemaName(); + final Table targetTable = getTable(foreignTableName, + foreignSchemaName); + if (targetTable != null) { + keySequence = keySequenceMap.get(foreignTableName); + if (keySequence == null) { + keySequence = 0; + keySequenceMap.put(foreignTableName, keySequence); + } + foreignKey.setForeignTable(targetTable); + if (table + .getImportedKeyCountByForeignTableName(foreignTableName) > 1) { + keySequenceMap.put(foreignTableName, + (short) (keySequence.shortValue() + 1)); + } + foreignKey.setKeySequence(keySequence); + } + + for (final Reference reference : foreignKey.getReferences()) { + if (reference.getLocalColumn() == null) { + final Column localColumn = table.findColumn(reference + .getLocalColumnName()); + if (localColumn != null) { + reference.setLocalColumn(localColumn); + + final Set fkSet = repeatedColumns + .containsKey(localColumn) ? repeatedColumns + .get(localColumn) + : new LinkedHashSet(); + fkSet.add(foreignKey); + repeatedColumns.put(localColumn, fkSet); + } + } + if (reference.getForeignColumn() == null + && foreignKey.getForeignTable() != null) { + final Column foreignColumn = foreignKey.getForeignTable() + .findColumn(reference.getForeignColumnName()); + if (foreignColumn != null) { + reference.setForeignColumn(foreignColumn); + } + } + } + } + + // Mark repeated columns with insertable = false and updatable = false + for (final Map.Entry> entrySet : repeatedColumns + .entrySet()) { + final Set foreignKeys = entrySet.getValue(); + for (final ForeignKey foreignKey : foreignKeys) { + if (foreignKeys.size() > 1 + || foreignKey.getForeignTableName().equals( + table.getName())) { + for (final Reference reference : foreignKey.getReferences()) { + reference.setInsertableOrUpdatable(false); + } + } + } + } + } + + private void initializeIndices(final Table table) { + for (final Index index : table.getIndices()) { + for (final IndexColumn indexColumn : index.getColumns()) { + final Column column = table.findColumn(indexColumn.getName()); + if (column != null && index.isUnique()) { + column.setUnique(true); + } + } + } + } + + /** + * Determines if a table is a many-to-many join table. + *

    + * To be identified as a many-to-many join table, the table must have have + * exactly two primary keys and have exactly two foreign-keys pointing to + * other entity tables and have no other columns. + */ + private void initializeJoinTable(final Table table) { + boolean equals = table.getColumnCount() == 2 + && table.getPrimaryKeyCount() == 2 + && table.getImportedKeyCount() == 2 + && table.getPrimaryKeyCount() == table.getImportedKeyCount(); + final Iterator iter = table.getColumns().iterator(); + while (equals && iter.hasNext()) { + final Column column = iter.next(); + equals &= table.findImportedKeyByLocalColumnName(column.getName()) != null; + } + if (equals) { + table.setJoinTable(true); + } + } + +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseContentHandler.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseContentHandler.java new file mode 100644 index 000000000..cd10f416f --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseContentHandler.java @@ -0,0 +1,281 @@ +package org.springframework.roo.addon.dbre.model; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.Stack; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.roo.addon.dbre.model.DatabaseXmlUtils.IndexType; +import org.springframework.roo.model.JavaPackage; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * {@link ContentHandler} implementation for converting the DBRE XML file into a + * {@link Database} object. + * + * @author Alan Stewart + * @since 1.1 + */ +public class DatabaseContentHandler extends DefaultHandler { + + private boolean activeRecord; + + private Database database; + private JavaPackage destinationPackage; + private boolean disableGeneratedIdentifiers; + private boolean disableVersionFields; + private boolean includeNonPortableAttributes; + private String moduleName; + private boolean repository; + private boolean service; + private final Stack stack = new Stack(); + private final Set

    tables = new LinkedHashSet
    (); + private boolean testAutomatically; + + /** + * Constructor + */ + public DatabaseContentHandler() { + super(); + } + + @Override + public void endElement(final String uri, final String localName, + final String qName) throws SAXException { + final Object tmp = stack.pop(); + + if (qName.equals("option")) { + final Option option = (Option) tmp; + if (stack.peek() instanceof ForeignKey) { + if (option.getKey().equals("exported")) { + ((ForeignKey) stack.peek()).setExported(Boolean + .parseBoolean(option.getValue())); + } + if (option.getKey().equals("foreignSchemaName")) { + ((ForeignKey) stack.peek()).setForeignSchemaName(option + .getValue()); + } + } + if (option.getKey().equals("moduleName")) { + moduleName = option.getValue(); + } + if (option.getKey().equals("includeNonPortableAttributes")) { + includeNonPortableAttributes = Boolean.parseBoolean(option + .getValue()); + } + if (option.getKey().equals("disableVersionFields")) { + disableVersionFields = Boolean.parseBoolean(option.getValue()); + } + if (option.getKey().equals("disableGeneratedIdentifiers")) { + disableGeneratedIdentifiers = Boolean.parseBoolean(option + .getValue()); + } + if (option.getKey().equals("activeRecord")) { + activeRecord = Boolean.parseBoolean(option.getValue()); + } + if (option.getKey().equals("repository")) { + repository = Boolean.parseBoolean(option.getValue()); + } + if (option.getKey().equals("service")) { + service = Boolean.parseBoolean(option.getValue()); + } + if (option.getKey().equals("testAutomatically")) { + testAutomatically = Boolean.parseBoolean(option.getValue()); + } + } + else if (qName.equals("table")) { + tables.add((Table) tmp); + } + else if (qName.equals("column")) { + ((Table) stack.peek()).addColumn((Column) tmp); + } + else if (qName.equals("foreign-key")) { + final ForeignKey foreignKey = (ForeignKey) tmp; + final Table table = (Table) stack.peek(); + if (foreignKey.isExported()) { + table.addExportedKey(foreignKey); + } + else { + table.addImportedKey(foreignKey); + } + } + else if (qName.equals("reference")) { + ((ForeignKey) stack.peek()).addReference((Reference) tmp); + } + else if (qName.equals("unique") || qName.equals("index")) { + ((Table) stack.peek()).addIndex((Index) tmp); + } + else if (qName.equals("unique-column") || qName.equals("index-column")) { + ((Index) stack.peek()).addColumn((IndexColumn) tmp); + } + else if (qName.equals("database")) { + database = new Database(tables); + database.setModuleName(moduleName); + database.setDestinationPackage(destinationPackage); + database.setIncludeNonPortableAttributes(includeNonPortableAttributes); + database.setDisableVersionFields(disableVersionFields); + database.setDisableGeneratedIdentifiers(disableGeneratedIdentifiers); + database.setActiveRecord(activeRecord); + database.setRepository(repository); + database.setService(service); + database.setTestAutomatically(testAutomatically); + } + else { + stack.push(tmp); + } + } + + public Database getDatabase() { + return database; + } + + @Override + public void startElement(final String uri, final String localName, + final String qName, final Attributes attributes) + throws SAXException { + if (qName.equals("database")) { + stack.push(new Object()); + if (StringUtils.isNotBlank(attributes.getValue("package"))) { + destinationPackage = new JavaPackage( + attributes.getValue("package")); + } + } + else if (qName.equals("option")) { + stack.push(new Option(attributes.getValue("key"), attributes + .getValue("value"))); + } + else if (qName.equals("table")) { + stack.push(getTable(attributes)); + } + else if (qName.equals("column")) { + stack.push(getColumn(attributes)); + } + else if (qName.equals("foreign-key")) { + stack.push(getForeignKey(attributes)); + } + else if (qName.equals("reference")) { + stack.push(getReference(attributes)); + } + else if (qName.equals("unique")) { + stack.push(getIndex(attributes, IndexType.UNIQUE)); + } + else if (qName.equals("index")) { + stack.push(getIndex(attributes, IndexType.INDEX)); + } + else if (qName.equals("unique-column") || qName.equals("index-column")) { + stack.push(getIndexColumn(attributes)); + } + } + + private Column getColumn(final Attributes attributes) { + final String type = attributes.getValue("type"); + final String[] dataTypeAndName = StringUtils.split(type, ","); + Validate.notNull( + dataTypeAndName, + "The 'type' attribute of the column element must contain a comma separated value pair, eg, type=\"12,varchar\"." + + getErrorMessage()); + final int dataType = Integer.parseInt(dataTypeAndName[0]); + final String typeName = dataTypeAndName[1]; + + int columnSize; + int scale = 0; + final String size = attributes.getValue("size"); + if (size.contains(",")) { + final String[] precisionScale = StringUtils.split(size, ","); + columnSize = Integer.parseInt(precisionScale[0]); + scale = Integer.parseInt(precisionScale[1]); + } + else { + columnSize = Integer.parseInt(size); + } + + if (StringUtils.isNotBlank(attributes.getValue("scale"))) { + scale = Integer.parseInt(attributes.getValue("scale")); + } + + final Column column = new Column( + attributes.getValue(DatabaseXmlUtils.NAME), dataType, typeName, + columnSize, scale); + column.setDescription(attributes.getValue(DatabaseXmlUtils.DESCRIPTION)); + column.setPrimaryKey(Boolean.parseBoolean(attributes + .getValue("primaryKey"))); + column.setRequired(Boolean.parseBoolean(attributes.getValue("required"))); + + return column; + } + + private String getErrorMessage() { + return "Your DBRE XML file may be not be in the current format. Delete the file and execute the database reverse engineer command again."; + } + + private ForeignKey getForeignKey(final Attributes attributes) { + final ForeignKey foreignKey = new ForeignKey( + attributes.getValue(DatabaseXmlUtils.NAME), + attributes.getValue(DatabaseXmlUtils.FOREIGN_TABLE)); + foreignKey.setOnDelete(CascadeAction.getCascadeAction(attributes + .getValue(DatabaseXmlUtils.ON_DELETE))); + foreignKey.setOnUpdate(CascadeAction.getCascadeAction(attributes + .getValue(DatabaseXmlUtils.ON_UPDATE))); + return foreignKey; + } + + private Index getIndex(final Attributes attributes, + final IndexType indexType) { + final Index index = new Index( + attributes.getValue(DatabaseXmlUtils.NAME)); + index.setUnique(indexType == IndexType.UNIQUE); + return index; + } + + private IndexColumn getIndexColumn(final Attributes attributes) { + return new IndexColumn(attributes.getValue(DatabaseXmlUtils.NAME)); + } + + private Reference getReference(final Attributes attributes) { + return new Reference(attributes.getValue(DatabaseXmlUtils.LOCAL), + attributes.getValue(DatabaseXmlUtils.FOREIGN)); + } + + private Table getTable(final Attributes attributes) { + final Table table = new Table( + attributes.getValue(DatabaseXmlUtils.NAME), new Schema( + attributes.getValue("alias"))); + if (StringUtils.isNotBlank(attributes + .getValue(DatabaseXmlUtils.DESCRIPTION))) { + table.setDescription(DatabaseXmlUtils.DESCRIPTION); + } + return table; + } + + private static class Option extends Pair { + + private static final long serialVersionUID = 3471455277824528758L; + private final String key; + private final String value; + + public Option(final String key, final String value) { + this.key = key; + this.value = value; + } + + @Override + public String getLeft() { + return key; + } + + @Override + public String getRight() { + return value; + } + + @Override + public String setValue(final String value) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseIntrospector.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseIntrospector.java new file mode 100644 index 000000000..5f543e98e --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseIntrospector.java @@ -0,0 +1,338 @@ +package org.springframework.roo.addon.dbre.model; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +/** + * Creates a {@link Database database} model from a live database using JDBC. + * + * @author Alan Stewart + * @since 1.1 + */ +public class DatabaseIntrospector extends AbstractIntrospector { + + private final Set excludeTables; + private final Set includeTables; + private final Set schemas; + private final boolean view; + + public DatabaseIntrospector(final Connection connection, + final Set schemas, final boolean view, + final Set includeTables, final Set excludeTables) + throws SQLException { + super(connection); + this.schemas = schemas; + this.view = view; + this.includeTables = includeTables; + this.excludeTables = excludeTables; + } + + public Database createDatabase() throws SQLException { + final Set
    tables = new LinkedHashSet
    (); + for (final Schema schema : schemas) { + tables.addAll(getTables(schema)); + } + return new Database(tables); + } + + private Index findIndex(final String name, final Set indices) { + for (final Index index : indices) { + if (index.getName().equalsIgnoreCase(name)) { + return index; + } + } + return null; + } + + private String getArtifact(final String artifactName) throws SQLException { + if (databaseMetaData.storesLowerCaseIdentifiers()) { + return StringUtils.lowerCase(artifactName); + } + else if (databaseMetaData.storesUpperCaseIdentifiers()) { + return StringUtils.upperCase(artifactName); + } + else { + return artifactName; + } + } + + private CascadeAction getCascadeAction(final Short actionValue) { + CascadeAction cascadeAction; + switch (actionValue.intValue()) { + case DatabaseMetaData.importedKeyCascade: + cascadeAction = CascadeAction.CASCADE; + break; + case DatabaseMetaData.importedKeySetNull: + cascadeAction = CascadeAction.SET_NULL; + break; + case DatabaseMetaData.importedKeySetDefault: + cascadeAction = CascadeAction.SET_DEFAULT; + break; + case DatabaseMetaData.importedKeyRestrict: + cascadeAction = CascadeAction.RESTRICT; + break; + case DatabaseMetaData.importedKeyNoAction: + cascadeAction = CascadeAction.NONE; + break; + default: + cascadeAction = CascadeAction.NONE; + } + return cascadeAction; + } + + private Set
    getTables(final Schema schema) throws SQLException { + final Set
    tables = new LinkedHashSet
    (); + + final String[] types = view ? new String[] { TableType.TABLE.name(), + TableType.VIEW.name() } + : new String[] { TableType.TABLE.name() }; + final ResultSet rs = databaseMetaData.getTables(null, + getArtifact(schema.getName()), null, types); + try { + while (rs.next()) { + final String tableName = rs.getString("TABLE_NAME"); + + // Check for certain tables such as Oracle recycle bin tables, + // and ignore + if (ignoreTables(tableName)) { + continue; + } + + if (hasIncludedTable(tableName) && !hasExcludedTable(tableName)) { + final Table table = new Table(tableName, new Schema( + rs.getString("TABLE_SCHEM"))); + table.setCatalog(rs.getString("TABLE_CAT")); + table.setDescription(rs.getString("REMARKS")); + + readColumns(table); + readForeignKeys(table, false); + readForeignKeys(table, true); + readIndices(table); + + for (final String columnName : readPrimaryKeyNames(table)) { + final Column column = table.findColumn(columnName); + if (column != null) { + column.setPrimaryKey(true); + } + } + + tables.add(table); + } + } + } + finally { + rs.close(); + } + + return tables; + } + + private boolean hasExcludedTable(final String tableName) { + if (excludeTables == null || excludeTables.isEmpty()) { + return false; + } + return hasTable(excludeTables, tableName); + } + + private boolean hasIncludedTable(final String tableName) { + if (includeTables == null || includeTables.isEmpty()) { + return true; + } + return hasTable(includeTables, tableName); + } + + private boolean hasTable(final Set tables, final String tableName) { + for (final String table : tables) { + final String regex = table.replaceAll("\\*", ".*").replaceAll( + "\\?", ".?"); + final Pattern pattern = Pattern.compile(regex); + if (pattern.matcher(tableName).matches()) { + return true; + } + } + return false; + } + + private boolean ignoreTables(final String tableName) { + boolean ignore = false; + try { + if ("Oracle".equalsIgnoreCase(databaseMetaData + .getDatabaseProductName()) && tableName.startsWith("BIN$")) { + ignore = true; + } + if ("MySQL".equalsIgnoreCase(databaseMetaData + .getDatabaseProductName()) && tableName.equals("SEQUENCE")) { + ignore = true; + } + } + catch (final SQLException ignored) { + } + return ignore; + } + + private void readColumns(final Table table) throws SQLException { + final ResultSet rs = databaseMetaData.getColumns(table.getCatalog(), + table.getSchema().getName(), table.getName(), null); + try { + while (rs.next()) { + final Column column = new Column(rs.getString("COLUMN_NAME"), + rs.getInt("DATA_TYPE"), rs.getString("TYPE_NAME"), + rs.getInt("COLUMN_SIZE"), rs.getInt("DECIMAL_DIGITS")); + column.setDescription(rs.getString("REMARKS")); + column.setDefaultValue(rs.getString("COLUMN_DEF")); + column.setRequired("NO".equalsIgnoreCase(rs + .getString("IS_NULLABLE"))); + + table.addColumn(column); + } + } + finally { + rs.close(); + } + } + + private void readForeignKeys(final Table table, final boolean exported) + throws SQLException { + final Map foreignKeys = new LinkedHashMap(); + + ResultSet rs; + if (exported) { + rs = databaseMetaData.getExportedKeys(table.getCatalog(), table + .getSchema().getName(), table.getName()); + } + else { + rs = databaseMetaData.getImportedKeys(table.getCatalog(), table + .getSchema().getName(), table.getName()); + } + + try { + while (rs.next()) { + final String name = rs.getString("FK_NAME"); + final String foreignTableName = rs + .getString(exported ? "FKTABLE_NAME" : "PKTABLE_NAME"); + final String key = name + "_" + foreignTableName; + + if (!hasExcludedTable(foreignTableName)) { + final ForeignKey foreignKey = new ForeignKey(name, + foreignTableName); + foreignKey.setForeignSchemaName(StringUtils.defaultIfEmpty( + rs.getString(exported ? "FKTABLE_SCHEM" + : "PKTABLE_SCHEM"), + DbreModelService.NO_SCHEMA_REQUIRED)); + foreignKey.setOnUpdate(getCascadeAction(rs + .getShort("UPDATE_RULE"))); + foreignKey.setOnDelete(getCascadeAction(rs + .getShort("DELETE_RULE"))); + foreignKey.setExported(exported); + + final String localColumnName = rs + .getString(exported ? "PKCOLUMN_NAME" + : "FKCOLUMN_NAME"); + final String foreignColumnName = rs + .getString(exported ? "FKCOLUMN_NAME" + : "PKCOLUMN_NAME"); + final Reference reference = new Reference(localColumnName, + foreignColumnName); + + if (foreignKeys.containsKey(key)) { + foreignKeys.get(key).addReference(reference); + } + else { + foreignKey.addReference(reference); + foreignKeys.put(key, foreignKey); + } + } + } + } + finally { + rs.close(); + } + + for (final ForeignKey foreignKey : foreignKeys.values()) { + if (exported) { + table.addExportedKey(foreignKey); + } + else { + table.addImportedKey(foreignKey); + } + } + } + + private void readIndices(final Table table) throws SQLException { + final Set indices = new LinkedHashSet(); + + ResultSet rs; + try { + // Catching SQLException here due to Oracle throwing exception when + // attempting to retrieve indices for deleted tables that exist in + // Oracle's recycle bin + rs = databaseMetaData.getIndexInfo(table.getCatalog(), table + .getSchema().getName(), table.getName(), false, false); + } + catch (final SQLException e) { + return; + } + + if (rs != null) { + try { + while (rs.next()) { + final Short type = rs.getShort("TYPE"); + if (type == DatabaseMetaData.tableIndexStatistic) { + continue; + } + + final String indexName = rs.getString("INDEX_NAME"); + Index index = findIndex(indexName, indices); + if (index == null) { + index = new Index(indexName); + } + else { + indices.remove(index); + } + index.setUnique(!rs.getBoolean("NON_UNIQUE")); + + final IndexColumn indexColumn = new IndexColumn( + rs.getString("COLUMN_NAME")); + index.addColumn(indexColumn); + + indices.add(index); + } + } + finally { + rs.close(); + } + } + + for (final Index index : indices) { + table.addIndex(index); + } + } + + private Set readPrimaryKeyNames(final Table table) + throws SQLException { + final Set columnNames = new LinkedHashSet(); + + final ResultSet rs = databaseMetaData.getPrimaryKeys( + table.getCatalog(), table.getSchema().getName(), + table.getName()); + try { + while (rs.next()) { + columnNames.add(rs.getString("COLUMN_NAME")); + } + } + finally { + rs.close(); + } + + return columnNames; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseXmlUtils.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseXmlUtils.java new file mode 100644 index 000000000..eaaaeffc0 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DatabaseXmlUtils.java @@ -0,0 +1,370 @@ +package org.springframework.roo.addon.dbre.model; + +import java.io.InputStream; +import java.util.EmptyStackException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Comment; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Assists converting a {@link Database} to and from XML using DOM or SAX. + * + * @author Alan Stewart + * @since 1.1 + */ +public abstract class DatabaseXmlUtils { + + public static enum IndexType { + INDEX, UNIQUE + } + + public static final String DESCRIPTION = "description"; + public static final String FOREIGN = "foreign"; + public static final String FOREIGN_TABLE = "foreignTable"; + public static final String LOCAL = "local"; + public static final String NAME = "name"; + public static final String ON_DELETE = "onDelete"; + public static final String ON_UPDATE = "onUpdate"; + public static final String REFERENCE = "reference"; + + /** + * Adds an
    tables = new LinkedHashSet
    (); + + final List tableElements = XmlUtils.findElements("table", + databaseElement); + for (final Element tableElement : tableElements) { + final String alias = tableElement.getAttribute("alias"); + final String schemaName = StringUtils.defaultIfEmpty(alias, + databaseElement.getAttribute(NAME)); + final Table table = new Table(tableElement.getAttribute(NAME), + new Schema(schemaName)); + if (StringUtils.isNotBlank(tableElement.getAttribute(DESCRIPTION))) { + table.setDescription(tableElement.getAttribute(DESCRIPTION)); + } + + final List columnElements = XmlUtils.findElements( + "column", tableElement); + for (final Element columnElement : columnElements) { + final String type = columnElement.getAttribute("type"); + final String[] dataTypeAndName = StringUtils.split(type, ","); + final int dataType = Integer.parseInt(dataTypeAndName[0]); + final String typeName = dataTypeAndName[1]; + + final int columnSize = Integer.parseInt(columnElement + .getAttribute("size")); + final int scale = Integer.parseInt(columnElement + .getAttribute("scale")); + + final Column column = new Column( + columnElement.getAttribute(NAME), dataType, typeName, + columnSize, scale); + column.setDescription(columnElement.getAttribute(DESCRIPTION)); + column.setPrimaryKey(Boolean.parseBoolean(columnElement + .getAttribute("primaryKey"))); + column.setRequired(Boolean.parseBoolean(columnElement + .getAttribute("required"))); + + table.addColumn(column); + } + + final List foreignKeyElements = XmlUtils.findElements( + "foreign-key", tableElement); + for (final Element foreignKeyElement : foreignKeyElements) { + final ForeignKey foreignKey = new ForeignKey( + foreignKeyElement.getAttribute(NAME), + foreignKeyElement.getAttribute(FOREIGN_TABLE)); + foreignKey.setOnDelete(CascadeAction + .getCascadeAction(foreignKeyElement + .getAttribute(ON_DELETE))); + foreignKey.setOnUpdate(CascadeAction + .getCascadeAction(foreignKeyElement + .getAttribute(ON_UPDATE))); + + final List optionElements = XmlUtils.findElements( + "option", foreignKeyElement); + for (final Element optionElement : optionElements) { + final String key = optionElement.getAttribute("key"); + final String value = optionElement.getAttribute("value"); + if (key.equals("exported")) { + foreignKey.setExported(Boolean.parseBoolean(value)); + } + if (key.equals("foreignSchemaName")) { + foreignKey.setForeignSchemaName(value); + } + } + + final List referenceElements = XmlUtils.findElements( + REFERENCE, foreignKeyElement); + for (final Element referenceElement : referenceElements) { + final Reference reference = new Reference( + referenceElement.getAttribute(LOCAL), + referenceElement.getAttribute(FOREIGN)); + foreignKey.addReference(reference); + } + table.addImportedKey(foreignKey); + } + + addIndices(table, tableElement, IndexType.INDEX); + addIndices(table, tableElement, IndexType.UNIQUE); + + tables.add(table); + } + + JavaPackage destinationPackage = null; + if (StringUtils.isNotBlank(databaseElement.getAttribute("package"))) { + destinationPackage = new JavaPackage( + databaseElement.getAttribute("package")); + } + + final Database database = new Database(tables); + database.setDestinationPackage(destinationPackage); + + final List optionElements = XmlUtils.findElements("option", + databaseElement); + for (final Element optionElement : optionElements) { + final String key = optionElement.getAttribute("key"); + final String value = optionElement.getAttribute("value"); + if (key.equals("moduleName")) { + database.setModuleName(value); + } + if (key.equals("activeRecord")) { + database.setActiveRecord(Boolean.parseBoolean(value)); + } + if (key.equals("repository")) { + database.setRepository(Boolean.parseBoolean(value)); + } + if (key.equals("service")) { + database.setService(Boolean.parseBoolean(value)); + } + if (key.equals("testAutomatically")) { + database.setTestAutomatically(Boolean.parseBoolean(value)); + } + if (key.equals("includeNonPortableAttributes")) { + database.setIncludeNonPortableAttributes(Boolean + .parseBoolean(value)); + } + if (key.equals("disableVersionFields")) { + database.setDisableVersionFields(Boolean.parseBoolean(value)); + } + if (key.equals("disableGeneratedIdentifiers")) { + database.setDisableGeneratedIdentifiers(Boolean + .parseBoolean(value)); + } + } + + return database; + } +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DbreModelService.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DbreModelService.java new file mode 100644 index 000000000..fad4475ce --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DbreModelService.java @@ -0,0 +1,72 @@ +package org.springframework.roo.addon.dbre.model; + +import java.util.Set; + +/** + * Retrieves database metadata from the DBRE XML file or a JDBC connection. Also + * writes database metadata to the DBRE XML file. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface DbreModelService { + + /** The DBRE XML file name */ + String DBRE_XML = "dbre.xml"; + + /** + * The schema string for databases which do not support schemas, such as + * MySQL. + */ + String NO_SCHEMA_REQUIRED = "no-schema-required"; + + /** + * Reads the database metadata information from either a cache or from the + * DBRE XML file if possible. + * + * @param evictCache forces eviction of the database from the cache before + * attempting retrieval + * @return the database metadata if it could be parsed, otherwise null if + * unavailable for any reason + */ + Database getDatabase(boolean evictCache); + + /** + * Returns a Set of available database {@link Schema schemas}. + * + * @param displayAddOns display available add-ons if possible (required) + * @return a Set of schemas. + */ + Set getSchemas(boolean displayAddOns); + + /** + * Retrieves the database metadata from a JDBC connection. + * + * @param schemas the schema(s) to query (required) + * @param view true if database views are to be retrieved, otherwise false + * @param includeTables a set of table names to include + * @param excludeTables a set of table names to exlude + * @return the database metadata if available (null if cannot connect to the + * database or the schema is not found) + */ + Database refreshDatabase(Set schemas, boolean view, + Set includeTables, Set excludeTables); + + /** + * Determines if the database uses schemas. + *

    + * Examples of databases that do not use schemas are MySQL and Firebird. + * + * @param displayAddOns display available add-ons if possible (required) + * @return true if the database supports schema, otherwise false; + * @throws RuntimeException if there is a problem acquiring a connection + */ + boolean supportsSchema(boolean displayAddOns) throws RuntimeException; + + /** + * Serializes the database to the DBRE XML file. + * + * @param database the database to be written out to disk + */ + void writeDatabase(Database database); +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DbreModelServiceImpl.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DbreModelServiceImpl.java new file mode 100644 index 000000000..309bddbe3 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/DbreModelServiceImpl.java @@ -0,0 +1,260 @@ +package org.springframework.roo.addon.dbre.model; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dbre.jdbc.ConnectionProvider; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of {@link DbreModelService}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class DbreModelServiceImpl implements DbreModelService { + + private final Set cachedIntrospections = new HashSet(); + @Reference private ConnectionProvider connectionProvider; + @Reference private FileManager fileManager; + private Database lastDatabase; + + @Reference private ProjectOperations projectOperations; + @Reference private PropFileOperations propFileOperations; + + private void cacheDatabase(final Database database) { + if (database != null) { + lastDatabase = database; + cachedIntrospections.add(database); + } + } + + private Connection getConnection(final boolean displayAddOns) { + final String dbProps = "database.properties"; + final String jndiDataSource = getJndiDataSourceName(); + if (StringUtils.isNotBlank(jndiDataSource)) { + final Map props = propFileOperations.getProperties( + Path.SPRING_CONFIG_ROOT.getModulePathId(projectOperations + .getFocusedModuleName()), "jndi.properties"); + return connectionProvider.getConnectionViaJndiDataSource( + jndiDataSource, props, displayAddOns); + } + else if (fileManager.exists(projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, dbProps))) { + final Map props = propFileOperations.getProperties( + Path.SPRING_CONFIG_ROOT.getModulePathId(projectOperations + .getFocusedModuleName()), dbProps); + return connectionProvider.getConnection(props, displayAddOns); + } + + final Properties connectionProperties = getConnectionPropertiesFromDataNucleusConfiguration(); + return connectionProvider.getConnection(connectionProperties, + displayAddOns); + } + + private Properties getConnectionPropertiesFromDataNucleusConfiguration() { + final String persistenceXmlPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, + "META-INF/persistence.xml"); + if (!fileManager.exists(persistenceXmlPath)) { + throw new IllegalStateException("Failed to find " + + persistenceXmlPath); + } + + final FileDetails fileDetails = fileManager + .readFile(persistenceXmlPath); + Document document = null; + try { + final InputStream is = new BufferedInputStream(new FileInputStream( + fileDetails.getFile())); + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + builder.setErrorHandler(null); + document = builder.parse(is); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + + final List propertyElements = XmlUtils.findElements( + "/persistence/persistence-unit/properties/property", + document.getDocumentElement()); + Validate.notEmpty(propertyElements, + "Failed to find property elements in %s", persistenceXmlPath); + final Properties properties = new Properties(); + + for (final Element propertyElement : propertyElements) { + final String key = propertyElement.getAttribute("name"); + final String value = propertyElement.getAttribute("value"); + if ("datanucleus.ConnectionDriverName".equals(key)) { + properties.put("database.driverClassName", value); + } + if ("datanucleus.ConnectionURL".equals(key)) { + properties.put("database.url", value); + } + if ("datanucleus.ConnectionUserName".equals(key)) { + properties.put("database.username", value); + } + if ("datanucleus.ConnectionPassword".equals(key)) { + properties.put("database.password", value); + } + + if (properties.size() == 4) { + // All required properties have been found so ignore rest of + // elements + break; + } + } + return properties; + } + + public Database getDatabase(final boolean evictCache) { + if (!evictCache && cachedIntrospections.contains(lastDatabase)) { + for (final Database database : cachedIntrospections) { + if (database.equals(lastDatabase)) { + return lastDatabase; + } + } + } + if (evictCache && cachedIntrospections.contains(lastDatabase)) { + cachedIntrospections.remove(lastDatabase); + } + + final String dbreXmlPath = getDbreXmlPath(); + if (StringUtils.isBlank(dbreXmlPath) + || !fileManager.exists(dbreXmlPath)) { + return null; + } + + Database database = null; + InputStream inputStream = null; + try { + inputStream = fileManager.getInputStream(dbreXmlPath); + database = DatabaseXmlUtils.readDatabase(inputStream); + cacheDatabase(database); + return database; + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + private String getDbreXmlPath() { + for (final String moduleName : projectOperations.getModuleNames()) { + final LogicalPath logicalPath = LogicalPath.getInstance( + Path.SRC_MAIN_RESOURCES, moduleName); + final String dbreXmlPath = projectOperations.getPathResolver() + .getIdentifier(logicalPath, DBRE_XML); + if (fileManager.exists(dbreXmlPath)) { + return dbreXmlPath; + } + } + return projectOperations.getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, DBRE_XML); + } + + private String getJndiDataSourceName() { + final String contextPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext.xml"); + final Document appCtx = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = appCtx.getDocumentElement(); + final Element dataSourceJndi = XmlUtils.findFirstElement( + "/beans/jndi-lookup[@id = 'dataSource']", root); + return dataSourceJndi != null ? dataSourceJndi + .getAttribute("jndi-name") : null; + } + + public Set getSchemas(final boolean displayAddOns) { + Connection connection = null; + try { + connection = getConnection(displayAddOns); + final SchemaIntrospector introspector = new SchemaIntrospector( + connection); + return introspector.getSchemas(); + } + catch (final Exception e) { + return Collections.emptySet(); + } + finally { + connectionProvider.closeConnection(connection); + } + } + + public Database refreshDatabase(final Set schemas, + final boolean view, final Set includeTables, + final Set excludeTables) { + Validate.notNull(schemas, "Schemas required"); + + Connection connection = null; + try { + connection = getConnection(true); + final DatabaseIntrospector introspector = new DatabaseIntrospector( + connection, schemas, view, includeTables, excludeTables); + final Database database = introspector.createDatabase(); + cacheDatabase(database); + return database; + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + connectionProvider.closeConnection(connection); + } + } + + public boolean supportsSchema(final boolean displayAddOns) + throws RuntimeException { + Connection connection = null; + try { + connection = getConnection(displayAddOns); + final DatabaseMetaData databaseMetaData = connection.getMetaData(); + final String schemaTerm = databaseMetaData.getSchemaTerm(); + return StringUtils.isNotBlank(schemaTerm) + && schemaTerm.equalsIgnoreCase("schema"); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + connectionProvider.closeConnection(connection); + } + } + + public void writeDatabase(final Database database) { + final Document document = DatabaseXmlUtils + .getDatabaseDocument(database); + fileManager.createOrUpdateTextFileIfRequired(getDbreXmlPath(), + XmlUtils.nodeToString(document), true); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/ForeignKey.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/ForeignKey.java new file mode 100644 index 000000000..ff499eec4 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/ForeignKey.java @@ -0,0 +1,197 @@ +package org.springframework.roo.addon.dbre.model; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.lang3.Validate; + +/** + * Represents a database foreign key. + *

    + * A foreign key is modeled from the + * {@link java.sql.DatabaseMetaData#getImportedKeys(String, String, String)} or + * {@link java.sql.DatabaseMetaData#getExportedKeys(String, String, String)} + * methods. + * + * @author Alan Stewart + * @since 1.1 + */ +public class ForeignKey { + + /** Whether the foreign key is an imported or exported key */ + private boolean exported; + + /** The schema name of the foreign table. */ + private String foreignSchemaName; + + /** The target table. */ + private Table foreignTable; + + /** The name of the foreign (target) table. */ + private final String foreignTableName; + + /** + * The sequence number of the key within the table for a given foreign + * table. + */ + private Short keySequence; + + /** The name of the foreign key, may be null. */ + private final String name; + + /** The action to perform when the referenced row is deleted. */ + private CascadeAction onDelete = CascadeAction.NONE; + + /** The action to perform when the value of the referenced column changes. */ + private CascadeAction onUpdate = CascadeAction.NONE; + + /** The references between local and remote columns. */ + private final Set references = new LinkedHashSet(); + + ForeignKey(final String name, final String foreignTableName) { + this.name = name; + this.foreignTableName = foreignTableName; + } + + public void addReference(final Reference reference) { + Validate.notNull(reference, "Reference required"); + references.add(reference); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ForeignKey other = (ForeignKey) obj; + if (exported != other.exported) { + return false; + } + if (foreignSchemaName == null) { + if (other.foreignSchemaName != null) { + return false; + } + } + else if (!foreignSchemaName.equals(other.foreignSchemaName)) { + return false; + } + if (foreignTableName == null) { + if (other.foreignTableName != null) { + return false; + } + } + else if (!foreignTableName.equals(other.foreignTableName)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + String getForeignSchemaName() { + return foreignSchemaName; + } + + public Table getForeignTable() { + return foreignTable; + } + + String getForeignTableName() { + return foreignTableName; + } + + public Short getKeySequence() { + return keySequence; + } + + public String getName() { + return name; + } + + public CascadeAction getOnDelete() { + return onDelete; + } + + public CascadeAction getOnUpdate() { + return onUpdate; + } + + public int getReferenceCount() { + return references.size(); + } + + public Set getReferences() { + return references; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (exported ? 1231 : 1237); + result = prime + * result + + (foreignSchemaName == null ? 0 : foreignSchemaName.hashCode()); + result = prime * result + + (foreignTableName == null ? 0 : foreignTableName.hashCode()); + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + public boolean hasLocalColumn(final Column column) { + for (final Reference reference : references) { + if (reference.getLocalColumn().equals(column)) { + return true; + } + } + return false; + } + + public boolean isExported() { + return exported; + } + + public void setExported(final boolean exported) { + this.exported = exported; + } + + void setForeignSchemaName(final String foreignSchemaName) { + this.foreignSchemaName = foreignSchemaName; + } + + void setForeignTable(final Table foreignTable) { + this.foreignTable = foreignTable; + } + + public void setKeySequence(final Short keySequence) { + this.keySequence = keySequence; + } + + public void setOnDelete(final CascadeAction onDelete) { + this.onDelete = onDelete; + } + + public void setOnUpdate(final CascadeAction onUpdate) { + this.onUpdate = onUpdate; + } + + @Override + public String toString() { + return String + .format("ForeignKey [name=%s, exported=%s, foreignTableName=%s, foreignSchemaName=%s, onUpdate=%s, onDelete=%s, keySequence=%s, references=%s]", + name, exported, foreignTableName, foreignSchemaName, + onUpdate, onDelete, keySequence, references); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Index.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Index.java new file mode 100644 index 000000000..986eef738 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Index.java @@ -0,0 +1,91 @@ +package org.springframework.roo.addon.dbre.model; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.lang3.Validate; + +/** + * Represents an index definition for a table which may be either unique or + * non-unique. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Index { + + private final Set columns = new LinkedHashSet(); + private String name; + private boolean unique; + + /** + * Constructor + * + * @param name + */ + Index(final String name) { + this.name = name; + } + + public boolean addColumn(final IndexColumn indexColumn) { + Validate.notNull(indexColumn, "Column required"); + return columns.add(indexColumn); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Index)) { + return false; + } + final Index other = (Index) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + public Set getColumns() { + return columns; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + public boolean isUnique() { + return unique; + } + + public void setName(final String name) { + this.name = name; + } + + public void setUnique(final boolean unique) { + this.unique = unique; + } + + @Override + public String toString() { + return String.format("Index [name=%s, unique=%s, columns=%s]", name, + unique, columns); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/IndexColumn.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/IndexColumn.java new file mode 100644 index 000000000..2063e153a --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/IndexColumn.java @@ -0,0 +1,68 @@ +package org.springframework.roo.addon.dbre.model; + +/** + * Represents a column of an index in the database model. + * + * @author Alan Stewart + * @since 1.1 + */ +public class IndexColumn { + private String name; + private int size; + + IndexColumn(final String name) { + this.name = name; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof IndexColumn)) { + return false; + } + final IndexColumn other = (IndexColumn) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + public String getName() { + return name; + } + + public int getSize() { + return size; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + public void setName(final String name) { + this.name = name; + } + + public void setSize(final int size) { + this.size = size; + } + + @Override + public String toString() { + return String.format("IndexColumn [name=%s, size=%s]", name, size); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Reference.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Reference.java new file mode 100644 index 000000000..df85a62e0 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Reference.java @@ -0,0 +1,133 @@ +package org.springframework.roo.addon.dbre.model; + +import org.apache.commons.lang3.Validate; + +/** + * Represents a reference between a column in the local table and a column in + * another table. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Reference { + + /** The foreign column. */ + private Column foreignColumn; + + /** The name of the foreign column. */ + private String foreignColumnName; + + private boolean insertableOrUpdatable = true; + + /** The local column. */ + private Column localColumn; + + /** The name of the local column. */ + private String localColumnName; + + /** + * Creates a new reference between the two given columns. + * + * @param localColumn The local column + * @param foreignColumn The remote column + */ + Reference(final Column localColumn, final Column foreignColumn) { + setLocalColumn(localColumn); + setForeignColumn(foreignColumn); + } + + /** + * Creates a new reference between the two given columns. + */ + Reference(final String localColumnName, final String foreignColumnName) { + Validate.notBlank(localColumnName, + "Foreign key reference local column name required"); + Validate.notBlank(foreignColumnName, + "Foreign key reference foreign column name required"); + this.localColumnName = localColumnName; + this.foreignColumnName = foreignColumnName; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Reference)) { + return false; + } + final Reference other = (Reference) obj; + if (foreignColumnName == null) { + if (other.foreignColumnName != null) { + return false; + } + } + else if (!foreignColumnName.equals(other.foreignColumnName)) { + return false; + } + if (localColumnName == null) { + if (other.localColumnName != null) { + return false; + } + } + else if (!localColumnName.equals(other.localColumnName)) { + return false; + } + return true; + } + + public Column getForeignColumn() { + return foreignColumn; + } + + public String getForeignColumnName() { + return foreignColumnName; + } + + public Column getLocalColumn() { + return localColumn; + } + + public String getLocalColumnName() { + return localColumnName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime + * result + + (foreignColumnName == null ? 0 : foreignColumnName.hashCode()); + result = prime * result + + (localColumnName == null ? 0 : localColumnName.hashCode()); + return result; + } + + public boolean isInsertableOrUpdatable() { + return insertableOrUpdatable; + } + + public void setForeignColumn(final Column foreignColumn) { + this.foreignColumn = foreignColumn; + } + + public void setInsertableOrUpdatable(final boolean insertableOrUpdatable) { + this.insertableOrUpdatable = insertableOrUpdatable; + } + + public void setLocalColumn(final Column localColumn) { + this.localColumn = localColumn; + } + + @Override + public String toString() { + return String + .format("Reference [localColumnName=%s, foreignColumnName=%s, insertableOrUpdatable=%s]", + localColumnName, foreignColumnName, + insertableOrUpdatable); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Schema.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Schema.java new file mode 100644 index 000000000..10f2c95cf --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Schema.java @@ -0,0 +1,58 @@ +package org.springframework.roo.addon.dbre.model; + +import org.apache.commons.lang3.StringUtils; + +/** + * Represents a schema in the database model. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Schema { + private final String name; + + public Schema(final String name) { + this.name = StringUtils.defaultIfEmpty(name, + DbreModelService.NO_SCHEMA_REQUIRED); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Schema)) { + return false; + } + final Schema other = (Schema) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + name.hashCode(); + return result; + } + + @Override + public String toString() { + return name; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/SchemaContentHandler.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/SchemaContentHandler.java new file mode 100644 index 000000000..5fa1164e2 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/SchemaContentHandler.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.dbre.model; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * {@link ContentHandler} for finding the schema attribute of the + * database element in the DBRE XML file and creating a + * {@link Schema} object. + * + * @author Alan Stewart + * @since 1.1 + */ +public class SchemaContentHandler extends DefaultHandler { + + private Schema schema; + + /** + * Constructor for no schema + */ + public SchemaContentHandler() { + } + + /** + * Returns the parsed schema + * + * @return null if not parsed yet + */ + public Schema getSchema() { + return schema; + } + + @Override + public void startElement(final String uri, final String localName, + final String qName, final Attributes attributes) + throws SAXException { + if (qName.equals("database")) { + schema = new Schema(attributes.getValue("name")); + } + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/SchemaIntrospector.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/SchemaIntrospector.java new file mode 100644 index 000000000..f8a051230 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/SchemaIntrospector.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.dbre.model; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Returns database schemas from a live database connection using JDBC. + * + * @author Alan Stewart + * @since 1.1.2 + */ +public class SchemaIntrospector extends AbstractIntrospector { + + public SchemaIntrospector(final Connection connection) throws SQLException { + super(connection); + } + + public Set getSchemas() throws SQLException { + final Set schemas = new LinkedHashSet(); + + final ResultSet rs = databaseMetaData.getSchemas(); + try { + while (rs.next()) { + schemas.add(new Schema(rs.getString("TABLE_SCHEM"))); + } + } + finally { + rs.close(); + } + + return schemas; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Table.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Table.java new file mode 100644 index 000000000..adf733376 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/Table.java @@ -0,0 +1,262 @@ +package org.springframework.roo.addon.dbre.model; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * Represents a table in the database model. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Table { + private String catalog; + private final Set columns = new LinkedHashSet(); + private String description; + private final Set exportedKeys = new LinkedHashSet(); + private final Set importedKeys = new LinkedHashSet(); + private boolean includeNonPortableAttributes; + private boolean disableVersionFields; + private boolean disableGeneratedIdentifiers; + private final Set indices = new LinkedHashSet(); + private boolean joinTable; + private final String name; + private final Schema schema; + + Table(final String name, final Schema schema) { + Validate.notBlank(name, "Table name required"); + Validate.notNull(schema, "Table schema required"); + this.name = name; + this.schema = schema; + } + + public boolean addColumn(final Column column) { + Validate.notNull(column, "Column required"); + return columns.add(column); + } + + public boolean addExportedKey(final ForeignKey exportedKey) { + Validate.notNull(exportedKey, "Exported key required"); + return exportedKeys.add(exportedKey); + } + + public boolean addImportedKey(final ForeignKey foreignKey) { + Validate.notNull(foreignKey, "Foreign key required"); + return importedKeys.add(foreignKey); + } + + public boolean addIndex(final Index index) { + Validate.notNull(index, "Index required"); + return indices.add(index); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Table)) { + return false; + } + final Table other = (Table) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equalsIgnoreCase(other.name)) { + return false; + } + if (schema == null) { + if (other.schema != null) { + return false; + } + } + else if (!schema.equals(other.schema)) { + return false; + } + return true; + } + + public Column findColumn(final String name) { + for (final Column column : columns) { + if (column.getName().equalsIgnoreCase(name)) { + return column; + } + } + return null; + } + + public ForeignKey findImportedKeyByLocalColumnName( + final String localColumnName) { + for (final ForeignKey foreignKey : importedKeys) { + for (final Reference reference : foreignKey.getReferences()) { + if (reference.getLocalColumnName().equalsIgnoreCase( + localColumnName)) { + return foreignKey; + } + } + } + return null; + } + + public String getCatalog() { + return StringUtils.trimToNull(catalog); + } + + public int getColumnCount() { + return columns.size(); + } + + public Set getColumns() { + return columns; + } + + public String getDescription() { + return description; + } + + public int getExportedKeyCountByForeignTableName( + final String foreignTableName) { + int count = 0; + for (final ForeignKey exportedKey : exportedKeys) { + if (exportedKey.getForeignTableName().equalsIgnoreCase( + foreignTableName)) { + count++; + } + } + return count; + } + + public Set getExportedKeys() { + return exportedKeys; + } + + public String getFullyQualifiedTableName() { + return DbreModelService.NO_SCHEMA_REQUIRED.equals(schema.getName()) ? name + : schema.getName() + "." + name; + } + + public ForeignKey getImportedKey(final String name) { + for (final ForeignKey foreignKey : importedKeys) { + Validate.notBlank(foreignKey.getName(), "Foreign key name required"); + if (foreignKey.getName().equalsIgnoreCase(name)) { + return foreignKey; + } + } + return null; + } + + public int getImportedKeyCount() { + return importedKeys.size(); + } + + public int getImportedKeyCountByForeignTableName( + final String foreignTableName) { + int count = 0; + for (final ForeignKey foreignKey : importedKeys) { + if (foreignKey.getForeignTableName().equalsIgnoreCase( + foreignTableName)) { + count++; + } + } + return count; + } + + public Set getImportedKeys() { + return importedKeys; + } + + public Set getIndices() { + return indices; + } + + public String getName() { + return name; + } + + public int getPrimaryKeyCount() { + return getPrimaryKeys().size(); + } + + public Set getPrimaryKeys() { + final Set primaryKeys = new LinkedHashSet(); + for (final Column column : columns) { + if (column.isPrimaryKey()) { + primaryKeys.add(column); + } + } + return primaryKeys; + } + + public Schema getSchema() { + return schema; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + name.hashCode(); + result = prime * result + schema.hashCode(); + return result; + } + + public boolean isIncludeNonPortableAttributes() { + return includeNonPortableAttributes; + } + + public boolean isDisableVersionFields() { + return disableVersionFields; + } + + public boolean isDisableGeneratedIdentifiers() { + return disableGeneratedIdentifiers; + } + + public boolean isJoinTable() { + return joinTable; + } + + public void setCatalog(final String catalog) { + this.catalog = catalog; + } + + public void setDescription(final String description) { + this.description = description; + } + + public void setIncludeNonPortableAttributes( + final boolean includeNonPortableAttributes) { + this.includeNonPortableAttributes = includeNonPortableAttributes; + } + + public void setDisableVersionFields(final boolean disableVersionFields) { + this.disableVersionFields = disableVersionFields; + } + + public void setDisableGeneratedIdentifiers( + final boolean disableGeneratedIdentifiers) { + this.disableGeneratedIdentifiers = disableGeneratedIdentifiers; + } + + public void setJoinTable(final boolean joinTable) { + this.joinTable = joinTable; + } + + @Override + public String toString() { + return String + .format("Table [name=%s, schema=%s, catalog=%s, description=%s, columns=%s, importedKeys=%s, exportedKeys=%s, indices=%s, includeNonPortableAttributes=%s, disableVersionFields=%s, disableGeneratedIdentifiers=%s]", + name, schema.getName(), catalog, description, columns, + importedKeys, exportedKeys, indices, + includeNonPortableAttributes, disableVersionFields, + disableGeneratedIdentifiers); + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/TableType.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/TableType.java new file mode 100644 index 000000000..9b09134fe --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/TableType.java @@ -0,0 +1,20 @@ +package org.springframework.roo.addon.dbre.model; + +/** + * SQL table types. + * + * @author Alan Stewart + * @since 1.1 + */ +public enum TableType { + ALIAS, SYNONYM, TABLE, UNKNOWN, VIEW; + + public static TableType getTableType(final String typeName) { + try { + return TableType.valueOf(typeName); + } + catch (final IllegalArgumentException e) { + return UNKNOWN; + } + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/AbstractDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/AbstractDialect.java new file mode 100644 index 000000000..541b6787b --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/AbstractDialect.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +/** + * Abstract base class for database {@link Dialect}s. + * + * @author Alan Stewart + * @since 1.1 + */ +public abstract class AbstractDialect { + + public AbstractDialect() { + super(); + } + + public boolean supportsSequences() { + return true; + } +} \ No newline at end of file diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DB2400Dialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DB2400Dialect.java new file mode 100644 index 000000000..ca4da0f6c --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DB2400Dialect.java @@ -0,0 +1,10 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +/** + * An SQL dialect for the DB2/400 database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class DB2400Dialect extends DB2Dialect { +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DB2Dialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DB2Dialect.java new file mode 100644 index 000000000..a4d0b2d26 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DB2Dialect.java @@ -0,0 +1,19 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the DB2 database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class DB2Dialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + Validate.notNull(schema, "Schema required"); + return "SELELCT SEQNAME FROM SYSIBM.SYSSEQUENCES WHRE SEQSCHEMA = '" + + schema.getName() + "'"; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DerbyDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DerbyDialect.java new file mode 100644 index 000000000..bc01250d2 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/DerbyDialect.java @@ -0,0 +1,19 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the Derby database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class DerbyDialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + Validate.notNull(schema, "Schema required"); + return "SELECT SEQUENCENAME FROM SYS.SYSSEQUENCES WHERE SEQUENCENAME = '" + + schema.getName() + "'"; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/Dialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/Dialect.java new file mode 100644 index 000000000..1a9d2f301 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/Dialect.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * Represents a dialect of SQL implemented by a particular RDBMS. + *

    + * Support for querying sequences is only provided at this stage. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface Dialect { + + String getQuerySequencesString(Schema schema) throws RuntimeException; + + boolean supportsSequences(); +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/H2Dialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/H2Dialect.java new file mode 100644 index 000000000..105291532 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/H2Dialect.java @@ -0,0 +1,16 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the H2 database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class H2Dialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + return "select sequence_name from system_sequences"; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/HSQLDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/HSQLDialect.java new file mode 100644 index 000000000..9342f54a7 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/HSQLDialect.java @@ -0,0 +1,16 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the HSQLDB database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class HSQLDialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + return "select sequence_name from system_sequences"; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/MySQLDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/MySQLDialect.java new file mode 100644 index 000000000..7a19b0993 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/MySQLDialect.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the MySQL database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class MySQLDialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + throw new UnsupportedOperationException( + "MySQL does not support sequences"); + } + + @Override + public boolean supportsSequences() { + return false; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/OracleDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/OracleDialect.java new file mode 100644 index 000000000..7c6d7b567 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/OracleDialect.java @@ -0,0 +1,19 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the Oracle 10 and 11 databases. + * + * @author Alan Stewart + * @since 1.1 + */ +public class OracleDialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + Validate.notNull(schema, "Schema required"); + return "SELECT SEQUENCE_NAME FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '" + + schema.getName() + "'"; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/PostgreSQLDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/PostgreSQLDialect.java new file mode 100644 index 000000000..00ef76cf5 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/PostgreSQLDialect.java @@ -0,0 +1,19 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the PostgreSQL database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class PostgreSQLDialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + Validate.notNull(schema, "Schema required"); + return "SELECT RELNAME FROM PG_CLASS WHERE RELKIND = 'S' AND RELNAMESPACE IN (SELECT OID FROM PG_NAMESPACE WHERE NSPNAME = '" + + schema.getName() + "')"; + } +} diff --git a/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/SybaseDialect.java b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/SybaseDialect.java new file mode 100644 index 000000000..1af957647 --- /dev/null +++ b/addon-dbre/src/main/java/org/springframework/roo/addon/dbre/model/dialect/SybaseDialect.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.dbre.model.dialect; + +import org.springframework.roo.addon.dbre.model.Schema; + +/** + * An SQL dialect for the MySQL database. + * + * @author Alan Stewart + * @since 1.1 + */ +public class SybaseDialect extends AbstractDialect implements Dialect { + + public String getQuerySequencesString(final Schema schema) { + throw new UnsupportedOperationException( + "Sybase does not support sequences"); + } + + @Override + public boolean supportsSequences() { + return false; + } +} diff --git a/addon-dbre/src/test/java/org/springframework/roo/addon/dbre/DbManagedAnnotationValuesTest.java b/addon-dbre/src/test/java/org/springframework/roo/addon/dbre/DbManagedAnnotationValuesTest.java new file mode 100644 index 000000000..0d83cb4cc --- /dev/null +++ b/addon-dbre/src/test/java/org/springframework/roo/addon/dbre/DbManagedAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.dbre; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link DbManagedAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DbManagedAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooDbManaged.class; + } + + @Override + protected Class getValuesClass() { + return DbManagedAnnotationValues.class; + } +} diff --git a/addon-dbre/src/test/java/org/springframework/roo/addon/dbre/DbreTypeUtilsTest.java b/addon-dbre/src/test/java/org/springframework/roo/addon/dbre/DbreTypeUtilsTest.java new file mode 100644 index 000000000..fa75a038c --- /dev/null +++ b/addon-dbre/src/test/java/org/springframework/roo/addon/dbre/DbreTypeUtilsTest.java @@ -0,0 +1,26 @@ +package org.springframework.roo.addon.dbre; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * JUnit tests for {@link DbreTypeUtils}. + * + * @author Alan Stewart + * @since 1.2.0. + */ +public class DbreTypeUtilsTest { + + @Test + public void testSuggestPackageName() { + String tableName = "table2"; + assertEquals("table2", DbreTypeUtils.suggestPackageName(tableName)); + + tableName = "1Roo-2424"; + assertEquals("p1roo2424", DbreTypeUtils.suggestPackageName(tableName)); + + tableName = "/-roo_2425_p"; + assertEquals("roo_2425_p", DbreTypeUtils.suggestPackageName(tableName)); + } +} diff --git a/addon-dod/pom.xml b/addon-dod/pom.xml new file mode 100644 index 000000000..1965ccde1 --- /dev/null +++ b/addon-dod/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.dod + bundle + Spring Roo - Addon - Test Data On Demand + Support for the automatic creation of sample data used for integration tests. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandAnnotationValues.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandAnnotationValues.java new file mode 100644 index 000000000..3d20b85e2 --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandAnnotationValues.java @@ -0,0 +1,35 @@ +package org.springframework.roo.addon.dod; + +import static org.springframework.roo.model.RooJavaType.ROO_DATA_ON_DEMAND; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; + +/** + * Represents a parsed {@link RooDataOnDemand} annotation. + * + * @author Ben Alex + * @since 1.0 + */ +public class DataOnDemandAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private JavaType entity; + @AutoPopulate private int quantity = 10; + + public DataOnDemandAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, ROO_DATA_ON_DEMAND); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public JavaType getEntity() { + return entity; + } + + public int getQuantity() { + return quantity; + } +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandCommands.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandCommands.java new file mode 100644 index 000000000..a36d41862 --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandCommands.java @@ -0,0 +1,55 @@ +package org.springframework.roo.addon.dod; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.ReservedWords; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Shell commands for creating data-on-demand (DoD) classes. + * + * @author Alan Stewart + * @since 1.1.3 + */ +@Component +@Service +public class DataOnDemandCommands implements CommandMarker { + + @Reference private DataOnDemandOperations dataOnDemandOperations; + + @CliAvailabilityIndicator({ "dod" }) + public boolean isDataOnDemandAvailable() { + return dataOnDemandOperations.isDataOnDemandInstallationPossible(); + } + + @CliCommand(value = "dod", help = "Creates a new data on demand for the specified entity") + public void newDod( + @CliOption(key = "entity", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The entity which this data on demand class will create and modify as required") final JavaType entity, + @CliOption(key = "class", mandatory = false, help = "The class which will be created to hold this data on demand provider (defaults to the entity name + 'DataOnDemand')") JavaType clazz, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(entity); + } + + Validate.isTrue( + BeanInfoUtils.isEntityReasonablyNamed(entity), + "Cannot create data on demand for an entity named 'Test' or 'TestCase' under any circumstances"); + + if (clazz == null) { + clazz = new JavaType(entity.getFullyQualifiedTypeName() + + "DataOnDemand"); + } + + dataOnDemandOperations.newDod(entity, clazz); + } +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadata.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadata.java new file mode 100644 index 000000000..8fa106cff --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadata.java @@ -0,0 +1,1722 @@ +package org.springframework.roo.addon.dod; + +import static org.springframework.roo.model.HibernateJavaType.VALIDATOR_CONSTRAINTS_EMAIL; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.BIG_INTEGER; +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JdkJavaType.GREGORIAN_CALENDAR; +import static org.springframework.roo.model.JdkJavaType.ITERATOR; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.RANDOM; +import static org.springframework.roo.model.JdkJavaType.SECURE_RANDOM; +import static org.springframework.roo.model.JdkJavaType.TIMESTAMP; +import static org.springframework.roo.model.JpaJavaType.JOIN_COLUMN; +import static org.springframework.roo.model.Jsr303JavaType.CONSTRAINT_VIOLATION; +import static org.springframework.roo.model.Jsr303JavaType.CONSTRAINT_VIOLATION_EXCEPTION; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MAX; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MIN; +import static org.springframework.roo.model.Jsr303JavaType.DIGITS; +import static org.springframework.roo.model.Jsr303JavaType.FUTURE; +import static org.springframework.roo.model.Jsr303JavaType.MAX; +import static org.springframework.roo.model.Jsr303JavaType.MIN; +import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; +import static org.springframework.roo.model.Jsr303JavaType.PAST; +import static org.springframework.roo.model.Jsr303JavaType.SIZE; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; +import static org.springframework.roo.model.SpringJavaType.COMPONENT; + +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooDataOnDemand}. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @author Greg Turnquist + * @author Andrew Swan + * @since 1.0 + */ +public class DataOnDemandMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String INDEX_VAR = "index"; + private static final JavaSymbolName INDEX_SYMBOL = new JavaSymbolName( + INDEX_VAR); + private static final JavaSymbolName MAX_SYMBOL = new JavaSymbolName("max"); + private static final JavaSymbolName MIN_SYMBOL = new JavaSymbolName("min"); + private static final String OBJ_VAR = "obj"; + private static final JavaSymbolName OBJ_SYMBOL = new JavaSymbolName(OBJ_VAR); + private static final String PROVIDES_TYPE_STRING = DataOnDemandMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final JavaSymbolName VALUE = new JavaSymbolName("value"); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private JavaSymbolName dataFieldName; + private final Map> embeddedFieldInitializers = new LinkedHashMap>(); + private List embeddedHolders; + private EmbeddedIdHolder embeddedIdHolder; + private JavaType entity; + private final Map fieldInitializers = new LinkedHashMap(); + private MemberTypeAdditions findMethod; + private MethodMetadata identifierAccessor; + private JavaType identifierType; + private MethodMetadata modifyMethod; + private MethodMetadata newTransientEntityMethod; + private MethodMetadata randomPersistentEntityMethod; + private final List requiredDataOnDemandCollaborators = new ArrayList(); + private JavaSymbolName rndFieldName; + private MethodMetadata specificPersistentEntityMethod; + + /** + * Constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalTypeMetadata + * @param annotationValues + * @param identifierAccessor + * @param findMethod + * @param findEntriesMethod + * @param persistMethod + * @param flushMethod + * @param locatedFields + * @param entity + * @param embeddedIdHolder + * @param embeddedHolders + */ + public DataOnDemandMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final DataOnDemandAnnotationValues annotationValues, + final MethodMetadata identifierAccessor, + final MemberTypeAdditions findMethod, + final MemberTypeAdditions findEntriesMethod, + final MemberTypeAdditions persistMethod, + final MemberTypeAdditions flushMethod, + final Map locatedFields, + final JavaType identifierType, + final EmbeddedIdHolder embeddedIdHolder, + final List embeddedHolders) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(identifierAccessor, + "Identifier accessor method required"); + Validate.notNull(locatedFields, "Located fields map required"); + Validate.notNull(embeddedHolders, "Embedded holders list required"); + + if (!isValid()) { + return; + } + + if (findEntriesMethod == null || persistMethod == null + || findMethod == null) { + valid = false; + return; + } + + this.embeddedIdHolder = embeddedIdHolder; + this.embeddedHolders = embeddedHolders; + this.identifierAccessor = identifierAccessor; + this.findMethod = findMethod; + this.identifierType = identifierType; + entity = annotationValues.getEntity(); + + // Calculate and store field initializers + for (final Map.Entry entry : locatedFields + .entrySet()) { + final FieldMetadata field = entry.getKey(); + final String initializer = getFieldInitializer(field, + entry.getValue()); + if (!StringUtils.isBlank(initializer)) { + fieldInitializers.put(field, initializer); + } + } + + for (final EmbeddedHolder embeddedHolder : embeddedHolders) { + final Map initializers = new LinkedHashMap(); + for (final FieldMetadata field : embeddedHolder.getFields()) { + initializers.put(field, getFieldInitializer(field, null)); + } + embeddedFieldInitializers.put(embeddedHolder.getEmbeddedField(), + initializers); + } + + builder.addAnnotation(getComponentAnnotation()); + builder.addField(getRndField()); + builder.addField(getDataField()); + + addCollaboratingDoDFieldsToBuilder(); + setNewTransientEntityMethod(); + + builder.addMethod(getEmbeddedIdMutatorMethod()); + + for (final EmbeddedHolder embeddedHolder : embeddedHolders) { + builder.addMethod(getEmbeddedClassMutatorMethod(embeddedHolder)); + addEmbeddedClassFieldMutatorMethodsToBuilder(embeddedHolder); + } + + for (final MethodMetadataBuilder fieldInitializerMethod : getFieldMutatorMethods()) { + builder.addMethod(fieldInitializerMethod); + } + + setSpecificPersistentEntityMethod(); + setRandomPersistentEntityMethod(); + setModifyMethod(); + builder.addMethod(getInitMethod(annotationValues.getQuantity(), + findEntriesMethod, persistMethod, flushMethod)); + + itdTypeDetails = builder.build(); + } + + private void addCollaboratingDoDFieldsToBuilder() { + final Set fields = new LinkedHashSet(); + for (final JavaType entityNeedingCollaborator : requiredDataOnDemandCollaborators) { + final JavaType collaboratorType = getCollaboratingType(entityNeedingCollaborator); + final String collaboratingFieldName = getCollaboratingFieldName( + entityNeedingCollaborator).getSymbolName(); + + final JavaSymbolName fieldSymbolName = new JavaSymbolName( + collaboratingFieldName); + final FieldMetadata candidate = governorTypeDetails + .getField(fieldSymbolName); + if (candidate != null) { + // We really expect the field to be correct if we're going to + // rely on it + Validate.isTrue( + candidate.getFieldType().equals(collaboratorType), + "Field '%s' on '%s' must be of type '%s'", + collaboratingFieldName, + destination.getFullyQualifiedTypeName(), + collaboratorType.getFullyQualifiedTypeName()); + Validate.isTrue(Modifier.isPrivate(candidate.getModifier()), + "Field '%s' on '%s' must be private", + collaboratingFieldName, + destination.getFullyQualifiedTypeName()); + Validate.notNull( + MemberFindingUtils.getAnnotationOfType( + candidate.getAnnotations(), AUTOWIRED), + "Field '%s' on '%s' must be @Autowired", + collaboratingFieldName, + destination.getFullyQualifiedTypeName()); + // It's ok, so we can move onto the new field + continue; + } + + // Create field and add it to the ITD, if it hasn't already been + if (!fields.contains(fieldSymbolName)) { + // Must make the field + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(AUTOWIRED)); + builder.addField(new FieldMetadataBuilder(getId(), 0, + annotations, fieldSymbolName, collaboratorType)); + fields.add(fieldSymbolName); + } + } + } + + private void addEmbeddedClassFieldMutatorMethodsToBuilder( + final EmbeddedHolder embeddedHolder) { + final JavaType embeddedFieldType = embeddedHolder.getEmbeddedField() + .getFieldType(); + final JavaType[] parameterTypes = { embeddedFieldType, + JavaType.INT_PRIMITIVE }; + final List parameterNames = Arrays.asList(OBJ_SYMBOL, + INDEX_SYMBOL); + + for (final FieldMetadata field : embeddedHolder.getFields()) { + final String initializer = getFieldInitializer(field, null); + final JavaSymbolName fieldMutatorMethodName = BeanInfoUtils + .getMutatorMethodName(field.getFieldName()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.append(getFieldValidationBody(field, initializer, + fieldMutatorMethodName, false)); + + final JavaSymbolName embeddedClassMethodName = getEmbeddedFieldMutatorMethodName( + embeddedHolder.getEmbeddedField().getFieldName(), + field.getFieldName()); + if (governorHasMethod(embeddedClassMethodName, parameterTypes)) { + // Method found in governor so do not create method in ITD + continue; + } + + builder.addMethod(new MethodMetadataBuilder(getId(), + Modifier.PUBLIC, embeddedClassMethodName, + JavaType.VOID_PRIMITIVE, AnnotatedJavaType + .convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder)); + } + } + + private JavaSymbolName getCollaboratingFieldName(final JavaType entity) { + return new JavaSymbolName( + StringUtils.uncapitalize(getCollaboratingType(entity) + .getSimpleTypeName())); + } + + private JavaType getCollaboratingType(final JavaType entity) { + return new JavaType(entity.getFullyQualifiedTypeName() + "DataOnDemand"); + } + + private String getColumnPrecisionAndScaleBody(final FieldMetadata field, + final Map values, final String suffix) { + if (values == null || !values.containsKey("precision")) { + return InvocableMemberBodyBuilder.getInstance().getOutput(); + } + + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType fieldType = field.getFieldType(); + + Integer precision = (Integer) values.get("precision"); + Integer scale = (Integer) values.get("scale"); + if (precision != null && scale != null && precision < scale) { + scale = 0; + } + + final BigDecimal maxValue; + if (scale == null || scale == 0) { + maxValue = new BigDecimal(StringUtils.rightPad("9", precision, '9')); + } else { + maxValue = new BigDecimal(StringUtils.rightPad("9", + precision - scale, '9') + + "." + + StringUtils.rightPad("9", scale, '9')); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + if (fieldType.equals(BIG_DECIMAL)) { + bodyBuilder.appendFormalLine("if (" + fieldName + ".compareTo(new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + maxValue + + "\")) == 1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + maxValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " > " + + maxValue.doubleValue() + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + + maxValue.doubleValue() + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + return bodyBuilder.getOutput(); + } + + /** + * Adds the @org.springframework.stereotype.Component annotation to the + * type, unless it already exists. + * + * @return the annotation is already exists or will be created, or null if + * it will not be created (required) + */ + public AnnotationMetadata getComponentAnnotation() { + if (governorTypeDetails.getAnnotation(COMPONENT) != null) { + return null; + } + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + COMPONENT); + return annotationBuilder.build(); + } + + /** + * @return the "data" field to use, which is either provided by the user or + * produced on demand (never returns null) + */ + private FieldMetadataBuilder getDataField() { + final List parameterTypes = Arrays.asList(entity); + final JavaType listType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + parameterTypes); + + int index = -1; + while (true) { + // Compute the required field name + index++; + + // The type parameters to be used by the field type + final JavaSymbolName fieldName = new JavaSymbolName("data" + + StringUtils.repeat("_", index)); + dataFieldName = fieldName; + final FieldMetadata candidate = governorTypeDetails + .getField(fieldName); + if (candidate != null) { + // Verify if candidate is suitable + if (!Modifier.isPrivate(candidate.getModifier())) { + // Candidate is not private, so we might run into naming + // clashes if someone subclasses this (therefore go onto the + // next possible name) + continue; + } + + if (!candidate.getFieldType().equals(listType)) { + // Candidate isn't a java.util.List, so it isn't + // suitable + // The equals method also verifies type params are present + continue; + } + + // If we got this far, we found a valid candidate + // We don't check if there is a corresponding initializer, but + // we assume the user knows what they're doing and have made one + return new FieldMetadataBuilder(candidate); + } + + // Candidate not found, so let's create one + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, + new ArrayList(), fieldName, + listType); + } + } + + private JavaSymbolName getDataFieldName() { + return dataFieldName; + } + + private String getDecimalMinAndDecimalMaxBody(final FieldMetadata field, + final AnnotationMetadata decimalMinAnnotation, + final AnnotationMetadata decimalMaxAnnotation, final String suffix) { + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType fieldType = field.getFieldType(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + if (decimalMinAnnotation != null && decimalMaxAnnotation == null) { + final String minValue = (String) decimalMinAnnotation.getAttribute( + VALUE).getValue(); + + if (fieldType.equals(BIG_DECIMAL)) { + bodyBuilder.appendFormalLine("if (" + fieldName + + ".compareTo(new " + BIG_DECIMAL.getSimpleTypeName() + + "(\"" + minValue + "\")) == -1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + minValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " < " + + minValue + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + minValue + + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else if (decimalMinAnnotation == null && decimalMaxAnnotation != null) { + final String maxValue = (String) decimalMaxAnnotation.getAttribute( + VALUE).getValue(); + + if (fieldType.equals(BIG_DECIMAL)) { + bodyBuilder.appendFormalLine("if (" + fieldName + + ".compareTo(new " + BIG_DECIMAL.getSimpleTypeName() + + "(\"" + maxValue + "\")) == 1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + maxValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " > " + + maxValue + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + maxValue + + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else if (decimalMinAnnotation != null && decimalMaxAnnotation != null) { + final String minValue = (String) decimalMinAnnotation.getAttribute( + VALUE).getValue(); + final String maxValue = (String) decimalMaxAnnotation.getAttribute( + VALUE).getValue(); + Validate.isTrue( + Double.parseDouble(maxValue) >= Double + .parseDouble(minValue), + "The value of @DecimalMax must be greater or equal to the value of @DecimalMin for field %s", + fieldName); + + if (fieldType.equals(BIG_DECIMAL)) { + bodyBuilder.appendFormalLine("if (" + fieldName + + ".compareTo(new " + BIG_DECIMAL.getSimpleTypeName() + + "(\"" + minValue + "\")) == -1 || " + fieldName + + ".compareTo(new " + BIG_DECIMAL.getSimpleTypeName() + + "(\"" + maxValue + "\")) == 1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + maxValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " < " + + minValue + suffix + " || " + fieldName + " > " + + maxValue + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + maxValue + + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + + return bodyBuilder.getOutput(); + } + + private String getDigitsBody(final FieldMetadata field, + final AnnotationMetadata digitsAnnotation, final String suffix) { + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType fieldType = field.getFieldType(); + + final Integer integerValue = (Integer) digitsAnnotation.getAttribute( + new JavaSymbolName("integer")).getValue(); + final Integer fractionValue = (Integer) digitsAnnotation.getAttribute( + new JavaSymbolName("fraction")).getValue(); + final BigDecimal maxValue = new BigDecimal(StringUtils.rightPad("9", + integerValue, '9') + + "." + + StringUtils.rightPad("9", fractionValue, '9')); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + if (fieldType.equals(BIG_DECIMAL)) { + bodyBuilder.appendFormalLine("if (" + fieldName + ".compareTo(new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + maxValue + + "\")) == 1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_DECIMAL.getSimpleTypeName() + "(\"" + maxValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " > " + + maxValue.doubleValue() + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + + maxValue.doubleValue() + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + return bodyBuilder.getOutput(); + } + + private MethodMetadataBuilder getEmbeddedClassMutatorMethod( + final EmbeddedHolder embeddedHolder) { + final JavaSymbolName methodName = getEmbeddedFieldMutatorMethodName(embeddedHolder + .getEmbeddedField().getFieldName()); + final JavaType[] parameterTypes = { entity, JavaType.INT_PRIMITIVE }; + + // Locate user-defined method + if (governorHasMethod(methodName, parameterTypes)) { + // Method found in governor so do not create method in ITD + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + // Create constructor for embedded class + final JavaType embeddedFieldType = embeddedHolder.getEmbeddedField() + .getFieldType(); + builder.getImportRegistrationResolver().addImport(embeddedFieldType); + bodyBuilder.appendFormalLine(embeddedFieldType.getSimpleTypeName() + + " embeddedClass = new " + + embeddedFieldType.getSimpleTypeName() + "();"); + for (final FieldMetadata field : embeddedHolder.getFields()) { + bodyBuilder.appendFormalLine(getEmbeddedFieldMutatorMethodName( + embeddedHolder.getEmbeddedField().getFieldName(), + field.getFieldName()).getSymbolName() + + "(embeddedClass, " + INDEX_VAR + ");"); + } + bodyBuilder.appendFormalLine(OBJ_VAR + "." + + embeddedHolder.getEmbeddedMutatorMethodName() + + "(embeddedClass);"); + + final List parameterNames = Arrays.asList(OBJ_SYMBOL, + INDEX_SYMBOL); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private JavaSymbolName getEmbeddedFieldMutatorMethodName( + final JavaSymbolName fieldName) { + return BeanInfoUtils.getMutatorMethodName(fieldName); + } + + private JavaSymbolName getEmbeddedFieldMutatorMethodName( + final JavaSymbolName embeddedFieldName, + final JavaSymbolName fieldName) { + return getEmbeddedFieldMutatorMethodName(new JavaSymbolName( + embeddedFieldName.getSymbolName() + + StringUtils.capitalize(fieldName.getSymbolName()))); + } + + private MethodMetadataBuilder getEmbeddedIdMutatorMethod() { + if (!hasEmbeddedIdentifier()) { + return null; + } + + final JavaSymbolName embeddedIdMutator = embeddedIdHolder + .getEmbeddedIdMutator(); + final JavaSymbolName methodName = getEmbeddedIdMutatorMethodName(); + final JavaType[] parameterTypes = { entity, JavaType.INT_PRIMITIVE }; + + // Locate user-defined method + if (governorHasMethod(methodName, parameterTypes)) { + // Method found in governor so do not create method in ITD + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + // Create constructor for embedded id class + final JavaType embeddedIdFieldType = embeddedIdHolder + .getEmbeddedIdField().getFieldType(); + builder.getImportRegistrationResolver().addImport(embeddedIdFieldType); + + final StringBuilder sb = new StringBuilder(); + final List identifierFields = embeddedIdHolder + .getIdFields(); + for (int i = 0, n = identifierFields.size(); i < n; i++) { + if (i > 0) { + sb.append(", "); + } + final FieldMetadata field = identifierFields.get(i); + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType fieldType = field.getFieldType(); + builder.getImportRegistrationResolver().addImport(fieldType); + final String initializer = getFieldInitializer(field, null); + bodyBuilder.append(getFieldValidationBody(field, initializer, null, + true)); + sb.append(fieldName); + } + bodyBuilder.appendFormalLine(""); + bodyBuilder.appendFormalLine(embeddedIdFieldType.getSimpleTypeName() + + " embeddedIdClass = new " + + embeddedIdFieldType.getSimpleTypeName() + "(" + sb.toString() + + ");"); + bodyBuilder.appendFormalLine(OBJ_VAR + "." + embeddedIdMutator + + "(embeddedIdClass);"); + + final List parameterNames = Arrays.asList(OBJ_SYMBOL, + INDEX_SYMBOL); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private JavaSymbolName getEmbeddedIdMutatorMethodName() { + final List fieldNames = new ArrayList(); + for (final FieldMetadata field : fieldInitializers.keySet()) { + fieldNames.add(field.getFieldName()); + } + + int index = -1; + JavaSymbolName embeddedIdField; + while (true) { + // Compute the required field name + index++; + embeddedIdField = new JavaSymbolName("embeddedIdClass" + + StringUtils.repeat("_", index)); + if (!fieldNames.contains(embeddedIdField)) { + // Found a usable name + break; + } + } + return BeanInfoUtils.getMutatorMethodName(embeddedIdField); + } + + public JavaType getEntityType() { + return entity; + } + + private String getFieldInitializer(final FieldMetadata field, + final DataOnDemandMetadata collaboratingMetadata) { + final JavaType fieldType = field.getFieldType(); + final String fieldName = field.getFieldName().getSymbolName(); + String initializer = "null"; + final String fieldInitializer = field.getFieldInitializer(); + final Set fieldCustomDataKeys = field.getCustomData().keySet(); + + // Date fields included for DataNucleus ( + if (fieldType.equals(DATE)) { + if (MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), + PAST) != null) { + builder.getImportRegistrationResolver().addImport(DATE); + initializer = "new Date(new Date().getTime() - 10000000L)"; + } + else if (MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), FUTURE) != null) { + builder.getImportRegistrationResolver().addImport(DATE); + initializer = "new Date(new Date().getTime() + 10000000L)"; + } + else { + builder.getImportRegistrationResolver().addImports(CALENDAR, + GREGORIAN_CALENDAR); + initializer = "new GregorianCalendar(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DAY_OF_MONTH), Calendar.getInstance().get(Calendar.HOUR_OF_DAY), Calendar.getInstance().get(Calendar.MINUTE), Calendar.getInstance().get(Calendar.SECOND) + new Double(Math.random() * 1000).intValue()).getTime()"; + } + } + else if (fieldType.equals(CALENDAR)) { + builder.getImportRegistrationResolver().addImports(CALENDAR, + GREGORIAN_CALENDAR); + + final String calendarString = "new GregorianCalendar(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DAY_OF_MONTH)"; + if (MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), + PAST) != null) { + initializer = calendarString + " - 1)"; + } + else if (MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), FUTURE) != null) { + initializer = calendarString + " + 1)"; + } + else { + initializer = "Calendar.getInstance()"; + } + } + else if (fieldType.equals(TIMESTAMP)) { + builder.getImportRegistrationResolver().addImports(CALENDAR, + GREGORIAN_CALENDAR, TIMESTAMP); + initializer = "new Timestamp(new GregorianCalendar(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DAY_OF_MONTH), Calendar.getInstance().get(Calendar.HOUR_OF_DAY), Calendar.getInstance().get(Calendar.MINUTE), Calendar.getInstance().get(Calendar.SECOND) + new Double(Math.random() * 1000).intValue()).getTime().getTime())"; + } + else if (fieldType.equals(STRING)) { + if (fieldInitializer != null && fieldInitializer.contains("\"")) { + final int offset = fieldInitializer.indexOf("\""); + initializer = fieldInitializer.substring(offset + 1, + fieldInitializer.lastIndexOf("\"")); + } + else { + initializer = fieldName; + } + + if (MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), + VALIDATOR_CONSTRAINTS_EMAIL) != null + || fieldName.toLowerCase().contains("email")) { + initializer = "\"foo\" + " + INDEX_VAR + " + \"@bar.com\""; + } + else { + int maxLength = Integer.MAX_VALUE; + + // Check for @Size + final AnnotationMetadata sizeAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), SIZE); + if (sizeAnnotation != null) { + final AnnotationAttributeValue maxValue = sizeAnnotation + .getAttribute(MAX_SYMBOL); + if (maxValue != null) { + validateNumericAnnotationAttribute(fieldName, "@Size", + "max", maxValue.getValue()); + maxLength = ((Integer) maxValue.getValue()).intValue(); + } + final AnnotationAttributeValue minValue = sizeAnnotation + .getAttribute(MIN_SYMBOL); + if (minValue != null) { + validateNumericAnnotationAttribute(fieldName, "@Size", + "min", minValue.getValue()); + final int minLength = ((Integer) minValue.getValue()) + .intValue(); + Validate.isTrue( + maxLength >= minLength, + "@Size attribute 'max' must be greater than 'min' for field '%s' in %s", + fieldName, entity.getFullyQualifiedTypeName()); + if (initializer.length() + 2 < minLength) { + initializer = String + .format("%1$-" + (minLength - 2) + "s", + initializer).replace(' ', 'x'); + } + } + } + else { + if (field.getCustomData().keySet() + .contains(CustomDataKeys.COLUMN_FIELD)) { + @SuppressWarnings("unchecked") + final Map columnValues = (Map) field + .getCustomData().get( + CustomDataKeys.COLUMN_FIELD); + if (columnValues.keySet().contains("length")) { + final Object lengthValue = columnValues + .get("length"); + validateNumericAnnotationAttribute(fieldName, + "@Column", "length", lengthValue); + maxLength = ((Integer) lengthValue).intValue(); + } + } + } + + switch (maxLength) { + case 0: + initializer = "\"\""; + break; + case 1: + initializer = "String.valueOf(" + INDEX_VAR + ")"; + break; + case 2: + initializer = "\"" + initializer.charAt(0) + "\" + " + + INDEX_VAR; + break; + default: + if (initializer.length() + 2 > maxLength) { + initializer = "\"" + + initializer.substring(0, maxLength - 2) + + "_\" + " + INDEX_VAR; + } + else { + initializer = "\"" + initializer + "_\" + " + INDEX_VAR; + } + } + } + } + else if (fieldType.equals(new JavaType(STRING + .getFullyQualifiedTypeName(), 1, DataType.TYPE, null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "{ \"Y\", \"N\" }"); + } + else if (fieldType.equals(JavaType.BOOLEAN_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "Boolean.TRUE"); + } + else if (fieldType.equals(JavaType.BOOLEAN_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, "true"); + } + else if (fieldType.equals(JavaType.INT_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ")"); + } + else if (fieldType.equals(JavaType.INT_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + INDEX_VAR); + } + else if (fieldType + .equals(new JavaType(JavaType.INT_OBJECT + .getFullyQualifiedTypeName(), 1, DataType.PRIMITIVE, + null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, "{ " + + INDEX_VAR + ", " + INDEX_VAR + " }"); + } + else if (fieldType.equals(JavaType.DOUBLE_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").doubleValue()"); // Auto-boxed + } + else if (fieldType.equals(JavaType.DOUBLE_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").doubleValue()"); + } + else if (fieldType + .equals(new JavaType(JavaType.DOUBLE_OBJECT + .getFullyQualifiedTypeName(), 1, DataType.PRIMITIVE, + null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "{ new Integer(" + INDEX_VAR + + ").doubleValue(), new Integer(" + INDEX_VAR + + ").doubleValue() }"); + } + else if (fieldType.equals(JavaType.FLOAT_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").floatValue()"); // Auto-boxed + } + else if (fieldType.equals(JavaType.FLOAT_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").floatValue()"); + } + else if (fieldType + .equals(new JavaType(JavaType.FLOAT_OBJECT + .getFullyQualifiedTypeName(), 1, DataType.PRIMITIVE, + null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "{ new Integer(" + INDEX_VAR + + ").floatValue(), new Integer(" + INDEX_VAR + + ").floatValue() }"); + } + else if (fieldType.equals(JavaType.LONG_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").longValue()"); // Auto-boxed + } + else if (fieldType.equals(JavaType.LONG_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").longValue()"); + } + else if (fieldType + .equals(new JavaType(JavaType.LONG_OBJECT + .getFullyQualifiedTypeName(), 1, DataType.PRIMITIVE, + null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "{ new Integer(" + INDEX_VAR + + ").longValue(), new Integer(" + INDEX_VAR + + ").longValue() }"); + } + else if (fieldType.equals(JavaType.SHORT_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").shortValue()"); // Auto-boxed + } + else if (fieldType.equals(JavaType.SHORT_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Integer(" + INDEX_VAR + ").shortValue()"); + } + else if (fieldType + .equals(new JavaType(JavaType.SHORT_OBJECT + .getFullyQualifiedTypeName(), 1, DataType.PRIMITIVE, + null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "{ new Integer(" + INDEX_VAR + + ").shortValue(), new Integer(" + INDEX_VAR + + ").shortValue() }"); + } + else if (fieldType.equals(JavaType.CHAR_OBJECT)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "new Character('N')"); + } + else if (fieldType.equals(JavaType.CHAR_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, "'N'"); + } + else if (fieldType + .equals(new JavaType(JavaType.CHAR_OBJECT + .getFullyQualifiedTypeName(), 1, DataType.PRIMITIVE, + null, null))) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "{ 'Y', 'N' }"); + } + else if (fieldType.equals(BIG_DECIMAL)) { + builder.getImportRegistrationResolver().addImport(BIG_DECIMAL); + initializer = BIG_DECIMAL.getSimpleTypeName() + ".valueOf(" + + INDEX_VAR + ")"; + } + else if (fieldType.equals(BIG_INTEGER)) { + builder.getImportRegistrationResolver().addImport(BIG_INTEGER); + initializer = BIG_INTEGER.getSimpleTypeName() + ".valueOf(" + + INDEX_VAR + ")"; + } + else if (fieldType.equals(JavaType.BYTE_OBJECT)) { + initializer = "new Byte(" + + StringUtils.defaultIfEmpty(fieldInitializer, "\"1\"") + + ")"; + } + else if (fieldType.equals(JavaType.BYTE_PRIMITIVE)) { + initializer = "new Byte(" + + StringUtils.defaultIfEmpty(fieldInitializer, "\"1\"") + + ").byteValue()"; + } + else if (fieldType.equals(JavaType.BYTE_ARRAY_PRIMITIVE)) { + initializer = StringUtils.defaultIfEmpty(fieldInitializer, + "String.valueOf(" + INDEX_VAR + ").getBytes()"); + } + else if (fieldType.equals(entity)) { + // Avoid circular references (ROO-562) + initializer = OBJ_VAR; + } + else if (fieldCustomDataKeys.contains(CustomDataKeys.ENUMERATED_FIELD)) { + builder.getImportRegistrationResolver().addImport(fieldType); + initializer = fieldType.getSimpleTypeName() + + ".class.getEnumConstants()[0]"; + } + else if (collaboratingMetadata != null + && collaboratingMetadata.getEntityType() != null) { + requiredDataOnDemandCollaborators.add(fieldType); + initializer = getFieldInitializerForRelatedEntity(field, + collaboratingMetadata, fieldCustomDataKeys); + } + + return initializer; + } + + private String getFieldInitializerForRelatedEntity( + final FieldMetadata field, + final DataOnDemandMetadata collaboratingMetadata, + final Set fieldCustomDataKeys) { + // To avoid circular references, we don't try to set nullable fields + final boolean nullableField = field.getAnnotation(NOT_NULL) == null + && isNullableJoinColumn(field); + if (nullableField) { + return null; + } + final String collaboratingFieldName = getCollaboratingFieldName( + field.getFieldType()).getSymbolName(); + if (fieldCustomDataKeys.contains(CustomDataKeys.ONE_TO_ONE_FIELD)) { + // We try to keep the same ID (ROO-568) + return collaboratingFieldName + + "." + + collaboratingMetadata.getSpecificPersistentEntityMethod() + .getMethodName().getSymbolName() + "(" + INDEX_VAR + + ")"; + } + return collaboratingFieldName + + "." + + collaboratingMetadata.getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "()"; + } + + private boolean isNullableJoinColumn(final FieldMetadata field) { + final AnnotationMetadata joinColumnAnnotation = field + .getAnnotation(JOIN_COLUMN); + if (joinColumnAnnotation == null) { + return true; + } + final AnnotationAttributeValue nullableAttr = joinColumnAnnotation + .getAttribute(new JavaSymbolName("nullable")); + return nullableAttr == null || (Boolean) nullableAttr.getValue(); + } + + private List getFieldMutatorMethods() { + final List fieldMutatorMethods = new ArrayList(); + final List parameterNames = Arrays.asList(OBJ_SYMBOL, + INDEX_SYMBOL); + final JavaType[] parameterTypes = { entity, JavaType.INT_PRIMITIVE }; + + for (final Map.Entry entry : fieldInitializers + .entrySet()) { + final FieldMetadata field = entry.getKey(); + final JavaSymbolName mutatorName = BeanInfoUtils + .getMutatorMethodName(field.getFieldName()); + + // Locate user-defined method + if (governorHasMethod(mutatorName, parameterTypes)) { + // Method found in governor so do not create method in ITD + continue; + } + + // Method not on governor so need to create it + final String initializer = entry.getValue(); + if (!StringUtils.isBlank(initializer)) { + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.append(getFieldValidationBody(field, initializer, + mutatorName, false)); + + fieldMutatorMethods.add(new MethodMetadataBuilder(getId(), + Modifier.PUBLIC, mutatorName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder)); + } + } + + return fieldMutatorMethods; + } + + private String getFieldValidationBody(final FieldMetadata field, + final String initializer, final JavaSymbolName mutatorName, + final boolean isFieldOfEmbeddableType) { + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType fieldType = field.getFieldType(); + + String suffix = ""; + if (fieldType.equals(JavaType.LONG_OBJECT) + || fieldType.equals(JavaType.LONG_PRIMITIVE)) { + suffix = "L"; + } + else if (fieldType.equals(JavaType.FLOAT_OBJECT) + || fieldType.equals(JavaType.FLOAT_PRIMITIVE)) { + suffix = "F"; + } + else if (fieldType.equals(JavaType.DOUBLE_OBJECT) + || fieldType.equals(JavaType.DOUBLE_PRIMITIVE)) { + suffix = "D"; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(getTypeStr(fieldType) + " " + fieldName + + " = " + initializer + ";"); + + if (fieldType.equals(JavaType.STRING)) { + boolean isUnique = isFieldOfEmbeddableType; + @SuppressWarnings("unchecked") + final Map values = (Map) field + .getCustomData().get(CustomDataKeys.COLUMN_FIELD); + if (!isUnique && values != null && values.containsKey("unique")) { + isUnique = (Boolean) values.get("unique"); + } + + // Check for @Size or @Column with length attribute + final AnnotationMetadata sizeAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), SIZE); + if (sizeAnnotation != null + && sizeAnnotation.getAttribute(MAX_SYMBOL) != null) { + final Integer maxValue = (Integer) sizeAnnotation.getAttribute( + MAX_SYMBOL).getValue(); + bodyBuilder.appendFormalLine("if (" + fieldName + + ".length() > " + maxValue + ") {"); + bodyBuilder.indent(); + if (isUnique) { + bodyBuilder.appendFormalLine(fieldName + + " = new Random().nextInt(10) + " + fieldName + + ".substring(1, " + maxValue + ");"); + } + else { + bodyBuilder.appendFormalLine(fieldName + " = " + fieldName + + ".substring(0, " + maxValue + ");"); + } + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else if (sizeAnnotation == null && values != null) { + if (values.containsKey("length")) { + final Integer lengthValue = (Integer) values.get("length"); + bodyBuilder.appendFormalLine("if (" + fieldName + + ".length() > " + lengthValue + ") {"); + bodyBuilder.indent(); + if (isUnique) { + bodyBuilder.appendFormalLine(fieldName + + " = new Random().nextInt(10) + " + fieldName + + ".substring(1, " + lengthValue + ");"); + } + else { + bodyBuilder.appendFormalLine(fieldName + " = " + + fieldName + ".substring(0, " + lengthValue + + ");"); + } + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + } + } + else if (JdkJavaType.isDecimalType(fieldType)) { + // Check for @Digits, @DecimalMax, @DecimalMin + final AnnotationMetadata digitsAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), DIGITS); + final AnnotationMetadata decimalMinAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), DECIMAL_MIN); + final AnnotationMetadata decimalMaxAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), DECIMAL_MAX); + + if (digitsAnnotation != null) { + bodyBuilder.append(getDigitsBody(field, digitsAnnotation, + suffix)); + } + else if (decimalMinAnnotation != null + || decimalMaxAnnotation != null) { + bodyBuilder.append(getDecimalMinAndDecimalMaxBody(field, + decimalMinAnnotation, decimalMaxAnnotation, suffix)); + } + else if (field.getCustomData().keySet() + .contains(CustomDataKeys.COLUMN_FIELD)) { + @SuppressWarnings("unchecked") + final Map values = (Map) field + .getCustomData().get(CustomDataKeys.COLUMN_FIELD); + bodyBuilder.append(getColumnPrecisionAndScaleBody(field, + values, suffix)); + } + } + else if (JdkJavaType.isIntegerType(fieldType)) { + // Check for @Min and @Max + bodyBuilder.append(getMinAndMaxBody(field, suffix)); + } + + if (mutatorName != null) { + bodyBuilder.appendFormalLine(OBJ_VAR + "." + + mutatorName.getSymbolName() + "(" + fieldName + ");"); + } + + return bodyBuilder.getOutput(); + } + + /** + * Returns the DoD type's "void init()" method (existing or generated) + * + * @param findEntriesMethod (required) + * @param persistMethod (required) + * @param flushMethod (required) + * @return never null + */ + private MethodMetadataBuilder getInitMethod(final int quantity, + final MemberTypeAdditions findEntriesMethod, + final MemberTypeAdditions persistMethod, + final MemberTypeAdditions flushMethod) { + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName("init"); + final JavaType[] parameterTypes = {}; + final List parameterNames = Collections + . emptyList(); + final JavaType returnType = JavaType.VOID_PRIMITIVE; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return new MethodMetadataBuilder(userMethod); + } + + // Create the method body + builder.getImportRegistrationResolver().addImports(ARRAY_LIST, + ITERATOR, CONSTRAINT_VIOLATION_EXCEPTION, CONSTRAINT_VIOLATION); + + findEntriesMethod.copyAdditionsTo(builder, governorTypeDetails); + persistMethod.copyAdditionsTo(builder, governorTypeDetails); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String dataField = getDataFieldName().getSymbolName(); + bodyBuilder.appendFormalLine("int from = 0;"); + bodyBuilder.appendFormalLine("int to = " + quantity + ";"); + bodyBuilder.appendFormalLine(dataField + " = " + + findEntriesMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("if (" + dataField + " == null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("throw new IllegalStateException(\"Find entries implementation for '" + + entity.getSimpleTypeName() + + "' illegally returned null\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("if (!" + dataField + ".isEmpty()) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(""); + bodyBuilder.appendFormalLine(dataField + " = new ArrayList<" + + entity.getSimpleTypeName() + ">();"); + bodyBuilder.appendFormalLine("for (int i = 0; i < " + quantity + + "; i++) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(entity.getSimpleTypeName() + " " + OBJ_VAR + + " = " + + newTransientEntityMethod.getMethodName().getSymbolName() + + "(i);"); + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";"); + bodyBuilder.indentRemove(); + bodyBuilder + .appendFormalLine("} catch (final ConstraintViolationException e) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("final StringBuilder msg = new StringBuilder();"); + bodyBuilder + .appendFormalLine("for (Iterator> iter = e.getConstraintViolations().iterator(); iter.hasNext();) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("final ConstraintViolation cv = iter.next();"); + bodyBuilder + .appendFormalLine("msg.append(\"[\").append(cv.getRootBean().getClass().getName()).append(\".\").append(cv.getPropertyPath()).append(\": \").append(cv.getMessage()).append(\" (invalid value = \").append(cv.getInvalidValue()).append(\")\").append(\"]\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("throw new IllegalStateException(msg.toString(), e);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + if (flushMethod != null) { + bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";"); + flushMethod.copyAdditionsTo(builder, governorTypeDetails); + } + bodyBuilder.appendFormalLine(dataField + ".add(" + OBJ_VAR + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + // Create the method + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private String getMinAndMaxBody(final FieldMetadata field, + final String suffix) { + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType fieldType = field.getFieldType(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + final AnnotationMetadata minAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), MIN); + final AnnotationMetadata maxAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), MAX); + if (minAnnotation != null && maxAnnotation == null) { + final Number minValue = (Number) minAnnotation.getAttribute(VALUE) + .getValue(); + + if (fieldType.equals(BIG_INTEGER)) { + bodyBuilder.appendFormalLine("if (" + fieldName + + ".compareTo(new " + BIG_INTEGER.getSimpleTypeName() + + "(\"" + minValue + "\")) == -1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_INTEGER.getSimpleTypeName() + "(\"" + minValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " < " + + minValue + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + minValue + + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else if (minAnnotation == null && maxAnnotation != null) { + final Number maxValue = (Number) maxAnnotation.getAttribute(VALUE) + .getValue(); + + if (fieldType.equals(BIG_INTEGER)) { + bodyBuilder.appendFormalLine("if (" + fieldName + + ".compareTo(new " + BIG_INTEGER.getSimpleTypeName() + + "(\"" + maxValue + "\")) == 1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_INTEGER.getSimpleTypeName() + "(\"" + maxValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " > " + + maxValue + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + maxValue + + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else if (minAnnotation != null && maxAnnotation != null) { + final Number minValue = (Number) minAnnotation.getAttribute(VALUE) + .getValue(); + final Number maxValue = (Number) maxAnnotation.getAttribute(VALUE) + .getValue(); + Validate.isTrue( + maxValue.longValue() >= minValue.longValue(), + "The value of @Max must be greater or equal to the value of @Min for field %s", + fieldName); + + if (fieldType.equals(BIG_INTEGER)) { + bodyBuilder.appendFormalLine("if (" + fieldName + + ".compareTo(new " + BIG_INTEGER.getSimpleTypeName() + + "(\"" + minValue + "\")) == -1 || " + fieldName + + ".compareTo(new " + BIG_INTEGER.getSimpleTypeName() + + "(\"" + maxValue + "\")) == 1) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = new " + + BIG_INTEGER.getSimpleTypeName() + "(\"" + maxValue + + "\");"); + } + else { + bodyBuilder.appendFormalLine("if (" + fieldName + " < " + + minValue + suffix + " || " + fieldName + " > " + + maxValue + suffix + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = " + maxValue + + suffix + ";"); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + + return bodyBuilder.getOutput(); + } + + /** + * @return the "modifyEntity(Entity):boolean" method (never returns null) + */ + public MethodMetadata getModifyMethod() { + return modifyMethod; + } + + /** + * @return the "getNewTransientEntity(int index):Entity" method (never + * returns null) + */ + public MethodMetadata getNewTransientEntityMethod() { + return newTransientEntityMethod; + } + + /** + * @return the "getRandomEntity():Entity" method (never returns null) + */ + public MethodMetadata getRandomPersistentEntityMethod() { + return randomPersistentEntityMethod; + } + + private FieldMetadataBuilder getRndField() { + int index = -1; + while (true) { + // Compute the required field name + index++; + final JavaSymbolName fieldName = new JavaSymbolName("rnd" + + StringUtils.repeat("_", index)); + rndFieldName = fieldName; + final FieldMetadata candidate = governorTypeDetails + .getField(fieldName); + if (candidate != null) { + // Verify if candidate is suitable + if (!Modifier.isPrivate(candidate.getModifier())) { + // Candidate is not private, so we might run into naming + // clashes if someone subclasses this (therefore go onto the + // next possible name) + continue; + } + if (!candidate.getFieldType().equals(RANDOM)) { + // Candidate isn't a java.util.Random, so it isn't suitable + continue; + } + // If we got this far, we found a valid candidate + // We don't check if there is a corresponding initializer, but + // we assume the user knows what they're doing and have made one + return new FieldMetadataBuilder(candidate); + } + + // Candidate not found, so let's create one + builder.getImportRegistrationResolver().addImports(RANDOM, + SECURE_RANDOM); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId()); + fieldBuilder.setModifier(Modifier.PRIVATE); + fieldBuilder.setFieldName(fieldName); + fieldBuilder.setFieldType(RANDOM); + fieldBuilder.setFieldInitializer("new SecureRandom()"); + return fieldBuilder; + } + } + + /** + * @return the "rnd" field to use, which is either provided by the user or + * produced on demand (never returns null) + */ + private JavaSymbolName getRndFieldName() { + return rndFieldName; + } + + /** + * @return the "getSpecificEntity(int):Entity" method (never returns null) + */ + private MethodMetadata getSpecificPersistentEntityMethod() { + return specificPersistentEntityMethod; + } + + private String getTypeStr(final JavaType fieldType) { + builder.getImportRegistrationResolver().addImport(fieldType); + + final String arrayStr = fieldType.isArray() ? "[]" : ""; + String typeStr = fieldType.getSimpleTypeName(); + + if (fieldType.getFullyQualifiedTypeName().equals( + JavaType.FLOAT_PRIMITIVE.getFullyQualifiedTypeName()) + && fieldType.isPrimitive()) { + typeStr = "float" + arrayStr; + } + else if (fieldType.getFullyQualifiedTypeName().equals( + JavaType.DOUBLE_PRIMITIVE.getFullyQualifiedTypeName()) + && fieldType.isPrimitive()) { + typeStr = "double" + arrayStr; + } + else if (fieldType.getFullyQualifiedTypeName().equals( + JavaType.INT_PRIMITIVE.getFullyQualifiedTypeName()) + && fieldType.isPrimitive()) { + typeStr = "int" + arrayStr; + } + else if (fieldType.getFullyQualifiedTypeName().equals( + JavaType.SHORT_PRIMITIVE.getFullyQualifiedTypeName()) + && fieldType.isPrimitive()) { + typeStr = "short" + arrayStr; + } + else if (fieldType.getFullyQualifiedTypeName().equals( + JavaType.BYTE_PRIMITIVE.getFullyQualifiedTypeName()) + && fieldType.isPrimitive()) { + typeStr = "byte" + arrayStr; + } + else if (fieldType.getFullyQualifiedTypeName().equals( + JavaType.CHAR_PRIMITIVE.getFullyQualifiedTypeName()) + && fieldType.isPrimitive()) { + typeStr = "char" + arrayStr; + } + else if (fieldType.equals(new JavaType(STRING + .getFullyQualifiedTypeName(), 1, DataType.TYPE, null, null))) { + typeStr = "String[]"; + } + return typeStr; + } + + public boolean hasEmbeddedIdentifier() { + return embeddedIdHolder != null; + } + + private void setModifyMethod() { + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName("modify" + + entity.getSimpleTypeName()); + final JavaType parameterType = entity; + final List parameterNames = Arrays.asList(OBJ_SYMBOL); + final JavaType returnType = JavaType.BOOLEAN_PRIMITIVE; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterType); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + modifyMethod = userMethod; + return; + } + + // Create method + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return false;"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + builder.addMethod(methodBuilder); + modifyMethod = methodBuilder.build(); + } + + private void setNewTransientEntityMethod() { + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName("getNewTransient" + + entity.getSimpleTypeName()); + + final JavaType parameterType = JavaType.INT_PRIMITIVE; + final List parameterNames = Arrays.asList(INDEX_SYMBOL); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterType); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(entity), + "Method '%s' on '%s' must return '%s'", methodName, + destination, entity.getNameIncludingTypeParameters()); + newTransientEntityMethod = userMethod; + return; + } + + // Create method + builder.getImportRegistrationResolver().addImport(entity); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entity.getSimpleTypeName() + " " + OBJ_VAR + + " = new " + entity.getSimpleTypeName() + "();"); + + // Create the composite key embedded id method call if required + if (hasEmbeddedIdentifier()) { + bodyBuilder.appendFormalLine(getEmbeddedIdMutatorMethodName() + "(" + + OBJ_VAR + ", " + INDEX_VAR + ");"); + } + + // Create a mutator method call for each embedded class + for (final EmbeddedHolder embeddedHolder : embeddedHolders) { + bodyBuilder + .appendFormalLine(getEmbeddedFieldMutatorMethodName(embeddedHolder + .getEmbeddedField().getFieldName()) + + "(" + + OBJ_VAR + + ", " + INDEX_VAR + ");"); + } + + // Create mutator method calls for each entity field + for (final FieldMetadata field : fieldInitializers.keySet()) { + final JavaSymbolName mutatorName = BeanInfoUtils + .getMutatorMethodName(field); + bodyBuilder.appendFormalLine(mutatorName.getSymbolName() + "(" + + OBJ_VAR + ", " + INDEX_VAR + ");"); + } + + bodyBuilder.appendFormalLine("return " + OBJ_VAR + ";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, entity, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + builder.addMethod(methodBuilder); + newTransientEntityMethod = methodBuilder.build(); + } + + /** + * @return the "getRandomEntity():Entity" method (never returns null) + */ + private void setRandomPersistentEntityMethod() { + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName("getRandom" + + entity.getSimpleTypeName()); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(entity), + "Method '%s' on '%s' must return '%s'", methodName, + destination, entity.getNameIncludingTypeParameters()); + randomPersistentEntityMethod = userMethod; + return; + } + + builder.getImportRegistrationResolver().addImport(identifierType); + + // Create method + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("init();"); + bodyBuilder.appendFormalLine(entity.getSimpleTypeName() + " " + OBJ_VAR + + " = " + getDataFieldName().getSymbolName() + ".get(" + + getRndFieldName().getSymbolName() + ".nextInt(" + + getDataField().getFieldName().getSymbolName() + ".size()));"); + bodyBuilder.appendFormalLine(identifierType.getSimpleTypeName() + + " id = " + OBJ_VAR + "." + + identifierAccessor.getMethodName().getSymbolName() + "();"); + bodyBuilder.appendFormalLine("return " + findMethod.getMethodCall() + + ";"); + + findMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, entity, bodyBuilder); + builder.addMethod(methodBuilder); + randomPersistentEntityMethod = methodBuilder.build(); + } + + private void setSpecificPersistentEntityMethod() { + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName("getSpecific" + + entity.getSimpleTypeName()); + final JavaType parameterType = JavaType.INT_PRIMITIVE; + final List parameterNames = Arrays.asList(INDEX_SYMBOL); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterType); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(entity), + "Method '%s on '%s' must return '%s'", methodName, + destination, entity.getNameIncludingTypeParameters()); + specificPersistentEntityMethod = userMethod; + return; + } + + builder.getImportRegistrationResolver().addImport(identifierType); + + // Create method + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("init();"); + bodyBuilder.appendFormalLine("if (" + INDEX_VAR + " < 0) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(INDEX_VAR + " = 0;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("if (" + INDEX_VAR + " > (" + + getDataFieldName().getSymbolName() + ".size() - 1)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(INDEX_VAR + " = " + + getDataFieldName().getSymbolName() + ".size() - 1;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(entity.getSimpleTypeName() + " " + OBJ_VAR + + " = " + getDataFieldName().getSymbolName() + ".get(" + + INDEX_VAR + ");"); + bodyBuilder.appendFormalLine(identifierType.getSimpleTypeName() + + " id = " + OBJ_VAR + "." + + identifierAccessor.getMethodName().getSymbolName() + "();"); + bodyBuilder.appendFormalLine("return " + findMethod.getMethodCall() + + ";"); + + findMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, entity, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + builder.addMethod(methodBuilder); + specificPersistentEntityMethod = methodBuilder.build(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } + + private void validateNumericAnnotationAttribute(final String fieldName, + final String annotationName, final String attributeName, + final Object object) { + Validate.isTrue( + NumberUtils.isNumber(object.toString()), + "%s '%s' attribute for field '%s' in backing type %s must be numeric", + annotationName, attributeName, fieldName, + entity.getFullyQualifiedTypeName()); + } +} \ No newline at end of file diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadataProvider.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadataProvider.java new file mode 100644 index 000000000..fc3b5f157 --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.dod; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link DataOnDemandMetadata}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface DataOnDemandMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadataProviderImpl.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadataProviderImpl.java new file mode 100644 index 000000000..5167801ec --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandMetadataProviderImpl.java @@ -0,0 +1,505 @@ +package org.springframework.roo.addon.dod; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_ID_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSISTENT_TYPE; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.TRANSIENT_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.VERSION_FIELD; +import static org.springframework.roo.model.RooJavaType.ROO_DATA_ON_DEMAND; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.shell.NaturalOrderComparator; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link DataOnDemandMetadataProvider}. + * + * @author Ben Alex + * @author Greg Turnquist + * @author Andrew Swan + * @since 1.0 + */ +@Component +@Service +public class DataOnDemandMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + DataOnDemandMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(DataOnDemandMetadataProviderImpl.class); + + private static final String FLUSH_METHOD = CustomDataKeys.FLUSH_METHOD + .name(); + private static final String PERSIST_METHOD = CustomDataKeys.PERSIST_METHOD + .name(); + + private ConfigurableMetadataProvider configurableMetadataProvider; + private LayerService layerService; + private final Map dodMidToEntityMap = new LinkedHashMap(); + private final Map entityToDodMidMap = new LinkedHashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + // DOD classes are @Configurable because they may need DI of other DOD + // classes that provide M:1 relationships + getConfigurableMetadataProvider().addMetadataTrigger(ROO_DATA_ON_DEMAND); + addMetadataTrigger(ROO_DATA_ON_DEMAND); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return DataOnDemandMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getConfigurableMetadataProvider().removeMetadataTrigger(ROO_DATA_ON_DEMAND); + removeMetadataTrigger(ROO_DATA_ON_DEMAND); + } + + private String getDataOnDemandMetadataId(final JavaType javaType, + final Iterable dataOnDemandTypes) { + for (final ClassOrInterfaceTypeDetails cid : dataOnDemandTypes) { + final AnnotationMetadata dodAnnotation = cid + .getAnnotation(ROO_DATA_ON_DEMAND); + final AnnotationAttributeValue entityAttribute = dodAnnotation + .getAttribute("entity"); + if (entityAttribute != null + && entityAttribute.getValue().equals(javaType)) { + // Found the DoD type for the given field's type + return DataOnDemandMetadata.createIdentifier(cid.getName(), + PhysicalTypeIdentifier.getPath(cid + .getDeclaredByMetadataId())); + } + } + return null; + } + + private List getEmbeddedHolders( + final MemberDetails memberDetails, + final String metadataIdentificationString) { + final List embeddedHolders = new ArrayList(); + + final List embeddedFields = MemberFindingUtils + .getFieldsWithTag(memberDetails, EMBEDDED_FIELD); + if (embeddedFields.isEmpty()) { + return embeddedHolders; + } + + for (final FieldMetadata embeddedField : embeddedFields) { + final MemberDetails embeddedMemberDetails = getMemberDetails(embeddedField + .getFieldType()); + if (embeddedMemberDetails == null) { + continue; + } + + final List fields = new ArrayList(); + + for (final FieldMetadata field : embeddedMemberDetails.getFields()) { + if (!(Modifier.isStatic(field.getModifier()) + || Modifier.isFinal(field.getModifier()) || Modifier + .isTransient(field.getModifier()))) { + getMetadataDependencyRegistry().registerDependency( + field.getDeclaredByMetadataId(), + metadataIdentificationString); + fields.add(field); + } + } + embeddedHolders.add(new EmbeddedHolder(embeddedField, fields)); + } + + return embeddedHolders; + } + + private EmbeddedIdHolder getEmbeddedIdHolder( + final MemberDetails memberDetails, + final String metadataIdentificationString) { + final List idFields = new ArrayList(); + final List fields = MemberFindingUtils.getFieldsWithTag( + memberDetails, EMBEDDED_ID_FIELD); + if (fields.isEmpty()) { + return null; + } + final FieldMetadata embeddedIdField = fields.get(0); + final MemberDetails identifierMemberDetails = getMemberDetails(embeddedIdField + .getFieldType()); + if (identifierMemberDetails == null) { + return null; + } + + for (final FieldMetadata field : identifierMemberDetails.getFields()) { + if (!(Modifier.isStatic(field.getModifier()) + || Modifier.isFinal(field.getModifier()) || Modifier + .isTransient(field.getModifier()))) { + getMetadataDependencyRegistry().registerDependency( + field.getDeclaredByMetadataId(), + metadataIdentificationString); + idFields.add(field); + } + } + + return new EmbeddedIdHolder(embeddedIdField, idFields); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = DataOnDemandMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = DataOnDemandMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "DataOnDemand"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any DOD metadata is + // even hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + + for (final JavaType type : itdTypeDetails.getGovernor() + .getLayerEntities()) { + final String localMidType = entityToDodMidMap.get(type); + if (localMidType != null) { + return localMidType; + } + } + + final String localMid = entityToDodMidMap.get(governor); + if (localMid == null) { + // No DOD is interested in this JavaType, so let's move on + return null; + } + + // We have some DOD metadata, so let's check if we care if any methods + // match our requirements + for (final MethodMetadata method : itdTypeDetails.getDeclaredMethods()) { + if (BeanInfoUtils.isMutatorMethod(method)) { + // A DOD cares about the JavaType, and an ITD offers a method + // likely of interest, so let's formally trigger it to run. + // Note that it will re-scan and discover this ITD, and register + // a direct dependency on it for the future. + return localMid; + } + } + + return null; + } + + private Map getLocatedFields( + final MemberDetails memberDetails, final String dodMetadataId) { + final Map locatedFields = new LinkedHashMap(); + final Iterable dataOnDemandTypes = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_DATA_ON_DEMAND); + + final List mutatorMethods = memberDetails.getMethods(); + // To avoid unnecessary rewriting of the DoD ITD we sort the mutators by + // method name to provide a consistent ordering + Collections.sort(mutatorMethods, + new NaturalOrderComparator() { + @Override + protected String stringify(final MethodMetadata object) { + return object.getMethodName().getSymbolName(); + } + }); + + for (final MethodMetadata method : mutatorMethods) { + if (!BeanInfoUtils.isMutatorMethod(method)) { + continue; + } + + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field == null) { + continue; + } + + // Track any changes to the mutator method (eg it goes away) + getMetadataDependencyRegistry().registerDependency( + method.getDeclaredByMetadataId(), dodMetadataId); + + final Set fieldCustomDataKeys = field.getCustomData() + .keySet(); + + // Never include id or version fields (they shouldn't normally have + // a mutator anyway, but the user might have added one), or embedded + // types + if (fieldCustomDataKeys.contains(IDENTIFIER_FIELD) + || fieldCustomDataKeys.contains(EMBEDDED_ID_FIELD) + || fieldCustomDataKeys.contains(EMBEDDED_FIELD) + || fieldCustomDataKeys.contains(VERSION_FIELD)) { + continue; + } + + // Never include persistence transient fields + if (fieldCustomDataKeys.contains(TRANSIENT_FIELD)) { + continue; + } + + // Never include any sort of collection; user has to make such + // entities by hand + if (field.getFieldType().isCommonCollectionType() + || fieldCustomDataKeys.contains(ONE_TO_MANY_FIELD) + || fieldCustomDataKeys.contains(MANY_TO_MANY_FIELD)) { + continue; + } + + // Look up collaborating metadata + final DataOnDemandMetadata collaboratingMetadata = locateCollaboratingMetadata( + dodMetadataId, field, dataOnDemandTypes); + locatedFields.put(field, collaboratingMetadata); + } + + return locatedFields; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String dodMetadataId, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(layerService == null){ + layerService = getLayerService(); + } + Validate.notNull(layerService, "LayerService is required"); + + // We need to parse the annotation, which we expect to be present + final DataOnDemandAnnotationValues annotationValues = new DataOnDemandAnnotationValues( + governorPhysicalTypeMetadata); + final JavaType entity = annotationValues.getEntity(); + if (!annotationValues.isAnnotationFound() || entity == null) { + return null; + } + + // Remember that this entity JavaType matches up with this DOD's + // metadata identification string + // Start by clearing the previous association + final JavaType oldEntity = dodMidToEntityMap.get(dodMetadataId); + if (oldEntity != null) { + entityToDodMidMap.remove(oldEntity); + } + entityToDodMidMap.put(annotationValues.getEntity(), dodMetadataId); + dodMidToEntityMap.put(dodMetadataId, annotationValues.getEntity()); + + final JavaType identifierType = getPersistenceMemberLocator() + .getIdentifierType(entity); + if (identifierType == null) { + return null; + } + + final MemberDetails memberDetails = getMemberDetails(entity); + if (memberDetails == null) { + return null; + } + + final MemberHoldingTypeDetails persistenceMemberHoldingTypeDetails = MemberFindingUtils + .getMostConcreteMemberHoldingTypeDetailsWithTag(memberDetails, + PERSISTENT_TYPE); + if (persistenceMemberHoldingTypeDetails == null) { + return null; + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + persistenceMemberHoldingTypeDetails.getDeclaredByMetadataId(), + dodMetadataId); + + // Get the additions to make for each required method + final MethodParameter fromParameter = new MethodParameter( + JavaType.INT_PRIMITIVE, "from"); + final MethodParameter toParameter = new MethodParameter( + JavaType.INT_PRIMITIVE, "to"); + final MemberTypeAdditions findEntriesMethod = layerService + .getMemberTypeAdditions(dodMetadataId, + FIND_ENTRIES_METHOD.name(), entity, identifierType, + LayerType.HIGHEST.getPosition(), fromParameter, + toParameter); + final MemberTypeAdditions findMethodAdditions = layerService + .getMemberTypeAdditions(dodMetadataId, FIND_METHOD.name(), + entity, identifierType, + LayerType.HIGHEST.getPosition(), new MethodParameter( + identifierType, "id")); + final MethodParameter entityParameter = new MethodParameter(entity, + "obj"); + final MemberTypeAdditions flushMethod = layerService + .getMemberTypeAdditions(dodMetadataId, FLUSH_METHOD, entity, + identifierType, LayerType.HIGHEST.getPosition(), + entityParameter); + final MethodMetadata identifierAccessor = memberDetails + .getMostConcreteMethodWithTag(IDENTIFIER_ACCESSOR_METHOD); + final MemberTypeAdditions persistMethodAdditions = layerService + .getMemberTypeAdditions(dodMetadataId, PERSIST_METHOD, entity, + identifierType, LayerType.HIGHEST.getPosition(), + entityParameter); + + if (findEntriesMethod == null || findMethodAdditions == null + || identifierAccessor == null || persistMethodAdditions == null) { + return null; + } + + // Identify all the fields we care about on the entity + final Map locatedFields = getLocatedFields( + memberDetails, dodMetadataId); + + // Get the embedded identifier metadata holder - may be null if no + // embedded identifier exists + final EmbeddedIdHolder embeddedIdHolder = getEmbeddedIdHolder( + memberDetails, dodMetadataId); + + // Get the list of embedded metadata holders - may be an empty list if + // no embedded identifier exists + final List embeddedHolders = getEmbeddedHolders( + memberDetails, dodMetadataId); + + return new DataOnDemandMetadata(dodMetadataId, aspectName, + governorPhysicalTypeMetadata, annotationValues, + identifierAccessor, findMethodAdditions, findEntriesMethod, + persistMethodAdditions, flushMethod, locatedFields, + identifierType, embeddedIdHolder, embeddedHolders); + } + + public String getProvidesType() { + return DataOnDemandMetadata.getMetadataIdentiferType(); + } + + /** + * Returns the {@link DataOnDemandMetadata} for the entity that's the target + * of the given reference field. + * + * @param dodMetadataId + * @param field + * @param dataOnDemandTypes + * @return null if it's not an n:1 or 1:1 field, or the DoD + * metadata is simply not available + */ + private DataOnDemandMetadata locateCollaboratingMetadata( + final String dodMetadataId, final FieldMetadata field, + final Iterable dataOnDemandTypes) { + if (!(field.getCustomData().keySet().contains(MANY_TO_ONE_FIELD) || field + .getCustomData().keySet().contains(ONE_TO_ONE_FIELD))) { + return null; + } + + final String otherDodMetadataId = getDataOnDemandMetadataId( + field.getFieldType(), dataOnDemandTypes); + + if (otherDodMetadataId == null + || otherDodMetadataId.equals(dodMetadataId)) { + // No DoD for this field's type, or it's a self-reference + return null; + } + + // Make this DoD depend on the related entity (not its Dod, otherwise + // we get a circular MD dependency) + registerDependencyUponType(dodMetadataId, field.getFieldType()); + + return (DataOnDemandMetadata) getMetadataService().get(otherDodMetadataId); + } + + private void registerDependencyUponType(final String dodMetadataId, + final JavaType type) { + final String fieldPhysicalTypeId = typeLocationService + .getPhysicalTypeIdentifier(type); + getMetadataDependencyRegistry().registerDependency(fieldPhysicalTypeId, + dodMetadataId); + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on DataOnDemandMetadataProviderImpl."); + return null; + } + }else{ + return configurableMetadataProvider; + } + + } + + public LayerService getLayerService(){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on DataOnDemandMetadataProviderImpl."); + return null; + } + } +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandOperations.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandOperations.java new file mode 100644 index 000000000..089ba7c56 --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandOperations.java @@ -0,0 +1,28 @@ +package org.springframework.roo.addon.dod; + +import org.springframework.roo.model.JavaType; + +/** + * Creates a new data-on-demand class for an entity. + * + * @author Alan Stewart + * @since 1.1.3 + */ +public interface DataOnDemandOperations { + + /** + * Checks for the existence the META-INF/persistence.xml + * + * @return true if the META-INF/persistence.xml exists, otherwise false + */ + boolean isDataOnDemandInstallationPossible(); + + /** + * Creates a new data-on-demand (DoD) provider for the entity. Silently + * returns if the DoD class already exists. + * + * @param entity to produce a DoD provider for (required) + * @param name the name of the new DoD class (required) + */ + void newDod(JavaType entity, JavaType name); +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandOperationsImpl.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandOperationsImpl.java new file mode 100644 index 000000000..0db3d520b --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/DataOnDemandOperationsImpl.java @@ -0,0 +1,126 @@ +package org.springframework.roo.addon.dod; + +import static org.springframework.roo.model.JpaJavaType.ENTITY; +import static org.springframework.roo.model.SpringJavaType.PERSISTENT; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * Implementation of {@link DataOnDemandOperations}. + * + * @author Alan Stewart + * @since 1.1.3 + */ +@Component +@Service +public class DataOnDemandOperationsImpl implements DataOnDemandOperations { + + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + /** + * @param entity the entity to lookup required + * @return the type details (never null; throws an exception if it cannot be + * obtained or parsed) + */ + private ClassOrInterfaceTypeDetails getEntity(final JavaType entity) { + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(entity); + Validate.notNull(cid, + "Java source code details unavailable for type '%s'", entity); + return cid; + } + + public boolean isDataOnDemandInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && projectOperations.isFeatureInstalledInFocusedModule( + FeatureNames.JPA, FeatureNames.MONGO); + } + + public void newDod(final JavaType entity, final JavaType name) { + Validate.notNull(entity, + "Entity to produce a data on demand provider for is required"); + Validate.notNull(name, + "Name of the new data on demand provider is required"); + + final LogicalPath path = LogicalPath.getInstance(Path.SRC_TEST_JAVA, + projectOperations.getFocusedModuleName()); + Validate.notNull(path, + "Location of the new data on demand provider is required"); + + // Verify the requested entity actually exists as a class and is not + // abstract + final ClassOrInterfaceTypeDetails cid = getEntity(entity); + Validate.isTrue( + cid.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS, + "Type %s is not a class", entity.getFullyQualifiedTypeName()); + Validate.isTrue(!Modifier.isAbstract(cid.getModifier()), + "Type %s is abstract", entity.getFullyQualifiedTypeName()); + + // Check if the requested entity is a JPA @Entity + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(DataOnDemandOperationsImpl.class.getName(), + cid); + final AnnotationMetadata entityAnnotation = memberDetails + .getAnnotation(ENTITY); + final AnnotationMetadata persistentAnnotation = memberDetails + .getAnnotation(PERSISTENT); + Validate.isTrue(entityAnnotation != null + || persistentAnnotation != null, + "Type %s must be a persistent type", + entity.getFullyQualifiedTypeName()); + + // Everything is OK to proceed + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, path); + + if (metadataService.get(declaredByMetadataId) != null) { + // The file already exists + return; + } + + final List annotations = new ArrayList(); + final List> dodConfig = new ArrayList>(); + dodConfig.add(new ClassAttributeValue(new JavaSymbolName("entity"), + entity)); + annotations.add(new AnnotationMetadataBuilder( + RooJavaType.ROO_DATA_ON_DEMAND, dodConfig)); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, name, + PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(annotations); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/EmbeddedHolder.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/EmbeddedHolder.java new file mode 100644 index 000000000..96fa93c36 --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/EmbeddedHolder.java @@ -0,0 +1,42 @@ +package org.springframework.roo.addon.dod; + +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Holder for embedded attributes + * + * @author Greg Turnquist + * @since 1.2.0 + */ +public class EmbeddedHolder { + + private final FieldMetadata embeddedField; + private final List fields; + + public EmbeddedHolder(final FieldMetadata embeddedField, + final List fields) { + Validate.notNull(embeddedField, "Identifier type required"); + Validate.notNull(fields, "Fields for " + + embeddedField.getFieldType().getFullyQualifiedTypeName() + + " required"); + this.embeddedField = embeddedField; + this.fields = fields; + } + + public FieldMetadata getEmbeddedField() { + return embeddedField; + } + + public JavaSymbolName getEmbeddedMutatorMethodName() { + return BeanInfoUtils.getMutatorMethodName(embeddedField); + } + + public List getFields() { + return fields; + } +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/EmbeddedIdHolder.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/EmbeddedIdHolder.java new file mode 100644 index 000000000..816d92a8c --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/EmbeddedIdHolder.java @@ -0,0 +1,44 @@ +package org.springframework.roo.addon.dod; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Holder for embedded id attributes + * + * @author Alan Stewart + * @author Greg Turnquist + * @since 1.1.3 + */ +public class EmbeddedIdHolder { + + private final FieldMetadata embeddedIdField; + private final List idFields; + + public EmbeddedIdHolder(final FieldMetadata embeddedIdField, + final List idFields) { + Validate.notNull(embeddedIdField, "Identifier type required"); + Validate.notNull(idFields, "Fields for %s required", embeddedIdField + .getFieldType().getFullyQualifiedTypeName()); + this.embeddedIdField = embeddedIdField; + this.idFields = idFields; + } + + public FieldMetadata getEmbeddedIdField() { + return embeddedIdField; + } + + public JavaSymbolName getEmbeddedIdMutator() { + return new JavaSymbolName("set" + + StringUtils.capitalize(embeddedIdField.getFieldName() + .getSymbolName())); + } + + public List getIdFields() { + return idFields; + } +} diff --git a/addon-dod/src/main/java/org/springframework/roo/addon/dod/RooDataOnDemand.java b/addon-dod/src/main/java/org/springframework/roo/addon/dod/RooDataOnDemand.java new file mode 100644 index 000000000..d980809e7 --- /dev/null +++ b/addon-dod/src/main/java/org/springframework/roo/addon/dod/RooDataOnDemand.java @@ -0,0 +1,29 @@ +package org.springframework.roo.addon.dod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates to produce a "data on demand" class, which is required for + * automated integration testing. + * + * @author Ben Alex + * @since 1.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooDataOnDemand { + + /** + * @return the type of class that will have data on demand created + * (required; must offer entity services) + */ + Class entity(); + + /** + * @return the number of entities to create (required; defaults to 10) + */ + int quantity() default 10; +} diff --git a/addon-dod/src/test/java/org/springframework/roo/addon/dod/DataOnDemandAnnotationValuesTest.java b/addon-dod/src/test/java/org/springframework/roo/addon/dod/DataOnDemandAnnotationValuesTest.java new file mode 100644 index 000000000..c5bf5b88c --- /dev/null +++ b/addon-dod/src/test/java/org/springframework/roo/addon/dod/DataOnDemandAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.dod; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link DataOnDemandAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DataOnDemandAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooDataOnDemand.class; + } + + @Override + protected Class getValuesClass() { + return DataOnDemandAnnotationValues.class; + } +} diff --git a/addon-email/pom.xml b/addon-email/pom.xml new file mode 100644 index 000000000..e2a853b6f --- /dev/null +++ b/addon-email/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.email + bundle + Spring Roo - Addon - Email + Support for integration and configuration of Spring's email support in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-email/src/main/java/org/springframework/roo/addon/email/MailCommands.java b/addon-email/src/main/java/org/springframework/roo/addon/email/MailCommands.java new file mode 100644 index 000000000..07740f4d9 --- /dev/null +++ b/addon-email/src/main/java/org/springframework/roo/addon/email/MailCommands.java @@ -0,0 +1,94 @@ +package org.springframework.roo.addon.email; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Commands for the 'email' add-on to be used by the Roo shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class MailCommands implements CommandMarker { + + @Reference private MailOperations mailOperations; + @Reference private StaticFieldConverter staticFieldConverter; + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(MailProtocol.class); + } + + @CliCommand(value = "email template setup", help = "Configures a template for a SimpleMailMessage") + public void configureEmailTemplate( + @CliOption(key = { "from" }, mandatory = false, help = "The 'from' email (optional)") final String from, + @CliOption(key = { "subject" }, mandatory = false, help = "The message subject (obtional)") final String subject) { + + mailOperations.configureTemplateMessage(from, subject); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(MailProtocol.class); + } + + @CliCommand(value = "field email template", help = "Inserts a MailTemplate field into an existing type") + public void injectEmailTemplate( + @CliOption(key = { "", "fieldName" }, mandatory = false, specifiedDefaultValue = "mailTemplate", unspecifiedDefaultValue = "mailTemplate", help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "async", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates if the injected method should be executed asynchronously") final boolean async) { + + mailOperations.injectEmailTemplate(typeName, fieldName, async); + } + + @CliCommand(value = "email sender setup", help = "Install a Spring JavaMailSender in your project") + public void installEmail( + @CliOption(key = { "hostServer" }, mandatory = true, help = "The host server") final String hostServer, + @CliOption(key = { "protocol" }, mandatory = false, help = "The protocol used by mail server") final MailProtocol protocol, + @CliOption(key = { "port" }, mandatory = false, help = "The port used by mail server") final String port, + @CliOption(key = { "encoding" }, mandatory = false, help = "The encoding used for mail") final String encoding, + @CliOption(key = { "username" }, mandatory = false, help = "The mail account username") final String username, + @CliOption(key = { "password" }, mandatory = false, help = "The mail account password") final String password) { + + mailOperations.installEmail(hostServer, protocol, port, encoding, + username, password); + } + + /** + * Indicates whether the mail template commands are available + * + * @return see above + * @deprecated call {@link #isMailTemplateAvailable()} instead + */ + @Deprecated + public boolean isInsertJmsAvailable() { + return isMailTemplateAvailable(); + } + + @CliAvailabilityIndicator("email sender setup") + public boolean isInstallEmailAvailable() { + return mailOperations.isEmailInstallationPossible(); + } + + /** + * Indicates whether the mail template commands are available + * + * @return see above + * @since 1.2.0 + */ + @CliAvailabilityIndicator({ "field email template", "email template setup" }) + public boolean isMailTemplateAvailable() { + return mailOperations.isManageEmailAvailable(); + } +} \ No newline at end of file diff --git a/addon-email/src/main/java/org/springframework/roo/addon/email/MailOperations.java b/addon-email/src/main/java/org/springframework/roo/addon/email/MailOperations.java new file mode 100644 index 000000000..999165037 --- /dev/null +++ b/addon-email/src/main/java/org/springframework/roo/addon/email/MailOperations.java @@ -0,0 +1,35 @@ +package org.springframework.roo.addon.email; + +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Provides email configuration operations. + * + * @author Ben Alex + */ +public interface MailOperations { + + void configureTemplateMessage(String from, String subject); + + void injectEmailTemplate(JavaType targetType, JavaSymbolName fieldName, + boolean async); + + void installEmail(String hostServer, MailProtocol protocol, String port, + String encoding, String username, String password); + + /** + * Indicates whether the command for adding a JavaMailSender to the user's + * project is available. + * + * @return see above + */ + boolean isEmailInstallationPossible(); + + /** + * Indicates whether the commands relating to mail templates are available + * + * @return see above + */ + boolean isManageEmailAvailable(); +} \ No newline at end of file diff --git a/addon-email/src/main/java/org/springframework/roo/addon/email/MailOperationsImpl.java b/addon-email/src/main/java/org/springframework/roo/addon/email/MailOperationsImpl.java new file mode 100644 index 000000000..2c3ac58f6 --- /dev/null +++ b/addon-email/src/main/java/org/springframework/roo/addon/email/MailOperationsImpl.java @@ -0,0 +1,426 @@ +package org.springframework.roo.addon.email; + +import static org.springframework.roo.addon.email.MailProtocol.SMTP; +import static org.springframework.roo.model.SpringJavaType.ASYNC; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; +import static org.springframework.roo.model.SpringJavaType.JAVA_MAIL_SENDER_IMPL; +import static org.springframework.roo.model.SpringJavaType.MAIL_SENDER; +import static org.springframework.roo.model.SpringJavaType.SIMPLE_MAIL_MESSAGE; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.PairList; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of {@link MailOperationsImpl}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class MailOperationsImpl implements MailOperations { + + private static final String LOCAL_MESSAGE_VARIABLE = "mailMessage"; + private static final Logger LOGGER = HandlerUtils + .getLogger(MailOperationsImpl.class); + private static final int PRIVATE_TRANSIENT = Modifier.PRIVATE + | Modifier.TRANSIENT; + private static final String SPRING_TASK_NS = "http://www.springframework.org/schema/task"; + private static final String SPRING_TASK_XSD = "http://www.springframework.org/schema/task/spring-task-3.1.xsd"; + private static final AnnotatedJavaType STRING = new AnnotatedJavaType( + JavaType.STRING); + private static final String TEMPLATE_MESSAGE_FIELD = "templateMessage"; + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private PropFileOperations propFileOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void configureTemplateMessage(final String from, final String subject) { + final String contextPath = getApplicationContextPath(); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = document.getDocumentElement(); + + final Map props = new HashMap(); + + if (StringUtils.isNotBlank(from) || StringUtils.isNotBlank(subject)) { + Element smmBean = getSimpleMailMessageBean(root); + if (smmBean == null) { + smmBean = document.createElement("bean"); + smmBean.setAttribute("class", + SIMPLE_MAIL_MESSAGE.getFullyQualifiedTypeName()); + smmBean.setAttribute("id", "templateMessage"); + } + + if (StringUtils.isNotBlank(from)) { + Element smmProperty = XmlUtils.findFirstElement( + "//property[@name='from']", smmBean); + if (smmProperty != null) { + smmBean.removeChild(smmProperty); + } + smmProperty = document.createElement("property"); + smmProperty.setAttribute("value", "${email.from}"); + smmProperty.setAttribute("name", "from"); + smmBean.appendChild(smmProperty); + props.put("email.from", from); + } + + if (StringUtils.isNotBlank(subject)) { + Element smmProperty = XmlUtils.findFirstElement( + "//property[@name='subject']", smmBean); + if (smmProperty != null) { + smmBean.removeChild(smmProperty); + } + smmProperty = document.createElement("property"); + smmProperty.setAttribute("value", "${email.subject}"); + smmProperty.setAttribute("name", "subject"); + smmBean.appendChild(smmProperty); + props.put("email.subject", subject); + } + + root.appendChild(smmBean); + + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(contextPath, + XmlUtils.nodeToString(document), false); + } + + if (props.size() > 0) { + propFileOperations.addProperties(Path.SPRING_CONFIG_ROOT + .getModulePathId(projectOperations.getFocusedModuleName()), + "email.properties", props, true, true); + } + } + + /** + * Returns the canonical path of the user project's applicationContext.xml + * file. + * + * @return a non-blank path + */ + private String getApplicationContextPath() { + return pathResolver.getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext.xml"); + } + + /** + * Generates the "send email" method to be added to the domain type + * + * @param mailSenderName the name of the MailSender field (required) + * @param async whether to send the email asynchronously + * @param targetClassMID the MID of the class to receive the method + * @param mutableTypeDetails the type to which the method is being added + * (required) + * @return a non-null method + */ + private MethodMetadataBuilder getSendMethod( + final JavaSymbolName mailSenderName, final boolean async, + final String targetClassMID, + final ClassOrInterfaceTypeDetailsBuilder cidBuilder) { + final String contextPath = getApplicationContextPath(); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = document.getDocumentElement(); + + // Make a builder for the created method's body + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + // Collect the types and names of the created method's parameters + final PairList parameters = new PairList(); + + if (getSimpleMailMessageBean(root) == null) { + // There's no SimpleMailMessage bean; use a local variable + bodyBuilder.appendFormalLine(SIMPLE_MAIL_MESSAGE + .getFullyQualifiedTypeName() + + " " + + LOCAL_MESSAGE_VARIABLE + + " = new " + + SIMPLE_MAIL_MESSAGE.getFullyQualifiedTypeName() + "();"); + // Set the from address + parameters.add(STRING, new JavaSymbolName("mailFrom")); + bodyBuilder.appendFormalLine(LOCAL_MESSAGE_VARIABLE + + ".setFrom(mailFrom);"); + // Set the subject + parameters.add(STRING, new JavaSymbolName("subject")); + bodyBuilder.appendFormalLine(LOCAL_MESSAGE_VARIABLE + + ".setSubject(subject);"); + } + else { + // A SimpleMailMessage bean exists; auto-wire it into the entity and + // use it as a template + final List smmAnnotations = Arrays + .asList(new AnnotationMetadataBuilder(AUTOWIRED)); + final FieldMetadataBuilder smmFieldBuilder = new FieldMetadataBuilder( + targetClassMID, PRIVATE_TRANSIENT, smmAnnotations, + new JavaSymbolName(TEMPLATE_MESSAGE_FIELD), + SIMPLE_MAIL_MESSAGE); + cidBuilder.addField(smmFieldBuilder); + // Use the injected bean as a template (for thread safety) + bodyBuilder.appendFormalLine(SIMPLE_MAIL_MESSAGE + .getFullyQualifiedTypeName() + + " " + + LOCAL_MESSAGE_VARIABLE + + " = new " + + SIMPLE_MAIL_MESSAGE.getFullyQualifiedTypeName() + + "(" + + TEMPLATE_MESSAGE_FIELD + ");"); + } + + // Set the to address + parameters.add(STRING, new JavaSymbolName("mailTo")); + bodyBuilder + .appendFormalLine(LOCAL_MESSAGE_VARIABLE + ".setTo(mailTo);"); + + // Set the message body + parameters.add(STRING, new JavaSymbolName("message")); + bodyBuilder.appendFormalLine(LOCAL_MESSAGE_VARIABLE + + ".setText(message);"); + + bodyBuilder.newLine(); + bodyBuilder.appendFormalLine(mailSenderName + ".send(" + + LOCAL_MESSAGE_VARIABLE + ");"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + targetClassMID, Modifier.PUBLIC, new JavaSymbolName( + "sendMessage"), JavaType.VOID_PRIMITIVE, + parameters.getKeys(), parameters.getValues(), bodyBuilder); + + if (async) { + if (DomUtils.findFirstElementByName("task:annotation-driven", root) == null) { + // Add asynchronous email support to the application + if (StringUtils.isBlank(root.getAttribute("xmlns:task"))) { + // Add the "task" namespace to the Spring config file + root.setAttribute("xmlns:task", SPRING_TASK_NS); + root.setAttribute("xsi:schemaLocation", + root.getAttribute("xsi:schemaLocation") + " " + + SPRING_TASK_NS + " " + SPRING_TASK_XSD); + } + root.appendChild(new XmlElementBuilder( + "task:annotation-driven", document).addAttribute( + "executor", "asyncExecutor").build()); + root.appendChild(new XmlElementBuilder("task:executor", + document).addAttribute("id", "asyncExecutor") + .addAttribute("pool-size", "${executor.poolSize}") + .build()); + // Write out the new Spring config file + fileManager.createOrUpdateTextFileIfRequired(contextPath, + XmlUtils.nodeToString(document), false); + // Update the email properties file + propFileOperations.addPropertyIfNotExists( + pathResolver.getFocusedPath(Path.SPRING_CONFIG_ROOT), + "email.properties", "executor.poolSize", "10", true); + } + methodBuilder.addAnnotation(new AnnotationMetadataBuilder(ASYNC)); + } + return methodBuilder; + } + + /** + * Finds the SimpleMailMessage bean in the Spring XML file with the given + * root element + * + * @param root + * @return null if there is no such bean + */ + private Element getSimpleMailMessageBean(final Element root) { + return XmlUtils.findFirstElement("/beans/bean[@class = '" + + SIMPLE_MAIL_MESSAGE.getFullyQualifiedTypeName() + "']", root); + } + + public void injectEmailTemplate(final JavaType targetType, + final JavaSymbolName fieldName, final boolean async) { + Validate.notNull(targetType, "Java type required"); + Validate.notNull(fieldName, "Field name required"); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(AUTOWIRED)); + + // Obtain the physical type and its mutable class details + final String declaredByMetadataId = typeLocationService + .getPhysicalTypeIdentifier(targetType); + final ClassOrInterfaceTypeDetails existing = typeLocationService + .getTypeDetails(targetType); + if (existing == null) { + LOGGER.warning("Aborting: Unable to find metadata for target type '" + + targetType.getFullyQualifiedTypeName() + "'"); + return; + } + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + existing); + + // Add the MailSender field + final FieldMetadataBuilder mailSenderFieldBuilder = new FieldMetadataBuilder( + declaredByMetadataId, PRIVATE_TRANSIENT, annotations, + fieldName, MAIL_SENDER); + cidBuilder.addField(mailSenderFieldBuilder); + + // Add the "sendMessage" method + cidBuilder.addMethod(getSendMethod(fieldName, async, + declaredByMetadataId, cidBuilder)); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void installEmail(final String hostServer, + final MailProtocol protocol, final String port, + final String encoding, final String username, final String password) { + Validate.notBlank(hostServer, "Host server name required"); + + final String contextPath = getApplicationContextPath(); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = document.getDocumentElement(); + + boolean installDependencies = true; + final Map props = new HashMap(); + + Element mailBean = XmlUtils.findFirstElement("/beans/bean[@class = '" + + JAVA_MAIL_SENDER_IMPL.getFullyQualifiedTypeName() + "']", + root); + if (mailBean != null) { + root.removeChild(mailBean); + installDependencies = false; + } + + mailBean = document.createElement("bean"); + mailBean.setAttribute("class", + JAVA_MAIL_SENDER_IMPL.getFullyQualifiedTypeName()); + mailBean.setAttribute("id", "mailSender"); + + final Element property = document.createElement("property"); + property.setAttribute("name", "host"); + property.setAttribute("value", "${email.host}"); + mailBean.appendChild(property); + root.appendChild(mailBean); + props.put("email.host", hostServer); + + if (protocol != null) { + final Element pElement = document.createElement("property"); + pElement.setAttribute("value", "${email.protocol}"); + pElement.setAttribute("name", "protocol"); + mailBean.appendChild(pElement); + props.put("email.protocol", protocol.getProtocol()); + } + + if (StringUtils.isNotBlank(port)) { + final Element pElement = document.createElement("property"); + pElement.setAttribute("name", "port"); + pElement.setAttribute("value", "${email.port}"); + mailBean.appendChild(pElement); + props.put("email.port", port); + } + + if (StringUtils.isNotBlank(encoding)) { + final Element pElement = document.createElement("property"); + pElement.setAttribute("name", "defaultEncoding"); + pElement.setAttribute("value", "${email.encoding}"); + mailBean.appendChild(pElement); + props.put("email.encoding", encoding); + } + + if (StringUtils.isNotBlank(username)) { + final Element pElement = document.createElement("property"); + pElement.setAttribute("name", "username"); + pElement.setAttribute("value", "${email.username}"); + mailBean.appendChild(pElement); + props.put("email.username", username); + } + + if (StringUtils.isNotBlank(password)) { + final Element pElement = document.createElement("property"); + pElement.setAttribute("name", "password"); + pElement.setAttribute("value", "${email.password}"); + mailBean.appendChild(pElement); + props.put("email.password", password); + + if (SMTP.equals(protocol)) { + final Element javaMailProperties = document + .createElement("property"); + javaMailProperties.setAttribute("name", "javaMailProperties"); + final Element securityProps = document.createElement("props"); + javaMailProperties.appendChild(securityProps); + final Element prop = document.createElement("prop"); + prop.setAttribute("key", "mail.smtp.auth"); + prop.setTextContent("true"); + securityProps.appendChild(prop); + final Element prop2 = document.createElement("prop"); + prop2.setAttribute("key", "mail.smtp.starttls.enable"); + prop2.setTextContent("true"); + securityProps.appendChild(prop2); + mailBean.appendChild(javaMailProperties); + } + } + + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(contextPath, + XmlUtils.nodeToString(document), false); + + if (installDependencies) { + updateConfiguration(projectOperations.getFocusedModuleName()); + } + + propFileOperations.addProperties(Path.SPRING_CONFIG_ROOT + .getModulePathId(projectOperations.getFocusedModuleName()), + "email.properties", props, true, true); + } + + public boolean isEmailInstallationPossible() { + return projectOperations.isFocusedProjectAvailable(); + } + + public boolean isManageEmailAvailable() { + return projectOperations.isFocusedProjectAvailable() + && fileManager.exists(getApplicationContextPath()); + } + + private void updateConfiguration(final String moduleName) { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List emailDependencies = XmlUtils.findElements( + "/configuration/email/dependencies/dependency", configuration); + for (final Element dependencyElement : emailDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + projectOperations.addDependencies(moduleName, dependencies); + } +} \ No newline at end of file diff --git a/addon-email/src/main/java/org/springframework/roo/addon/email/MailProtocol.java b/addon-email/src/main/java/org/springframework/roo/addon/email/MailProtocol.java new file mode 100644 index 000000000..d5c66164b --- /dev/null +++ b/addon-email/src/main/java/org/springframework/roo/addon/email/MailProtocol.java @@ -0,0 +1,66 @@ +package org.springframework.roo.addon.email; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Protocols known to the email add-on. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class MailProtocol implements Comparable { + + public static final MailProtocol IMAP = new MailProtocol("IMAP", "imap"); + public static final MailProtocol POP3 = new MailProtocol("POP3", "pop3"); + public static final MailProtocol SMTP = new MailProtocol("SMTP", "smtp"); + + private final String protocol; + private final String protocolLabel; + + public MailProtocol(final String protocolLabel, final String protocol) { + Validate.notNull(protocolLabel, "Protocol label required"); + Validate.notNull(protocol, "protocol required"); + this.protocolLabel = protocolLabel; + this.protocol = protocol; + } + + public final int compareTo(final MailProtocol o) { + if (o == null) { + return -1; + } + final int result = protocolLabel.compareTo(o.protocolLabel); + + return result; + } + + @Override + public final boolean equals(final Object obj) { + return obj instanceof MailProtocol + && compareTo((MailProtocol) obj) == 0; + } + + public String getKey() { + return protocolLabel; + } + + public String getProtocol() { + return protocol; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (protocolLabel == null ? 0 : protocolLabel.hashCode()); + return result; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("provider", protocolLabel); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-email/src/main/resources/org/springframework/roo/addon/email/configuration.xml b/addon-email/src/main/resources/org/springframework/roo/addon/email/configuration.xml new file mode 100644 index 000000000..2b765c467 --- /dev/null +++ b/addon-email/src/main/resources/org/springframework/roo/addon/email/configuration.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/addon-equals/pom.xml b/addon-equals/pom.xml new file mode 100644 index 000000000..314b12d58 --- /dev/null +++ b/addon-equals/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.equals + bundle + Spring Roo - Addon - Equals/HashCode + Adds equals and hashCode methods to a class + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsAnnotationValues.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsAnnotationValues.java new file mode 100644 index 000000000..5752760ae --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsAnnotationValues.java @@ -0,0 +1,39 @@ +package org.springframework.roo.addon.equals; + +import static org.springframework.roo.model.RooJavaType.ROO_EQUALS; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; + +/** + * Represents a parsed {@link RooEquals} annotation. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class EqualsAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private boolean appendSuper; + @AutoPopulate private String[] excludeFields; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public EqualsAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, ROO_EQUALS); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String[] getExcludeFields() { + return excludeFields; + } + + public boolean isAppendSuper() { + return appendSuper; + } +} diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsCommands.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsCommands.java new file mode 100644 index 000000000..7b6f5d66f --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsCommands.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.equals; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the Equals add-on to be used by the ROO shell. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class EqualsCommands implements CommandMarker { + + @Reference private EqualsOperations equalsOperations; + + @CliCommand(value = "equals", help = "Add equals and hashCode methods to a class") + public void addEquals( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class") final JavaType javaType, + @CliOption(key = "appendSuper", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Whether to call the super class equals and hashCode methods") final boolean appendSuper, + @CliOption(key = "excludeFields", mandatory = false, specifiedDefaultValue = "", optionContext = "exclude-fields", help = "The fields to exclude in the equals and hashcode methods. Multiple field names must be a double-quoted list separated by spaces") final Set excludeFields) { + + equalsOperations.addEqualsAndHashCodeMethods(javaType, appendSuper, + excludeFields); + } +} \ No newline at end of file diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadata.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadata.java new file mode 100644 index 000000000..68a9e5719 --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadata.java @@ -0,0 +1,209 @@ +package org.springframework.roo.addon.equals; + +import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.OBJECT; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Metadata for {@link RooEquals}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class EqualsMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final JavaType EQUALS_BUILDER = new JavaType( + "org.apache.commons.lang3.builder.EqualsBuilder"); + private static final JavaSymbolName EQUALS_METHOD_NAME = new JavaSymbolName( + "equals"); + private static final JavaType HASH_CODE_BUILDER = new JavaType( + "org.apache.commons.lang3.builder.HashCodeBuilder"); + private static final JavaSymbolName HASH_CODE_METHOD_NAME = new JavaSymbolName( + "hashCode"); + private static final String OBJECT_NAME = "obj"; + private static final String PROVIDES_TYPE_STRING = EqualsMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + /** + * Returns the class-level ID of this type of metadata + * + * @return a valid class-level MID + */ + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final EqualsAnnotationValues annotationValues; + private final List locatedFields; + + /** + * Constructor + * + * @param identifier the ID of this piece of metadata (required) + * @param aspectName the name of the ITD to generate (required) + * @param governorPhysicalTypeMetadata the details of the governor + * (required) + * @param annotationValues the values of the @RooEquals annotation + * (required) + * @param equalityFields the fields to be compared by the + * equals method (can be null or empty) + */ + public EqualsMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final EqualsAnnotationValues annotationValues, + final List equalityFields) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), "Metadata id '%s' is invalid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + + this.annotationValues = annotationValues; + locatedFields = equalityFields; + + if (!CollectionUtils.isEmpty(equalityFields)) { + builder.addMethod(getEqualsMethod()); + builder.addMethod(getHashCodeMethod()); + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Returns the equals method to be generated + * + * @return null if no generation is required + */ + private MethodMetadataBuilder getEqualsMethod() { + final JavaType parameterType = OBJECT; + if (governorHasMethod(EQUALS_METHOD_NAME, parameterType)) { + return null; + } + + final List parameterNames = Arrays + .asList(new JavaSymbolName(OBJECT_NAME)); + + builder.getImportRegistrationResolver().addImport(EQUALS_BUILDER); + + final String typeName = destination.getSimpleTypeName(); + + // Create the method + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (!(" + OBJECT_NAME + " instanceof " + + typeName + ")) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return false;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("if (this == " + OBJECT_NAME + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return true;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(typeName + " rhs = (" + typeName + ") " + + OBJECT_NAME + ";"); + + final StringBuilder builder = new StringBuilder( + "return new EqualsBuilder()"); + if (annotationValues.isAppendSuper()) { + builder.append(".appendSuper(super.equals(" + OBJECT_NAME + "))"); + } + for (final FieldMetadata field : locatedFields) { + builder.append(".append(" + field.getFieldName() + ", rhs." + + field.getFieldName() + ")"); + } + builder.append(".isEquals();"); + + bodyBuilder.appendFormalLine(builder.toString()); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + EQUALS_METHOD_NAME, BOOLEAN_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + /** + * Returns the hashCode method to be generated + * + * @return null if no generation is required + */ + private MethodMetadataBuilder getHashCodeMethod() { + if (governorHasMethod(HASH_CODE_METHOD_NAME)) { + return null; + } + + builder.getImportRegistrationResolver().addImport(HASH_CODE_BUILDER); + + // Create the method + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + final StringBuilder builder = new StringBuilder( + "return new HashCodeBuilder()"); + if (annotationValues.isAppendSuper()) { + builder.append(".appendSuper(super.hashCode())"); + } + for (final FieldMetadata field : locatedFields) { + builder.append(".append(" + field.getFieldName() + ")"); + } + builder.append(".toHashCode();"); + + bodyBuilder.appendFormalLine(builder.toString()); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + HASH_CODE_METHOD_NAME, INT_PRIMITIVE, bodyBuilder); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadataProvider.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadataProvider.java new file mode 100644 index 000000000..7becc63d3 --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadataProvider.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.equals; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link EqualsMetadata}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface EqualsMetadataProvider extends ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadataProviderImpl.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadataProviderImpl.java new file mode 100644 index 000000000..a88239844 --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsMetadataProviderImpl.java @@ -0,0 +1,151 @@ +package org.springframework.roo.addon.equals; + +import static org.springframework.roo.model.RooJavaType.ROO_EQUALS; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Implementation of {@link EqualsMetadataProvider}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class EqualsMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + EqualsMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_EQUALS); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return EqualsMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_EQUALS); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = EqualsMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = EqualsMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Equals"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + return getLocalMid(itdTypeDetails); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final EqualsAnnotationValues annotationValues = new EqualsAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound()) { + return null; + } + + final MemberDetails memberDetails = getMemberDetails(governorPhysicalTypeMetadata); + if (memberDetails == null) { + return null; + } + + final JavaType javaType = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails().getName(); + final List equalityFields = locateFields(javaType, + annotationValues.getExcludeFields(), memberDetails, + metadataIdentificationString); + + return new EqualsMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues, equalityFields); + } + + public String getProvidesType() { + return EqualsMetadata.getMetadataIdentiferType(); + } + + private List locateFields(final JavaType javaType, + final String[] excludeFields, final MemberDetails memberDetails, + final String metadataIdentificationString) { + final SortedSet locatedFields = new TreeSet( + new Comparator() { + public int compare(final FieldMetadata l, + final FieldMetadata r) { + return l.getFieldName().compareTo(r.getFieldName()); + } + }); + + final List excludeFieldsList = CollectionUtils + .arrayToList(excludeFields); + final FieldMetadata versionField = getPersistenceMemberLocator() + .getVersionField(javaType); + + for (final FieldMetadata field : memberDetails.getFields()) { + if (excludeFieldsList + .contains(field.getFieldName().getSymbolName())) { + continue; + } + if (Modifier.isStatic(field.getModifier()) + || Modifier.isTransient(field.getModifier()) + || field.getFieldType().isCommonCollectionType() + || field.getFieldType().isArray()) { + continue; + } + if (versionField != null + && field.getFieldName().equals(versionField.getFieldName())) { + continue; + } + + locatedFields.add(field); + getMetadataDependencyRegistry().registerDependency( + field.getDeclaredByMetadataId(), + metadataIdentificationString); + } + + return new ArrayList(locatedFields); + } +} diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsOperations.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsOperations.java new file mode 100644 index 000000000..df1ea6b8a --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsOperations.java @@ -0,0 +1,17 @@ +package org.springframework.roo.addon.equals; + +import java.util.Set; + +import org.springframework.roo.model.JavaType; + +/** + * Provides equals and hashCode method operations. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface EqualsOperations { + + void addEqualsAndHashCodeMethods(final JavaType javaType, + boolean appendSuper, final Set excludeFields); +} diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsOperationsImpl.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsOperationsImpl.java new file mode 100644 index 000000000..30516bc52 --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/EqualsOperationsImpl.java @@ -0,0 +1,66 @@ +package org.springframework.roo.addon.equals; + +import static org.springframework.roo.model.RooJavaType.ROO_EQUALS; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Implementation of {@link EqualsOperations}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class EqualsOperationsImpl implements EqualsOperations { + + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void addEqualsAndHashCodeMethods(final JavaType javaType, + final boolean appendSuper, final Set excludeFields) { + // Add @RooEquals annotation to class if not yet present + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(javaType); + if (cid == null || cid.getTypeAnnotation(ROO_EQUALS) != null) { + return; + } + + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + ROO_EQUALS); + if (appendSuper) { + annotationBuilder.addBooleanAttribute("appendSuper", appendSuper); + } + if (!CollectionUtils.isEmpty(excludeFields)) { + final List attributes = new ArrayList(); + for (final String excludeField : excludeFields) { + attributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), excludeField)); + } + annotationBuilder + .addAttribute(new ArrayAttributeValue( + new JavaSymbolName("excludeFields"), attributes)); + } + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + cidBuilder.addAnnotation(annotationBuilder.build()); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } +} diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/ExcludeFieldsConverter.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/ExcludeFieldsConverter.java new file mode 100644 index 000000000..58684510b --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/ExcludeFieldsConverter.java @@ -0,0 +1,46 @@ +package org.springframework.roo.addon.equals; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion between a space-separated list of field names to a set of + * field names. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class ExcludeFieldsConverter implements Converter> { + + public Set convertFromText(final String value, + final Class requiredType, final String optionContext) { + final Set fields = new LinkedHashSet(); + final StringTokenizer st = new StringTokenizer(value, " "); + while (st.hasMoreTokens()) { + fields.add(st.nextToken()); + } + return fields; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Set.class.isAssignableFrom(requiredType) + && optionContext.contains("exclude-fields"); + } +} diff --git a/addon-equals/src/main/java/org/springframework/roo/addon/equals/RooEquals.java b/addon-equals/src/main/java/org/springframework/roo/addon/equals/RooEquals.java new file mode 100644 index 000000000..4cecce230 --- /dev/null +++ b/addon-equals/src/main/java/org/springframework/roo/addon/equals/RooEquals.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.equals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Creates equals and hashCode methods. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooEquals { + + boolean appendSuper() default false; + + /** + * @return an array of fields exclude in the equals and hashCode methods + */ + String[] excludeFields() default ""; +} diff --git a/addon-finder/pom.xml b/addon-finder/pom.xml new file mode 100644 index 000000000..f1471925b --- /dev/null +++ b/addon-finder/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.finder + bundle + Spring Roo - Addon - Finder + Support for the generation of finder methods for JPA-backed entities. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.jpa + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/DynamicFinderServices.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/DynamicFinderServices.java new file mode 100644 index 000000000..2170dc8c4 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/DynamicFinderServices.java @@ -0,0 +1,67 @@ +package org.springframework.roo.addon.finder; + +import java.util.List; +import java.util.Set; + +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; + +/** + * The {@link DynamicFinderServices} is used for the generation of dynamic + * finder methods based on a {@link JavaSymbolName} which would look like, for + * example, 'findByFirstNameAndLastName'. This class will suggest possible + * finder combinations, create a query String and provide access to parameter + * types and names. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +public interface DynamicFinderServices { + + /** + * This method provides a convenient generator for all possible combinations + * of finder method signatures. This is used by the {@link FinderCommands} + * to locate possible finders the user may wish to install. + * + * @param memberDetails the {@link MemberDetails} object to search + * (required) + * @param plural the pluralised form of the entity name, which is used for + * finder method names (required) + * @param depth the depth of combinations used for finder signatures + * combinations (a depth of 2 will combine a maximum of two + * attributes from the member details (required) + * @param exclusions field names which should not be contained in the + * suggested finders + * @return immutable representation of all possible finder method signatures + * for the given depth (never returns null, but list may be empty) + */ + List getFinders(MemberDetails memberDetails, String plural, + int depth, Set exclusions); + + /** + * This method generates a {@link QueryHolder} object that consists of the: + *
      + *
    • named JPA query string to be used in JPA entity manager queries + *
    • parameter types used in the named JPA query + *
    • parameter names used in the named JPA query + *
    + * + * @param memberDetails the {@link MemberDetails} object to search + * (required) + * @param finderName the finder method signature to use (required; must be a + * valid signature) + * @param plural the pluralised form of the entity name, which is used for + * finder method names (required) + * @param entityName the name of the entity to be used in the Query + * @return a {@link QueryHolder} object containing all the attributes to be + * used in a JPA named query (null if the finder is unable to be + * built at this time) + */ + QueryHolder getQueryHolder(MemberDetails memberDetails, + JavaSymbolName finderName, String plural, String entityName); + + QueryHolder getCountQueryHolder(MemberDetails memberDetails, + JavaSymbolName finderName, String plural, String entityName); +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/DynamicFinderServicesImpl.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/DynamicFinderServicesImpl.java new file mode 100644 index 000000000..d152c9a95 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/DynamicFinderServicesImpl.java @@ -0,0 +1,558 @@ +package org.springframework.roo.addon.finder; + +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JdkJavaType.MAP; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Default implementation of {@link DynamicFinderServices}. + * + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class DynamicFinderServicesImpl implements DynamicFinderServices { + + private Set createFinders(final FieldMetadata field, + final Set finders, final String prepend, + final boolean isFirst) { + final Set tempFinders = new HashSet(); + + if (isNumberOrDate(field.getFieldType())) { + for (final ReservedToken keyWord : ReservedTokenHolder.NUMERIC_TOKENS) { + tempFinders.addAll(populateFinders(finders, field, prepend, + isFirst, keyWord.getValue())); + } + } + else if (field.getFieldType().equals(JavaType.STRING)) { + for (final ReservedToken keyWord : ReservedTokenHolder.STRING_TOKENS) { + tempFinders.addAll(populateFinders(finders, field, prepend, + isFirst, keyWord.getValue())); + } + } + else if (field.getFieldType().equals(JavaType.BOOLEAN_OBJECT) + || field.getFieldType().equals(JavaType.BOOLEAN_PRIMITIVE)) { + for (final ReservedToken keyWord : ReservedTokenHolder.BOOLEAN_TOKENS) { + tempFinders.addAll(populateFinders(finders, field, prepend, + isFirst, keyWord.getValue())); + } + } + else { + tempFinders.addAll(populateFinders(finders, field, prepend, + isFirst, "")); + } + + return tempFinders; + } + + /** + * Returns the {@link JavaType} from the specified {@link MemberDetails} + * object; + *

    + * If the found type is abstract the next {@link MemberHoldingTypeDetails} + * is searched. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @return the first non-abstract JavaType, or null if not found + */ + private JavaType getConcreteJavaType(final MemberDetails memberDetails) { + Validate.notNull(memberDetails, "Member details required"); + JavaType javaType = null; + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails + .getDetails()) { + if (Modifier.isAbstract(memberHoldingTypeDetails.getModifier())) { + continue; + } + javaType = memberHoldingTypeDetails.getName(); + } + return javaType; + } + + public List getFinders(final MemberDetails memberDetails, + final String plural, final int depth, + final Set exclusions) { + Validate.notNull(memberDetails, "Member details required"); + Validate.notBlank(plural, "Plural required"); + Validate.notNull(depth, + "The depth of combinations used for finder signatures combinations required"); + Validate.notNull(exclusions, "Exclusions required"); + + final SortedSet finders = new TreeSet(); + + final List fields = memberDetails.getFields(); + for (int i = 0; i < depth; i++) { + final SortedSet tempFinders = new TreeSet(); + for (final FieldMetadata field : fields) { + // Ignoring java.util.Map field types (see ROO-194) + if (field == null || field.getFieldType().equals(MAP)) { + continue; + } + if (exclusions.contains(field.getFieldName())) { + continue; + } + if (i == 0) { + tempFinders.addAll(createFinders(field, finders, "find" + + plural + "By", true)); + } + else { + tempFinders.addAll(createFinders(field, finders, "And", + false)); + tempFinders.addAll(createFinders(field, finders, "Or", + false)); + } + } + finders.addAll(tempFinders); + } + + return Collections.unmodifiableList(new ArrayList( + finders)); + } + + private Token getFirstToken(final SortedSet fieldTokens, + final String finder, final String originalFinder, + final String simpleTypeName) { + for (final FieldToken fieldToken : fieldTokens) { + if (finder.startsWith(fieldToken.getValue())) { + return fieldToken; + } + } + for (final ReservedToken reservedToken : ReservedTokenHolder.ALL_TOKENS) { + if (finder.startsWith(reservedToken.getValue())) { + return reservedToken; + } + } + if (finder.length() > 0) { + // TODO: Make this a FinderFieldTokenMissingException instead, to + // make it easier to detect this + throw new FinderFieldTokenMissingException( + "Dynamic finder is unable to match '" + finder + + "' token of '" + originalFinder + + "' finder definition in " + simpleTypeName + + ".java"); + } + + return null; // Finder does not start with reserved or field token + } + + private String getJpaQuery(final List tokens, + final String simpleTypeName, final JavaSymbolName finderName, + final String plural, final String entityName) { + final String typeName = StringUtils.defaultIfEmpty(entityName, + simpleTypeName); + final StringBuilder builder = new StringBuilder(); + builder.append("SELECT o FROM ").append(typeName); + builder.append(" AS o WHERE "); + + FieldToken lastFieldToken = null; + boolean isNewField = true; + boolean isFieldApplied = false; + + for (final Token token : tokens) { + if (token instanceof ReservedToken) { + final String reservedToken = token.getValue(); + if (lastFieldToken == null) { + continue; + } + final String fieldName = lastFieldToken.getField() + .getFieldName().getSymbolName(); + boolean setField = true; + + if (!lastFieldToken.getField().getFieldType() + .isCommonCollectionType()) { + if (isNewField) { + if (reservedToken.equalsIgnoreCase("Like")) { + builder.append("LOWER(").append("o.") + .append(fieldName).append(')'); + } + else { + builder.append("o.").append(fieldName); + } + isNewField = false; + isFieldApplied = false; + } + if (reservedToken.equalsIgnoreCase("And")) { + if (!isFieldApplied) { + builder.append(" = :").append(fieldName); + isFieldApplied = true; + } + builder.append(" AND "); + setField = false; + } + else if (reservedToken.equalsIgnoreCase("Or")) { + if (!isFieldApplied) { + builder.append(" = :").append(fieldName); + isFieldApplied = true; + } + builder.append(" OR "); + setField = false; + } + else if (reservedToken.equalsIgnoreCase("Between")) { + builder.append(" BETWEEN ") + .append(":min") + .append(lastFieldToken.getField() + .getFieldName() + .getSymbolNameCapitalisedFirstLetter()) + .append(" AND ") + .append(":max") + .append(lastFieldToken.getField() + .getFieldName() + .getSymbolNameCapitalisedFirstLetter()) + .append(" "); + setField = false; + isFieldApplied = true; + } + else if (reservedToken.equalsIgnoreCase("Like")) { + builder.append(" LIKE "); + setField = true; + } + else if (reservedToken.equalsIgnoreCase("IsNotNull")) { + builder.append(" IS NOT NULL "); + setField = false; + isFieldApplied = true; + } + else if (reservedToken.equalsIgnoreCase("IsNull")) { + builder.append(" IS NULL "); + setField = false; + isFieldApplied = true; + } + else if (reservedToken.equalsIgnoreCase("Not")) { + builder.append(" IS NOT "); + } + else if (reservedToken.equalsIgnoreCase("NotEquals")) { + builder.append(" != "); + } + else if (reservedToken.equalsIgnoreCase("LessThan")) { + builder.append(" < "); + } + else if (reservedToken.equalsIgnoreCase("LessThanEquals")) { + builder.append(" <= "); + } + else if (reservedToken.equalsIgnoreCase("GreaterThan")) { + builder.append(" > "); + } + else if (reservedToken + .equalsIgnoreCase("GreaterThanEquals")) { + builder.append(" >= "); + } + else if (reservedToken.equalsIgnoreCase("Equals")) { + builder.append(" = "); + } + if (setField) { + if (builder.toString().endsWith("LIKE ")) { + builder.append("LOWER(:").append(fieldName) + .append(") "); + } + else { + builder.append(':').append(fieldName).append(' '); + } + isFieldApplied = true; + } + } + } + else { + lastFieldToken = (FieldToken) token; + isNewField = true; + } + } + if (isNewField) { + if (lastFieldToken != null + && !lastFieldToken.getField().getFieldType() + .isCommonCollectionType()) { + builder.append("o.").append( + lastFieldToken.getField().getFieldName() + .getSymbolName()); + } + isFieldApplied = false; + } + if (!isFieldApplied) { + if (lastFieldToken != null + && !lastFieldToken.getField().getFieldType() + .isCommonCollectionType()) { + builder.append(" = :").append( + lastFieldToken.getField().getFieldName() + .getSymbolName()); + } + } + return builder.toString().trim(); + } + + private String getJpaCountQuery(final List tokens, + final String simpleTypeName, final JavaSymbolName finderName, + final String plural, final String entityName) { + String jpaQuery = this.getJpaQuery(tokens, simpleTypeName, finderName, plural, entityName); + return jpaQuery.replaceFirst("SELECT o FROM ", "SELECT COUNT(o) FROM "); + } + + private List getLocatedMutators( + final MemberDetails memberDetails) { + final List locatedMutators = new ArrayList(); + for (final MethodMetadata method : memberDetails.getMethods()) { + if (isMethodOfInterest(method)) { + locatedMutators.add(method); + } + } + return locatedMutators; + } + + private List getParameterNames(final List tokens, + final JavaSymbolName finderName, final String plural) { + final List parameterNames = new ArrayList(); + + for (int i = 0; i < tokens.size(); i++) { + final Token token = tokens.get(i); + if (token instanceof FieldToken) { + final String fieldName = ((FieldToken) token).getField() + .getFieldName().getSymbolName(); + parameterNames.add(new JavaSymbolName(fieldName)); + } + else { + if ("Between".equals(token.getValue())) { + final Token field = tokens.get(i - 1); + if (field instanceof FieldToken) { + final JavaSymbolName fieldName = parameterNames + .get(parameterNames.size() - 1); + // Remove the last field token + parameterNames.remove(parameterNames.size() - 1); + + // Replace by a min and a max value + parameterNames + .add(new JavaSymbolName( + "min" + + fieldName + .getSymbolNameCapitalisedFirstLetter())); + parameterNames + .add(new JavaSymbolName( + "max" + + fieldName + .getSymbolNameCapitalisedFirstLetter())); + } + } + else if ("IsNull".equals(token.getValue()) + || "IsNotNull".equals(token.getValue())) { + final Token field = tokens.get(i - 1); + if (field instanceof FieldToken) { + parameterNames.remove(parameterNames.size() - 1); + } + } + } + } + + return parameterNames; + } + + private List getParameterTypes(final List tokens, + final JavaSymbolName finderName, final String plural) { + final List parameterTypes = new ArrayList(); + + for (int i = 0; i < tokens.size(); i++) { + final Token token = tokens.get(i); + if (token instanceof FieldToken) { + parameterTypes.add(((FieldToken) token).getField() + .getFieldType()); + } + else { + if ("Between".equals(token.getValue())) { + final Token field = tokens.get(i - 1); + if (field instanceof FieldToken) { + parameterTypes.add(parameterTypes.get(parameterTypes + .size() - 1)); + } + } + else if ("IsNull".equals(token.getValue()) + || "IsNotNull".equals(token.getValue())) { + final Token field = tokens.get(i - 1); + if (field instanceof FieldToken) { + parameterTypes.remove(parameterTypes.size() - 1); + } + } + } + } + return parameterTypes; + } + + public QueryHolder getQueryHolder(final MemberDetails memberDetails, + final JavaSymbolName finderName, final String plural, + final String entityName) { + Validate.notNull(memberDetails, "Member details required"); + Validate.notNull(finderName, "Finder name required"); + Validate.notBlank(plural, "Plural required"); + + List tokens; + try { + tokens = tokenize(memberDetails, finderName, plural); + } + catch (final FinderFieldTokenMissingException e) { + return null; + } + catch (final InvalidFinderException e) { + return null; + } + + final String simpleTypeName = getConcreteJavaType(memberDetails) + .getSimpleTypeName(); + final String jpaQuery = getJpaQuery(tokens, simpleTypeName, finderName, + plural, entityName); + final List parameterTypes = getParameterTypes(tokens, + finderName, plural); + final List parameterNames = getParameterNames(tokens, + finderName, plural); + return new QueryHolder(jpaQuery, parameterTypes, parameterNames, tokens); + } + + public QueryHolder getCountQueryHolder(final MemberDetails memberDetails, + final JavaSymbolName finderName, final String plural, + final String entityName) { + Validate.notNull(memberDetails, "Member details required"); + Validate.notNull(finderName, "Finder name required"); + Validate.notBlank(plural, "Plural required"); + + List tokens; + try { + tokens = tokenize(memberDetails, finderName, plural); + } + catch (final FinderFieldTokenMissingException e) { + return null; + } + catch (final InvalidFinderException e) { + return null; + } + + final String simpleTypeName = getConcreteJavaType(memberDetails) + .getSimpleTypeName(); + final String jpaQuery = getJpaCountQuery(tokens, simpleTypeName, finderName, + plural, entityName); + final List parameterTypes = getParameterTypes(tokens, + finderName, plural); + final List parameterNames = getParameterNames(tokens, + finderName, plural); + return new QueryHolder(jpaQuery, parameterTypes, parameterNames, tokens); + } + + private boolean isMethodOfInterest(final MethodMetadata method) { + return method.getMethodName().getSymbolName().startsWith("set") + && method.getModifier() == Modifier.PUBLIC; + } + + private boolean isNumberOrDate(final JavaType fieldType) { + return fieldType.equals(JavaType.DOUBLE_OBJECT) + || fieldType.equals(JavaType.FLOAT_OBJECT) + || fieldType.equals(JavaType.INT_OBJECT) + || fieldType.equals(LONG_OBJECT) + || fieldType.equals(JavaType.SHORT_OBJECT) + || fieldType.getFullyQualifiedTypeName().equals( + Date.class.getName()) + || fieldType.getFullyQualifiedTypeName().equals( + Calendar.class.getName()); + } + + private boolean isTransient(final FieldMetadata field) { + return Modifier.isTransient(field.getModifier()) + || field.getCustomData().keySet() + .contains(CustomDataKeys.TRANSIENT_FIELD); + } + + private Set populateFinders( + final Set finders, final FieldMetadata field, + final String prepend, final boolean isFirst, final String keyWord) { + final Set tempFinders = new HashSet(); + + if (isTransient(field)) { + // No need to add transient fields + } + else if (isFirst) { + final String finderName = prepend + + field.getFieldName() + .getSymbolNameCapitalisedFirstLetter() + keyWord; + tempFinders.add(new JavaSymbolName(finderName)); + } + else { + for (final JavaSymbolName finder : finders) { + final String finderName = finder.getSymbolName(); + if (!finderName.contains(field.getFieldName() + .getSymbolNameCapitalisedFirstLetter())) { + tempFinders.add(new JavaSymbolName(finderName + + prepend + + field.getFieldName() + .getSymbolNameCapitalisedFirstLetter() + + keyWord)); + } + } + } + + return tempFinders; + } + + private List tokenize(final MemberDetails memberDetails, + final JavaSymbolName finderName, final String plural) { + final String simpleTypeName = getConcreteJavaType(memberDetails) + .getSimpleTypeName(); + String finder = finderName.getSymbolName(); + + // Just in case it starts with findBy we can remove it here + final String findBy = "find" + plural + "By"; + if (finder.startsWith(findBy)) { + finder = finder.substring(findBy.length()); + } + + // If finder still contains the findBy sequence it is most likely a + // wrong finder (ie someone pasted the finder string accidentally twice + if (finder.contains(findBy)) { + throw new InvalidFinderException("Dynamic finder definition for '" + + finderName.getSymbolName() + "' in " + simpleTypeName + + ".java is invalid"); + } + + final SortedSet fieldTokens = new TreeSet(); + for (final MethodMetadata method : getLocatedMutators(memberDetails)) { + final FieldMetadata field = BeanInfoUtils.getFieldForPropertyName( + memberDetails, method.getParameterNames().get(0)); + + // If we did find a field matching the first parameter name of the + // mutator method we can add it to the finder ITD + if (field != null) { + fieldTokens.add(new FieldToken(field)); + } + } + + final List tokens = new ArrayList(); + + while (finder.length() > 0) { + final Token token = getFirstToken(fieldTokens, finder, + finderName.getSymbolName(), simpleTypeName); + if (token != null) { + if (token instanceof FieldToken + || token instanceof ReservedToken) { + tokens.add(token); + } + finder = finder.substring(token.getValue().length()); + } + } + + return tokens; + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FieldToken.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FieldToken.java new file mode 100644 index 000000000..2a1545c1a --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FieldToken.java @@ -0,0 +1,50 @@ +package org.springframework.roo.addon.finder; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Token which represents a field in an JPA Entity + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +public class FieldToken implements Token, Comparable { + + private final FieldMetadata field; + private JavaSymbolName fieldName; + + /** + * Constructor + * + * @param field + */ + public FieldToken(final FieldMetadata field) { + Validate.notNull(field, "FieldMetadata required"); + this.field = field; + fieldName = field.getFieldName(); + } + + public int compareTo(final FieldToken o) { + final int l = o.getValue().length() - getValue().length(); + return l == 0 ? -1 : l; + } + + public FieldMetadata getField() { + return field; + } + + public JavaSymbolName getFieldName() { + return fieldName; + } + + public String getValue() { + return field.getFieldName().getSymbolNameCapitalisedFirstLetter(); + } + + public void setFieldName(final JavaSymbolName fieldName) { + this.fieldName = fieldName; + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderCommands.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderCommands.java new file mode 100644 index 000000000..dc464e9d8 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderCommands.java @@ -0,0 +1,84 @@ +package org.springframework.roo.addon.finder; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import java.util.HashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.text.StrTokenizer; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'finder' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class FinderCommands implements CommandMarker { + + @Reference private FinderOperations finderOperations; + + @CliCommand(value = "finder add", help = "Install finders in the given target (must be an entity)") + public void installFinders( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The controller or entity for which the finders are generated") final JavaType typeName, + @CliOption(key = { "finderName", "" }, mandatory = true, help = "The finder string as generated with the 'finder list' command") final JavaSymbolName finderName) { + + finderOperations.installFinder(typeName, finderName); + } + + @CliAvailabilityIndicator({ "finder list", "finder add" }) + public boolean isFinderCommandAvailable() { + return finderOperations.isFinderInstallationPossible(); + } + + @CliCommand(value = "finder list", help = "List all finders for a given target (must be an entity)") + public SortedSet listFinders( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The controller or entity for which the finders are generated") final JavaType typeName, + @CliOption(key = { "", "depth" }, mandatory = false, unspecifiedDefaultValue = "1", specifiedDefaultValue = "1", help = "The depth of attribute combinations to be generated for the finders") final Integer depth, + @CliOption(key = "filter", mandatory = false, help = "A comma separated list of strings that must be present in a filter to be included") final String filter) { + + Validate.isTrue(depth >= 1, "Depth must be at least 1"); + Validate.isTrue(depth <= 3, "Depth must not be greater than 3"); + + final SortedSet finders = finderOperations.listFindersFor( + typeName, depth); + if (StringUtils.isBlank(filter)) { + return finders; + } + + final Set requiredEntries = new HashSet(); + final String[] filterTokens = new StrTokenizer(filter, ",") + .getTokenArray(); + for (final String requiredString : filterTokens) { + requiredEntries.add(requiredString.toLowerCase()); + } + if (requiredEntries.isEmpty()) { + return finders; + } + + final SortedSet result = new TreeSet(); + for (final String finder : finders) { + required: for (final String requiredEntry : requiredEntries) { + if (finder.toLowerCase().contains(requiredEntry)) { + result.add(finder); + break required; + } + } + } + return result; + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderFieldTokenMissingException.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderFieldTokenMissingException.java new file mode 100644 index 000000000..8876d38fd --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderFieldTokenMissingException.java @@ -0,0 +1,16 @@ +package org.springframework.roo.addon.finder; + +/** + * Thrown when a dynamic finder method cannot be matched. + * + * @author Alan Stewart + * @since 1.1.2 + */ +public class FinderFieldTokenMissingException extends RuntimeException { + + private static final long serialVersionUID = 2328865678880608749L; + + public FinderFieldTokenMissingException(final String string) { + super(string); + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadata.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadata.java new file mode 100644 index 000000000..feb52ed7c --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadata.java @@ -0,0 +1,418 @@ +package org.springframework.roo.addon.finder; + +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JpaJavaType.ENTITY_MANAGER; +import static org.springframework.roo.model.JpaJavaType.TYPED_QUERY; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooJpaActiveRecord#finders()}. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +public class FinderMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = FinderMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final List dynamicFinderMethods = new ArrayList(); + + private Map queryHolders; + + public FinderMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final MethodMetadata entityManagerMethod, + final Map queryHolders) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.isTrue(entityManagerMethod != null || queryHolders.isEmpty(), + "EntityManager method required if any query holders are provided"); + Validate.notNull(queryHolders, "Query holders required"); + + if (!isValid()) { + return; + } + + this.queryHolders = queryHolders; + + for (final JavaSymbolName finderName : queryHolders.keySet()) { + + // finder and count + final MethodMetadataBuilder methodBuilder = getDynamicFinderMethod( + finderName, entityManagerMethod, false); + builder.addMethod(methodBuilder); + dynamicFinderMethods.add(methodBuilder.build()); + + // sorted finder + if(!finderName.getSymbolName().startsWith("count")) { + final MethodMetadataBuilder methodBuilderSorted = getDynamicFinderMethod( + finderName, entityManagerMethod, true); + builder.addMethod(methodBuilderSorted); + dynamicFinderMethods.add(methodBuilderSorted.build()); + } + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Obtains all the currently-legal dynamic finders known to this metadata + * instance. This may be a subset (or even completely empty) versus those + * requested via the {@link RooJpaActiveRecord} annotation, as the user may + * have made a typing error in representing the requested dynamic finder, + * the field may have been deleted by the user, or an add-on which produces + * the field (or its mutator) might not yet be loaded or in error or other + * similar conditions. + * + * @return a non-null, immutable representation of currently-available + * finder methods (never returns null, but may be empty) + */ + public List getAllDynamicFinders() { + return Collections.unmodifiableList(dynamicFinderMethods); + } + + /** + * Locates a dynamic finder method of the specified name, or creates one on + * demand if not present. + *

    + * It is required that the requested name was defined in the + * {@link RooJpaActiveRecord#finders()}. If it is not present, an exception + * is thrown. + * + * @param finderName the dynamic finder method name + * @param entityManagerMethod required + * @return the user-defined method, or an ITD-generated method (never + * returns null) + */ + private MethodMetadataBuilder getDynamicFinderMethod( + final JavaSymbolName finderName, + final MethodMetadata entityManagerMethod, final Boolean sorted) { + Validate.notNull(finderName, "Dynamic finder method name is required"); + Validate.isTrue(queryHolders.containsKey(finderName), + "Undefined method name '%s'", finderName.getSymbolName()); + + + // To get this far we need to create the method... + final List parameters = new ArrayList(); + parameters.add(destination); + JavaType typedQueryType = new JavaType( + TYPED_QUERY.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, parameters); + if(finderName.getSymbolName().startsWith("count")) { + typedQueryType = new JavaType("Long"); + } + + final QueryHolder queryHolder = queryHolders.get(finderName); + final String jpaQuery = queryHolder.getJpaQuery(); + final List parameterTypes = queryHolder.getParameterTypes(); + final List parameterNames = queryHolder + .getParameterNames(); + + + // Now we have parameters types, we can scan by name + // AND with parameters types + // We do not scan the superclass, as the caller is expected to know + // we'll only scan the current class + List parameterTypes4Test = new ArrayList(parameterTypes); + if(!finderName.getSymbolName().startsWith("count") && sorted) { + parameterTypes4Test.add(STRING); + parameterTypes4Test.add(STRING); + } + final MethodMetadata userMethod = MemberFindingUtils.getDeclaredMethod(governorTypeDetails, + finderName, parameterTypes4Test); + if (userMethod != null) { + return new MethodMetadataBuilder(userMethod); + } + + + // We declared the field in this ITD, so produce a public accessor for + // it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String methodName = finderName.getSymbolName(); + boolean containsCollectionType = false; + + for (int i = 0; i < parameterTypes.size(); i++) { + final String name = parameterNames.get(i).getSymbolName(); + + final StringBuilder length = new StringBuilder(); + if (parameterTypes.get(i).equals(STRING)) { + length.append(" || ").append(parameterNames.get(i)) + .append(".length() == 0"); + } + + if (!parameterTypes.get(i).isPrimitive()) { + bodyBuilder.appendFormalLine("if (" + name + " == null" + + length.toString() + + ") throw new IllegalArgumentException(\"The " + name + + " argument is required\");"); + } + + if (length.length() > 0 + && methodName.substring( + methodName.indexOf(parameterNames.get(i) + .getSymbolNameCapitalisedFirstLetter()) + + name.length()).startsWith("Like")) { + bodyBuilder.appendFormalLine(name + " = " + name + + ".replace('*', '%');"); + bodyBuilder.appendFormalLine("if (" + name + + ".charAt(0) != '%') {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(name + " = \"%\" + " + name + ";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("if (" + name + ".charAt(" + name + + ".length() - 1) != '%') {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(name + " = " + name + " + \"%\";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + + if (parameterTypes.get(i).isCommonCollectionType()) { + containsCollectionType = true; + } + } + + + // Get the entityManager() method (as per ROO-216) + bodyBuilder.appendFormalLine(ENTITY_MANAGER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()) + + " em = " + + destination.getSimpleTypeName() + + "." + + entityManagerMethod.getMethodName().getSymbolName() + "();"); + + String typeNameIncludingTypeParameters = typedQueryType.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + String typeName = destination.getSimpleTypeName(); + if(methodName.startsWith("count")) { + final List parametersCount = new ArrayList(); + parameters.add(JavaType.LONG_OBJECT); + JavaType typedQueryTypeCount = new JavaType( + TYPED_QUERY.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, parametersCount); + typeNameIncludingTypeParameters = typedQueryTypeCount.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + typeName = "Long"; + } + + final List collectionTypeNames = new ArrayList(); + if (containsCollectionType) { + bodyBuilder + .appendFormalLine("StringBuilder queryBuilder = new StringBuilder(\"" + + jpaQuery + "\");"); + boolean jpaQueryComplete = false; + for (int i = 0; i < parameterTypes.size(); i++) { + if (!jpaQueryComplete && !jpaQuery.trim().endsWith("WHERE") + && !jpaQuery.trim().endsWith("AND") + && !jpaQuery.trim().endsWith("OR")) { + bodyBuilder.appendFormalLine("queryBuilder.append(\"" + + (methodName.substring( + methodName.toLowerCase().indexOf( + parameterNames.get(i) + .getSymbolName() + .toLowerCase()) + + parameterNames.get(i) + .getSymbolName().length()) + .startsWith("And") ? " AND" : " OR") + + "\");"); + jpaQueryComplete = true; + } + if (parameterTypes.get(i).isCommonCollectionType()) { + collectionTypeNames.add(parameterNames.get(i)); + } + } + int position = 0; + for (final JavaSymbolName name : collectionTypeNames) { + bodyBuilder.appendFormalLine("for (int i = 0; i < " + name + + ".size(); i++) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("if (i > 0) queryBuilder.append(\" AND\");"); + bodyBuilder.appendFormalLine("queryBuilder.append(\" :" + name + + "_item\").append(i).append(\" MEMBER OF o." + name + + "\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + if (collectionTypeNames.size() > ++position) { + bodyBuilder.appendFormalLine("queryBuilder.append(\"" + + (methodName.substring( + methodName.toLowerCase().indexOf( + name.getSymbolName().toLowerCase()) + + name.getSymbolName().length()) + .startsWith("And") ? " AND" : " OR") + + "\");"); + } + } + + // sorting part + if(!methodName.startsWith("count") && sorted) { + bodyBuilder.appendFormalLine("if (fieldNames4OrderClauseFilter.contains(sortFieldName)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("queryBuilder.append(\" ORDER BY \").append(sortFieldName);"); + bodyBuilder.appendFormalLine("if (\"ASC\".equalsIgnoreCase(sortOrder) || \"DESC\".equalsIgnoreCase(sortOrder)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("queryBuilder.append(\" \" + sortOrder);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + + bodyBuilder.appendFormalLine(typeNameIncludingTypeParameters + + " q = em.createQuery(queryBuilder.toString(), " + + typeName + ".class);"); + + for (int i = 0; i < parameterTypes.size(); i++) { + if (parameterTypes.get(i).isCommonCollectionType()) { + bodyBuilder.appendFormalLine("int " + parameterNames.get(i) + + "Index = 0;"); + bodyBuilder.appendFormalLine("for (" + + parameterTypes.get(i).getParameters().get(0) + .getSimpleTypeName() + + " _" + + parameterTypes.get(i).getParameters().get(0) + .getSimpleTypeName().toLowerCase() + ": " + + parameterNames.get(i) + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("q.setParameter(\"" + + parameterNames.get(i) + + "_item\" + " + + parameterNames.get(i) + + "Index++, _" + + parameterTypes.get(i).getParameters().get(0) + .getSimpleTypeName().toLowerCase() + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else { + bodyBuilder.appendFormalLine("q.setParameter(\"" + + parameterNames.get(i) + "\", " + + parameterNames.get(i) + ");"); + } + } + } + else { + // sorting part + if(!methodName.startsWith("count") && sorted) { + bodyBuilder + .appendFormalLine("StringBuilder queryBuilder = new StringBuilder(\"" + + jpaQuery + "\");"); + bodyBuilder.appendFormalLine("if (fieldNames4OrderClauseFilter.contains(sortFieldName)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("queryBuilder.append(\" ORDER BY \").append(sortFieldName);"); + bodyBuilder.appendFormalLine("if (\"ASC\".equalsIgnoreCase(sortOrder) || \"DESC\".equalsIgnoreCase(sortOrder)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("queryBuilder.append(\" \").append(sortOrder);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(typeNameIncludingTypeParameters + + " q = em.createQuery(queryBuilder.toString(), " + + typeName + ".class);"); + } else { + bodyBuilder.appendFormalLine(typeNameIncludingTypeParameters + + " q = em.createQuery(\"" + + jpaQuery + + "\", " + + typeName + ".class);"); + } + + for (final JavaSymbolName name : parameterNames) { + bodyBuilder.appendFormalLine("q.setParameter(\"" + name + + "\", " + name + ");"); + } + } + + if(methodName.startsWith("count")) { + bodyBuilder.appendFormalLine("return ((Long) q.getSingleResult());"); + } else { + bodyBuilder.appendFormalLine("return q;"); + } + + List methodParameterTypes = new ArrayList(parameterTypes); + List methodParameterNames = new ArrayList(parameterNames); + + // sort parameters : sortFieldName & sortOrder + if(!methodName.startsWith("count") && sorted) { + methodParameterTypes.add(STRING); + methodParameterTypes.add(STRING); + methodParameterNames.add(new JavaSymbolName("sortFieldName")); + methodParameterNames.add(new JavaSymbolName("sortOrder")); + } + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC + | Modifier.STATIC, finderName, typedQueryType, + AnnotatedJavaType.convertFromJavaTypes(methodParameterTypes), + methodParameterNames, bodyBuilder); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadataProvider.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadataProvider.java new file mode 100644 index 000000000..2887ca8d3 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadataProvider.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.finder; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link FinderMetadata}. + * + * @author Alan Stewart + * @since 1.1.2 + */ +public interface FinderMetadataProvider extends ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadataProviderImpl.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadataProviderImpl.java new file mode 100644 index 000000000..73efe32c8 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderMetadataProviderImpl.java @@ -0,0 +1,207 @@ +package org.springframework.roo.addon.finder; + +import java.util.Collections; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link FinderMetadataProvider}. + * + * @author Stefan Schmidt + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class FinderMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + FinderMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(FinderMetadataProviderImpl.class); + + private DynamicFinderServices dynamicFinderServices; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + // Ignoring trigger annotations means that other MD providers that want + // to discover whether a type has finders can do so. + setIgnoreTriggerAnnotations(true); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return FinderMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = FinderMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = FinderMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Finder"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + return getLocalMid(itdTypeDetails); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(dynamicFinderServices == null){ + dynamicFinderServices = getDynamicFinderServices(); + } + Validate.notNull(dynamicFinderServices, "DynamicFinderServices is required"); + + // We know governor type details are non-null and can be safely cast + + // Work out the MIDs of the other metadata we depend on + final JavaType javaType = FinderMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = FinderMetadata + .getPath(metadataIdentificationString); + final String jpaActiveRecordMetadataKey = JpaActiveRecordMetadata + .createIdentifier(javaType, path); + + // We need to lookup the metadata we depend on + final JpaActiveRecordMetadata jpaActiveRecordMetadata = (JpaActiveRecordMetadata) getMetadataService() + .get(jpaActiveRecordMetadataKey); + if (jpaActiveRecordMetadata == null + || !jpaActiveRecordMetadata.isValid()) { + return new FinderMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, null, + Collections. emptyMap()); + } + final MethodMetadata entityManagerMethod = jpaActiveRecordMetadata + .getEntityManagerMethod(); + if (entityManagerMethod == null) { + return null; + } + + final MemberDetails memberDetails = getMemberDetails(governorPhysicalTypeMetadata); + if (memberDetails == null) { + return null; + } + + final String plural = jpaActiveRecordMetadata.getPlural(); + final String entityName = jpaActiveRecordMetadata.getEntityName(); + + // Using SortedMap to ensure that the ITD emits finders in the same + // order each time + final SortedMap queryHolders = new TreeMap(); + for (final String methodName : jpaActiveRecordMetadata + .getDynamicFinders()) { + final JavaSymbolName finderName = new JavaSymbolName(methodName); + final QueryHolder queryHolder = dynamicFinderServices + .getQueryHolder(memberDetails, finderName, plural, + entityName); + if (queryHolder != null) { + queryHolders.put(finderName, queryHolder); + } + + char[] methodNameArray = methodName.toCharArray(); + methodNameArray[0] = Character.toUpperCase(methodNameArray[0]); + + final JavaSymbolName countFinderName = new JavaSymbolName("count" + new String(methodNameArray)); + final QueryHolder countQueryHolder = dynamicFinderServices + .getCountQueryHolder(memberDetails, finderName, plural, + entityName); + if (countQueryHolder != null) { + queryHolders.put(countFinderName, countQueryHolder); + } + } + + // Now determine all the ITDs we're relying on to ensure we are notified + // if they change + for (final QueryHolder queryHolder : queryHolders.values()) { + for (final Token token : queryHolder.getTokens()) { + if (token instanceof FieldToken) { + final FieldToken fieldToken = (FieldToken) token; + final String declaredByMid = fieldToken.getField() + .getDeclaredByMetadataId(); + getMetadataDependencyRegistry().registerDependency( + declaredByMid, metadataIdentificationString); + } + } + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + jpaActiveRecordMetadataKey, metadataIdentificationString); + + // We make the queryHolders immutable in case FinderMetadata in the + // future makes it available through an accessor etc + return new FinderMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, entityManagerMethod, + Collections.unmodifiableSortedMap(queryHolders)); + } + + public String getProvidesType() { + return FinderMetadata.getMetadataIdentiferType(); + } + + public DynamicFinderServices getDynamicFinderServices(){ + // Get all Services implement DynamicFinderServices interface + try { + ServiceReference[] references = context.getAllServiceReferences(DynamicFinderServices.class.getName(), null); + + for(ServiceReference ref : references){ + return (DynamicFinderServices) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load DynamicFinderServices on FinderMetadataProviderImpl."); + return null; + } + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderOperations.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderOperations.java new file mode 100644 index 000000000..649e25999 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderOperations.java @@ -0,0 +1,21 @@ +package org.springframework.roo.addon.finder; + +import java.util.SortedSet; + +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Provides Finder add-on operations. + * + * @author Ben Alex + * @since 1.0 + */ +public interface FinderOperations { + + void installFinder(JavaType typeName, JavaSymbolName finderName); + + boolean isFinderInstallationPossible(); + + SortedSet listFindersFor(JavaType typeName, Integer depth); +} \ No newline at end of file diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderOperationsImpl.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderOperationsImpl.java new file mode 100644 index 000000000..a7925917a --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/FinderOperationsImpl.java @@ -0,0 +1,443 @@ +package org.springframework.roo.addon.finder; + +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link FinderOperations}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class FinderOperationsImpl implements FinderOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(FinderOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + private DynamicFinderServices dynamicFinderServices; + private MemberDetailsScanner memberDetailsScanner; + private MetadataService metadataService; + private PersistenceMemberLocator persistenceMemberLocator; + private ProjectOperations projectOperations; + private TypeLocationService typeLocationService; + private TypeManagementService typeManagementService; + + private String getErrorMsg() { + return "Annotation " + ROO_JPA_ACTIVE_RECORD.getSimpleTypeName() + + " attribute 'finders' must be an array of strings"; + } + + public void installFinder(final JavaType typeName, + final JavaSymbolName finderName) { + Validate.notNull(typeName, "Java type required"); + Validate.notNull(finderName, "Finer name required"); + + final String id = getTypeLocationService() + .getPhysicalTypeIdentifier(typeName); + if (id == null) { + LOGGER.warning("Cannot locate source for '" + + typeName.getFullyQualifiedTypeName() + "'"); + return; + } + + // Go and get the entity metadata, as any type with finders has to be an + // entity + final JavaType javaType = PhysicalTypeIdentifier.getJavaType(id); + final LogicalPath path = PhysicalTypeIdentifier.getPath(id); + final String entityMid = JpaActiveRecordMetadata.createIdentifier( + javaType, path); + + // Get the entity metadata + final JpaActiveRecordMetadata jpaActiveRecordMetadata = (JpaActiveRecordMetadata) getMetadataService() + .get(entityMid); + if (jpaActiveRecordMetadata == null) { + LOGGER.warning("Cannot provide finders because '" + + typeName.getFullyQualifiedTypeName() + + "' is not an entity - " + entityMid); + return; + } + + // We know the file exists, as there's already entity metadata for it + final ClassOrInterfaceTypeDetails cid = getTypeLocationService() + .getTypeDetails(id); + if (cid == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + javaType.getFullyQualifiedTypeName() + "'"); + } + + // We know there should be an existing RooEntity annotation + final List annotations = cid + .getAnnotations(); + final AnnotationMetadata jpaActiveRecordAnnotation = MemberFindingUtils + .getAnnotationOfType(annotations, ROO_JPA_ACTIVE_RECORD); + if (jpaActiveRecordAnnotation == null) { + LOGGER.warning("Unable to find the entity annotation on '" + + typeName.getFullyQualifiedTypeName() + "'"); + return; + } + + // Confirm they typed a valid finder name + final MemberDetails memberDetails = getMemberDetailsScanner() + .getMemberDetails(getClass().getName(), cid); + if (getDynamicFinderServices().getQueryHolder(memberDetails, finderName, + jpaActiveRecordMetadata.getPlural(), + jpaActiveRecordMetadata.getEntityName()) == null) { + LOGGER.warning("Finder name '" + finderName.getSymbolName() + + "' either does not exist or contains an error"); + return; + } + + // Make a destination list to store our final attributes + final List> attributes = new ArrayList>(); + final List desiredFinders = new ArrayList(); + + // Copy the existing attributes, excluding the "finder" attribute + boolean alreadyAdded = false; + final AnnotationAttributeValue val = jpaActiveRecordAnnotation + .getAttribute(new JavaSymbolName("finders")); + if (val != null) { + // Ensure we have an array of strings + if (!(val instanceof ArrayAttributeValue)) { + LOGGER.warning(getErrorMsg()); + return; + } + final ArrayAttributeValue arrayVal = (ArrayAttributeValue) val; + for (final Object o : arrayVal.getValue()) { + if (!(o instanceof StringAttributeValue)) { + LOGGER.warning(getErrorMsg()); + return; + } + final StringAttributeValue sv = (StringAttributeValue) o; + if (sv.getValue().equals(finderName.getSymbolName())) { + alreadyAdded = true; + } + desiredFinders.add(sv); + } + } + + // Add the desired finder to the end + if (!alreadyAdded) { + desiredFinders.add(new StringAttributeValue(new JavaSymbolName( + "ignored"), finderName.getSymbolName())); + } + + // Now let's add the "finders" attribute + attributes.add(new ArrayAttributeValue( + new JavaSymbolName("finders"), desiredFinders)); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + final AnnotationMetadataBuilder annotation = new AnnotationMetadataBuilder( + ROO_JPA_ACTIVE_RECORD, attributes); + cidBuilder.updateTypeAnnotation(annotation.build(), + new HashSet()); + getTypeManagementService().createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public boolean isFinderInstallationPossible() { + return getProjectOperations().isFocusedProjectAvailable() + && getProjectOperations() + .isFeatureInstalledInFocusedModule(FeatureNames.JPA); + } + + public SortedSet listFindersFor(final JavaType typeName, + final Integer depth) { + Validate.notNull(typeName, "Java type required"); + + final String id = getTypeLocationService() + .getPhysicalTypeIdentifier(typeName); + if (id == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + typeName.getFullyQualifiedTypeName() + "'"); + } + + // Go and get the entity metadata, as any type with finders has to be an + // entity + final JavaType javaType = PhysicalTypeIdentifier.getJavaType(id); + final LogicalPath path = PhysicalTypeIdentifier.getPath(id); + final String entityMid = JpaActiveRecordMetadata.createIdentifier( + javaType, path); + + // Get the entity metadata + final JpaActiveRecordMetadata jpaActiveRecordMetadata = (JpaActiveRecordMetadata) getMetadataService() + .get(entityMid); + if (jpaActiveRecordMetadata == null) { + throw new IllegalArgumentException( + "Cannot provide finders because '" + + typeName.getFullyQualifiedTypeName() + + "' is not an 'active record' entity"); + } + + // Get the member details + final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService() + .get(PhysicalTypeIdentifier.createIdentifier(javaType, path)); + if (physicalTypeMetadata == null) { + throw new IllegalStateException( + "Could not determine physical type metadata for type " + + javaType); + } + final ClassOrInterfaceTypeDetails cid = physicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (cid == null) { + throw new IllegalStateException( + "Could not determine class or interface type details for type " + + javaType); + } + final MemberDetails memberDetails = getMemberDetailsScanner() + .getMemberDetails(getClass().getName(), cid); + final List idFields = getPersistenceMemberLocator() + .getIdentifierFields(javaType); + final FieldMetadata versionField = getPersistenceMemberLocator() + .getVersionField(javaType); + + // Compute the finders (excluding the ID, version, and EM fields) + final Set exclusions = new HashSet(); + exclusions.add(jpaActiveRecordMetadata.getEntityManagerField() + .getFieldName()); + for (final FieldMetadata idField : idFields) { + exclusions.add(idField.getFieldName()); + } + + if (versionField != null) { + exclusions.add(versionField.getFieldName()); + } + + final SortedSet result = new TreeSet(); + + final List finders = getDynamicFinderServices().getFinders( + memberDetails, jpaActiveRecordMetadata.getPlural(), depth, + exclusions); + for (final JavaSymbolName finder : finders) { + // Avoid displaying problematic finders + try { + final QueryHolder queryHolder = getDynamicFinderServices() + .getQueryHolder(memberDetails, finder, + jpaActiveRecordMetadata.getPlural(), + jpaActiveRecordMetadata.getEntityName()); + final List parameterNames = queryHolder + .getParameterNames(); + final List parameterTypes = queryHolder + .getParameterTypes(); + final StringBuilder signature = new StringBuilder(); + int x = -1; + for (final JavaType param : parameterTypes) { + x++; + if (x > 0) { + signature.append(", "); + } + signature.append(param.getSimpleTypeName()).append(" ") + .append(parameterNames.get(x).getSymbolName()); + } + result.add(finder.getSymbolName() + "(" + signature + ")" /* + * query: + * '" + * + + * query + * + + * "'" + */); + } + catch (final RuntimeException e) { + result.add(finder.getSymbolName() + " - failure"); + } + } + return result; + } + + public DynamicFinderServices getDynamicFinderServices(){ + if(dynamicFinderServices == null){ + // Get all Services implement DynamicFinderServices interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(DynamicFinderServices.class.getName(), null); + + for(ServiceReference ref : references){ + return (DynamicFinderServices) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load DynamicFinderServices on FinderOperationsImpl."); + return null; + } + }else{ + return dynamicFinderServices; + } + } + + public MemberDetailsScanner getMemberDetailsScanner(){ + if(memberDetailsScanner == null){ + // Get all Services implement MemberDetailsScanner interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MemberDetailsScanner.class.getName(), null); + + for(ServiceReference ref : references){ + return (MemberDetailsScanner) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MemberDetailsScanner on FinderOperationsImpl."); + return null; + } + }else{ + return memberDetailsScanner; + } + } + + public MetadataService getMetadataService(){ + if(metadataService == null){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on FinderOperationsImpl."); + return null; + } + }else{ + return metadataService; + } + } + + public PersistenceMemberLocator getPersistenceMemberLocator(){ + if(persistenceMemberLocator == null){ + // Get all Services implement PersistenceMemberLocator interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PersistenceMemberLocator.class.getName(), null); + + for(ServiceReference ref : references){ + return (PersistenceMemberLocator) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PersistenceMemberLocator on FinderOperationsImpl."); + return null; + } + }else{ + return persistenceMemberLocator; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on FinderOperationsImpl."); + return null; + } + }else{ + return projectOperations; + } + } + + public TypeLocationService getTypeLocationService(){ + if(typeLocationService == null){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on FinderOperationsImpl."); + return null; + } + }else{ + return typeLocationService; + } + } + + public TypeManagementService getTypeManagementService(){ + if(typeManagementService == null){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on FinderOperationsImpl."); + return null; + } + }else{ + return typeManagementService; + } + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/InvalidFinderException.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/InvalidFinderException.java new file mode 100644 index 000000000..ec55121a9 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/InvalidFinderException.java @@ -0,0 +1,16 @@ +package org.springframework.roo.addon.finder; + +/** + * Thrown when a dynamic finder method is invalid. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +public class InvalidFinderException extends RuntimeException { + + private static final long serialVersionUID = 2328865678880608749L; + + public InvalidFinderException(final String string) { + super(string); + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/QueryHolder.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/QueryHolder.java new file mode 100644 index 000000000..f8358c1f5 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/QueryHolder.java @@ -0,0 +1,60 @@ +package org.springframework.roo.addon.finder; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Bean to hold the JPA query string, the method parameter types and parameter + * names. + *

    + * Immutable once constructed. + * + * @author Alan Stewart + * @since 1.1.2 + */ +public class QueryHolder { + + private final String jpaQuery; + private List parameterNames; + private List parameterTypes; + private final List tokens; + + public QueryHolder(final String jpaQuery, + final List parameterTypes, + final List parameterNames, final List tokens) { + Validate.notBlank(jpaQuery, "JPA query required"); + Validate.notNull(parameterTypes, "Parameter types required"); + Validate.notNull(parameterNames, "Parameter names required"); + Validate.notNull(tokens, "Tokens required"); + this.jpaQuery = jpaQuery; + this.parameterTypes = parameterTypes; + this.parameterNames = parameterNames; + this.tokens = Collections.unmodifiableList(tokens); + } + + public String getJpaQuery() { + return jpaQuery; + } + + public List getParameterNames() { + return parameterNames; + } + + public List getParameterTypes() { + return parameterTypes; + } + + /** + * Package protected as it is only intended for internal use. + * + * @return the tokens used to process this query (used internally; + * immutable) + */ + List getTokens() { + return tokens; + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/ReservedToken.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/ReservedToken.java new file mode 100644 index 000000000..ef34ab1f8 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/ReservedToken.java @@ -0,0 +1,66 @@ +package org.springframework.roo.addon.finder; + +import org.apache.commons.lang3.Validate; + +/** + * A reserved token is a reserved word which is used as part of a JPA compliant + * SQL query. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +public class ReservedToken implements Token, Comparable { + + private final String value; + + /** + * Create an instance of the {@link ReservedToken} + * + * @param token the String token. + */ + public ReservedToken(final String token) { + Validate.notBlank(token, "Reserved token required"); + value = token; + } + + public int compareTo(final ReservedToken o) { + final int l = o.getValue().length() - getValue().length(); + return l == 0 ? -1 : l; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ReservedToken other = (ReservedToken) obj; + if (value == null) { + if (other.value != null) { + return false; + } + } + else if (!value.equals(other.value)) { + return false; + } + return true; + } + + public String getValue() { + return value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (value == null ? 0 : value.hashCode()); + return result; + } +} \ No newline at end of file diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/ReservedTokenHolder.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/ReservedTokenHolder.java new file mode 100644 index 000000000..ffdf660d6 --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/ReservedTokenHolder.java @@ -0,0 +1,79 @@ +package org.springframework.roo.addon.finder; + +import java.util.Collections; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Contains utility methods to return {@link SortedSet}s of + * {@link ReservedToken}s. + *

    + * Collections available through this class are immutable (non-modifiable). + * + * @author Stefan Schmidt + * @since 1.0; + */ +public abstract class ReservedTokenHolder { + + public static final SortedSet ALL_TOKENS; + public static final SortedSet BOOLEAN_TOKENS; + public static final SortedSet NUMERIC_TOKENS; + public static final SortedSet STRING_TOKENS; + + static { + NUMERIC_TOKENS = Collections.unmodifiableSortedSet(getNumericTokens()); + BOOLEAN_TOKENS = Collections.unmodifiableSortedSet(getBooleanTokens()); + STRING_TOKENS = Collections.unmodifiableSortedSet(getStringTokens()); + ALL_TOKENS = Collections.unmodifiableSortedSet(getAllTokens()); + } + + private static SortedSet getAllTokens() { + final SortedSet reservedTokens = new TreeSet(); + reservedTokens.add(new ReservedToken("Or")); + reservedTokens.add(new ReservedToken("And")); + reservedTokens.add(new ReservedToken("Not")); + reservedTokens.add(new ReservedToken("Like")); + reservedTokens.add(new ReservedToken("Ilike")); // Ignore case like + reservedTokens.add(new ReservedToken("LessThanEquals")); + reservedTokens.add(new ReservedToken("IsNull")); + reservedTokens.add(new ReservedToken("Equals")); + reservedTokens.add(new ReservedToken("Between")); + reservedTokens.add(new ReservedToken("LessThan")); + reservedTokens.add(new ReservedToken("NotEquals")); + reservedTokens.add(new ReservedToken("IsNotNull")); + reservedTokens.add(new ReservedToken("GreaterThan")); + reservedTokens.add(new ReservedToken("GreaterThanEquals")); + reservedTokens.add(new ReservedToken("Member")); + return reservedTokens; + } + + private static SortedSet getBooleanTokens() { + final SortedSet booleanTokens = new TreeSet(); + booleanTokens.add(new ReservedToken("Not")); + return booleanTokens; + } + + private static SortedSet getNumericTokens() { + final SortedSet numericTokens = new TreeSet(); + numericTokens.add(new ReservedToken("Between")); + numericTokens.add(new ReservedToken("LessThan")); + numericTokens.add(new ReservedToken("LessThanEquals")); + numericTokens.add(new ReservedToken("GreaterThan")); + numericTokens.add(new ReservedToken("GreaterThanEquals")); + numericTokens.add(new ReservedToken("IsNotNull")); + numericTokens.add(new ReservedToken("IsNull")); + numericTokens.add(new ReservedToken("NotEquals")); + numericTokens.add(new ReservedToken("Equals")); + return numericTokens; + } + + private static SortedSet getStringTokens() { + final SortedSet stringTokens = new TreeSet(); + stringTokens.add(new ReservedToken("Equals")); + stringTokens.add(new ReservedToken("NotEquals")); + stringTokens.add(new ReservedToken("Like")); + stringTokens.add(new ReservedToken("IsNotNull")); + stringTokens.add(new ReservedToken("IsNull")); + return stringTokens; + } +} diff --git a/addon-finder/src/main/java/org/springframework/roo/addon/finder/Token.java b/addon-finder/src/main/java/org/springframework/roo/addon/finder/Token.java new file mode 100644 index 000000000..acc9aeaca --- /dev/null +++ b/addon-finder/src/main/java/org/springframework/roo/addon/finder/Token.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.finder; + +/** + * A simple interface representing tokens in a JPA SQL query. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +public interface Token { + + String getValue(); +} diff --git a/addon-git/legal-git.txt b/addon-git/legal-git.txt new file mode 100644 index 000000000..e9523b78a --- /dev/null +++ b/addon-git/legal-git.txt @@ -0,0 +1,27 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: JGit +Software Web Site: http://www.eclipse.org/jgit/ +Effective License: Eclipse Distribution License - v 1.0 +License Info Page: http://www.eclipse.org/org/documents/edl-v10.php + +JGit is used to integrate git functionality into Spring Roo. + +----------------------------------------------------------------------- + +Licensed Software: JSch +Software Web Site: http://www.jcraft.com/jsch/ +Effective License: BSD-style license +License Info Page: http://www.jcraft.com/jsch/LICENSE.txt + +JSch is a transitive dependency of JGit. + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/addon-git/pom.xml b/addon-git/pom.xml new file mode 100644 index 000000000..389ba0bfd --- /dev/null +++ b/addon-git/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.git + bundle + Spring Roo - Addon - GIT + Offers GIT integration in the project. Each successfully executed command will be automatically committed to a local GIT repository. Tags: #git, #scm, #wrappedCoreDependency + + + jgit-repository + http://download.eclipse.org/jgit/maven + + + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.jsch + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.jgit + + + \ No newline at end of file diff --git a/addon-git/src/main/java/org/springframework/roo/addon/git/GitCommands.java b/addon-git/src/main/java/org/springframework/roo/addon/git/GitCommands.java new file mode 100644 index 000000000..150d7e60e --- /dev/null +++ b/addon-git/src/main/java/org/springframework/roo/addon/git/GitCommands.java @@ -0,0 +1,106 @@ +package org.springframework.roo.addon.git; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for addon-git. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class GitCommands implements CommandMarker { + + @Reference private GitOperations gitOperations; + + @CliCommand(value = "git setup", help = "Setup Git revision control") + public void config() { + gitOperations.setup(); + } + + @CliCommand(value = "git commit all", help = "Trigger a commit manually for the project") + public void config( + @CliOption(key = { "message" }, mandatory = true, help = "The commit message") final String message) { + + gitOperations.commitAllChanges(message); + } + + @CliCommand(value = "git config", help = "Git revision control configuration (.git/config)") + public void config( + @CliOption(key = { "userName" }, mandatory = false, help = "The user name") final String userName, + @CliOption(key = { "email" }, mandatory = false, help = "The user email") final String email, + @CliOption(key = { "repoUrl" }, mandatory = false, help = "The URL of the remote repository") final String repoUrl, + @CliOption(key = { "colorCoding" }, mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Enable color coding of commands in OS shell") final boolean color, + @CliOption(key = { "automaticCommit" }, mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "true", help = "Enable automatic commit after successful execution of Roo shell command") final Boolean automaticCommit) { + + if (userName != null && userName.length() > 0) { + gitOperations.setConfig("user", "name", userName); + } + if (email != null && email.length() > 0) { + gitOperations.setConfig("user", "email", email); + } + if (repoUrl != null && repoUrl.length() > 0) { + gitOperations.setConfig("remote \"origin\"", "url", repoUrl); + } + if (color) { + gitOperations.setConfig("color", "diff", "auto"); + gitOperations.setConfig("color", "branch", "auto"); + gitOperations.setConfig("color", "status", "auto"); + } + gitOperations.setConfig("roo", "automaticCommit", + automaticCommit.toString()); + } + + @CliAvailabilityIndicator({ "git config", "git commit all", + "git revert last", "git revert commit", "git log", "git push", + "git reset" }) + public boolean isCommandAvailable() { + return gitOperations.isGitCommandAvailable(); + } + + @CliAvailabilityIndicator("git setup") + public boolean isGitSetupAvailable() { + return gitOperations.isGitInstallationPossible(); + } + + @CliCommand(value = "git log", help = "Commit log") + public void log( + @CliOption(key = { "maxMessages" }, mandatory = false, help = "Number of commit messages to display") final Integer count) { + + gitOperations.log(count == null ? Integer.MAX_VALUE : count); + } + + @CliCommand(value = "git push", help = "Roll project back to a specific commit") + public void push() { + gitOperations.push(); + } + + @CliCommand(value = "git reset", help = "Reset (hard) last (x) commit(s)") + public void resetLast( + @CliOption(key = { "commitCount" }, mandatory = false, help = "Number of commits to reset") final Integer history, + @CliOption(key = { "message" }, mandatory = true, help = "The commit message") final String message) { + + gitOperations.reset(history == null ? 0 : history, message); + } + + @CliCommand(value = "git revert commit", help = "Roll project back to a specific commit") + public void revertCommit( + @CliOption(key = { "revString" }, mandatory = true, help = "Commit id") final String revstr, + @CliOption(key = { "message" }, mandatory = true, help = "The commit message") final String message) { + + gitOperations.revertCommit(revstr, message); + } + + @CliCommand(value = "git revert last", help = "Revert last commit") + public void revertLast( + @CliOption(key = { "message" }, mandatory = true, help = "The commit message") final String message) { + gitOperations.revertLastCommit(message); + } +} diff --git a/addon-git/src/main/java/org/springframework/roo/addon/git/GitOperations.java b/addon-git/src/main/java/org/springframework/roo/addon/git/GitOperations.java new file mode 100644 index 000000000..74ed2b437 --- /dev/null +++ b/addon-git/src/main/java/org/springframework/roo/addon/git/GitOperations.java @@ -0,0 +1,89 @@ +package org.springframework.roo.addon.git; + +/** + * Operations offered by Git addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface GitOperations { + + /** + * Triggers commit for all changes in the Git tree. (works like 'git commmit + * -a -m {message}') + * + * @param message Commit message + */ + void commitAllChanges(String message); + + /** + * Check if automatic commit is enabled for successful Roo commands. + * + * @return automaticCommit + */ + boolean isAutomaticCommit(); + + /** + * Check if Git commands are available in Shell. Depends on presence of .git + * repository. + * + * @return availability + */ + boolean isGitCommandAvailable(); + + /** + * Check if Git setup command is available in Shell. + * + * @return availability + */ + boolean isGitInstallationPossible(); + + /** + * Present git log. + * + * @param maxHistory + */ + void log(int maxHistory); + + /** + * Trigger git push. + */ + void push(); + + /** + * Triggers Git reset (hard). + * + * @param noOfCommitsToReset number of commits to reset (HEAD - n) + * @param message Commit message + */ + void reset(int noOfCommitsToReset, String message); + + /** + * Trigger revert of commit with given rev string. + * + * @param revstr + * @param message + */ + void revertCommit(String revstr, String message); + + /** + * Triggers revert of last commit. + * + * @param message Commit message + */ + void revertLastCommit(String message); + + /** + * Convenience access to the Git config (allows setting config options) + * + * @param category The Git config category. + * @param key The config key. + * @param value The config value. + */ + void setConfig(String category, String key, String value); + + /** + * Initial setup of git repository in target project. + */ + void setup(); +} diff --git a/addon-git/src/main/java/org/springframework/roo/addon/git/GitOperationsImpl.java b/addon-git/src/main/java/org/springframework/roo/addon/git/GitOperationsImpl.java new file mode 100644 index 000000000..f74d38ba1 --- /dev/null +++ b/addon-git/src/main/java/org/springframework/roo/addon/git/GitOperationsImpl.java @@ -0,0 +1,268 @@ +package org.springframework.roo.addon.git; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.PushResult; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.support.util.FileUtils; + +/** + * Operations for Git addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class GitOperationsImpl implements GitOperations { + + private static final Logger LOGGER = Logger + .getLogger(GitOperationsImpl.class.getName()); + private static final String REVISION_STRING_DELIMITER = "~"; + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + + private PersonIdent person; + + public void commitAllChanges(final String message) { + final Repository repository = getRepository(); + try { + final Git git = new Git(repository); + git.add().addFilepattern(".").call(); + final Status status = git.status().call(); + if (status.getChanged().size() > 0 || status.getAdded().size() > 0 + || status.getModified().size() > 0 + || status.getRemoved().size() > 0) { + final RevCommit rev = git.commit().setAll(true) + .setCommitter(person).setAuthor(person) + .setMessage(message).call(); + LOGGER.info("Git commit " + rev.getName() + " [" + message + + "]"); + } + } + catch (final Exception e) { + throw new IllegalStateException( + "Could not commit changes to local Git repository", e); + } + } + + private RevCommit findCommit(final String revstr, + final Repository repository) { + final RevWalk walk = new RevWalk(repository); + RevCommit commit = null; + try { + commit = walk.parseCommit(repository.resolve(revstr)); + } + catch (final MissingObjectException e1) { + LOGGER.warning("Could not find commit with id: " + revstr); + } + catch (final IncorrectObjectTypeException e1) { + LOGGER.warning("The provided rev is not a commit: " + revstr); + } + catch (final Exception ignore) { + } + finally { + walk.release(); + } + return commit; + } + + private Repository getRepository() { + if (hasDotGit()) { + try { + final String repositoryPath = pathResolver + .getFocusedIdentifier(Path.ROOT, Constants.DOT_GIT); + return new FileRepositoryBuilder().readEnvironment() + .findGitDir(new File(repositoryPath)).build(); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + } + throw new IllegalStateException("Git support not available"); + } + + private boolean hasDotGit() { + return fileManager.exists(pathResolver.getFocusedIdentifier(Path.ROOT, + Constants.DOT_GIT)); + } + + public boolean isAutomaticCommit() { + return getRepository().getConfig().getBoolean("roo", "automaticCommit", + true); + } + + public boolean isGitCommandAvailable() { + return hasDotGit(); + } + + public boolean isGitInstallationPossible() { + return !hasDotGit(); + } + + public void log(final int maxHistory) { + final Repository repository = getRepository(); + final Git git = new Git(repository); + try { + int counter = 0; + LOGGER.warning("---------- Start Git log ----------"); + for (final RevCommit commit : git.log().call()) { + LOGGER.info("commit id: " + commit.getName()); + LOGGER.info("message: " + commit.getFullMessage()); + LOGGER.info(""); + if (++counter >= maxHistory) { + break; + } + } + LOGGER.warning("---------- End Git log ----------"); + } + catch (final Exception e) { + throw new IllegalStateException("Could not parse git log", e); + } + } + + public void push() { + final Git git = new Git(getRepository()); + try { + for (final PushResult result : git.push().setPushAll().call()) { + LOGGER.info(result.getMessages()); + } + } + catch (final Exception e) { + throw new IllegalStateException( + "Unable to perform push operation ", e); + } + } + + public void reset(final int noOfCommitsToRevert, final String message) { + final Repository repository = getRepository(); + final RevCommit commit = findCommit(Constants.HEAD + + REVISION_STRING_DELIMITER + noOfCommitsToRevert, repository); + if (commit == null) { + return; + } + + try { + final Git git = new Git(repository); + git.reset().setRef(commit.getName()).setMode(ResetType.HARD).call(); + // Commit changes + commitAllChanges(message); + LOGGER.info("Reset of last " + (noOfCommitsToRevert + 1) + + " successful."); + } + catch (final Exception e) { + throw new IllegalStateException("Reset did not succeed.", e); + } + } + + public void revertCommit(final String revstr, final String message) { + final Repository repository = getRepository(); + final RevCommit commit = findCommit(revstr, repository); + if (commit == null) { + return; + } + + try { + final Git git = new Git(repository); + git.revert().include(commit).call(); + // Commit changes + commitAllChanges(message); + LOGGER.info("Revert of commit " + revstr + " successful."); + } + catch (final Exception e) { + throw new IllegalStateException("Revert of commit " + revstr + + " did not succeed.", e); + } + } + + public void revertLastCommit(final String message) { + revertCommit(Constants.HEAD + REVISION_STRING_DELIMITER + "0", message); + } + + public void setConfig(final String category, final String key, + final String value) { + final Repository repository = getRepository(); + try { + repository.getConfig().setString(category, null, key, value); + repository.getConfig().save(); + } + catch (final IOException ex) { + throw new IllegalStateException( + "Could not initialize Git repository", ex); + } + } + + public void setup() { + if (hasDotGit()) { + LOGGER.info("Git is already configured"); + return; + } + if (person == null) { + person = new PersonIdent("Roo Git Add-On", "s2-roo@vmware.com"); + } + try { + final String repositoryPath = pathResolver.getFocusedIdentifier( + Path.ROOT, Constants.DOT_GIT); + final Repository repository = new FileRepositoryBuilder() + .readEnvironment().setGitDir(new File(repositoryPath)) + .build(); + repository.create(); + } + catch (final Exception e) { + throw new IllegalStateException( + "Could not initialize Git repository", e); + } + setConfig("user", "name", person.getName()); + setConfig("user", "email", person.getEmailAddress()); + + setConfig("remote \"origin\"", "fetch", + "+refs/heads/*:refs/remotes/origin/*"); + setConfig("branch \"master\"", "remote", "origin"); + setConfig("branch \"master\"", "merge", "refs/heads/master"); + + final String gitIgnore = pathResolver.getFocusedIdentifier(Path.ROOT, + Constants.GITIGNORE_FILENAME); + + if (!fileManager.exists(gitIgnore)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "gitignore-template"); + outputStream = fileManager.createFile(gitIgnore) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Could not install " + + Constants.GITIGNORE_FILENAME + " file in project", e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } +} diff --git a/addon-git/src/main/java/org/springframework/roo/addon/git/GitShellEventListener.java b/addon-git/src/main/java/org/springframework/roo/addon/git/GitShellEventListener.java new file mode 100644 index 000000000..7fa4b9d91 --- /dev/null +++ b/addon-git/src/main/java/org/springframework/roo/addon/git/GitShellEventListener.java @@ -0,0 +1,50 @@ +package org.springframework.roo.addon.git; + +import java.io.File; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.eclipse.jgit.lib.Constants; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.event.ShellStatus; +import org.springframework.roo.shell.event.ShellStatus.Status; +import org.springframework.roo.shell.event.ShellStatusListener; + +/** + * Listener for Shell events to support automatic Git repository commits. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class GitShellEventListener implements ShellStatusListener { + + @Reference private GitOperations gitOperations; + @Reference private PathResolver pathResolver; + @Reference private Shell shell; + + protected void activate(final ComponentContext context) { + shell.addShellStatusListener(this); + } + + protected void deactivate(final ComponentContext context) { + shell.removeShellStatusListener(this); + } + + private boolean isGitEnabled() { + return new File(pathResolver.getRoot(), Constants.DOT_GIT) + .isDirectory(); + } + + public void onShellStatusChange(final ShellStatus oldStatus, + final ShellStatus newStatus) { + if (newStatus.getStatus().equals(Status.EXECUTION_SUCCESS) + && isGitEnabled() && gitOperations.isAutomaticCommit()) { + gitOperations.commitAllChanges(newStatus.getMessage()); + } + } +} diff --git a/addon-git/src/main/resources/org/springframework/roo/addon/git/gitignore-template b/addon-git/src/main/resources/org/springframework/roo/addon/git/gitignore-template new file mode 100644 index 000000000..d1d886ae6 --- /dev/null +++ b/addon-git/src/main/resources/org/springframework/roo/addon/git/gitignore-template @@ -0,0 +1,6 @@ +.settings +.classpath +.project +log.roo +target +bin diff --git a/addon-gwt/legal-addon-gwt.txt b/addon-gwt/legal-addon-gwt.txt new file mode 100644 index 000000000..ede28d493 --- /dev/null +++ b/addon-gwt/legal-addon-gwt.txt @@ -0,0 +1,40 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: Apache Velocity +Software Web Site: http://velocity.apache.org/ +Effective License: Apache License V2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.txt + +Apache Velocity is a runtime dependency of this module. Velocity +provides template services for writing files by the add-on. + +----------------------------------------------------------------------- + +Licensed Software: Apache Commons Collections +Software Web Site: http://commons.apache.org/collections/ +Effective License: Apache License V2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.txt + +Apache Commons Collections is a runtime dependency of this module. It +is required by Apache Velocity. + +----------------------------------------------------------------------- + +Licensed Software: Apache Commons Logging +Software Web Site: http://commons.apache.org/logging/ +Effective License: Apache License V2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.txt + +Apache Commons Logging is a runtime dependency of this module. It is +required by Apache Velocity. + +----------------------------------------------------------------------- + + +[end] diff --git a/addon-gwt/pom.xml b/addon-gwt/pom.xml new file mode 100644 index 000000000..8f8a9f53e --- /dev/null +++ b/addon-gwt/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.gwt + bundle + Spring Roo - Addon - Google Web Toolkit + Support for UI scaffolding using Google Web Toolkit. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.hapax + + + \ No newline at end of file diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtCommands.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtCommands.java new file mode 100644 index 000000000..c1a7ab5d8 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtCommands.java @@ -0,0 +1,118 @@ +package org.springframework.roo.addon.gwt; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the GWT add-on to be used by the Roo shell. + * + * @author Ben Alex + * @author James Tyrrell + * @since 1.1 + */ +@Component +@Service +public class GwtCommands implements CommandMarker { + + @Reference protected GwtOperations gwtOperations; + + @Deprecated + @CliCommand(value = "gwt setup", help = "Install Google Web Toolkit (GWT) into your project - deprecated, use 'web gwt setup' instead") + public void installGwt() { + gwtOperations.setup(); + } + + @CliAvailabilityIndicator({ "web gwt setup", "gwt setup" }) + public boolean isGwtSetupAvailable() { + return gwtOperations.isGwtInstallationPossible(); + } + + @CliAvailabilityIndicator({ "web gwt proxy all", "web gwt proxy type", + "web gwt request all", "web gwt request type", "web gwt all", + "web gwt scaffold", "web gwt proxy request all", + "web gwt proxy request type", "web gwt gae update" }) + public boolean isScaffoldAvailable() { + return gwtOperations.isScaffoldAvailable(); + } + + @CliCommand(value = "web gwt proxy all", help = "Locates all entities in the project and creates GWT proxies") + public void proxyAll( + @CliOption(key = "package", mandatory = true, optionContext = PROJECT, help = "The package in which created proxies will be placed") final JavaPackage javaPackage) { + + gwtOperations.proxyAll(javaPackage); + } + + @CliCommand(value = "web gwt proxy request all", help = "Locates all entities in the project and creates GWT requests and proxies") + public void proxyAndRequestAll( + @CliOption(key = "package", mandatory = true, optionContext = PROJECT, help = "The package in which created proxies and requests will be placed") final JavaPackage javaPackage) { + + gwtOperations.proxyAndRequestAll(javaPackage); + } + + @CliCommand(value = "web gwt proxy request type", help = "Creates a proxy and request based on the specified type") + public void proxyAndRequestType( + @CliOption(key = "package", mandatory = true, help = "The package in which created proxies and requests will be placed") final JavaPackage javaPackage, + @CliOption(key = "type", mandatory = true, optionContext = PROJECT, help = "The type to base the created proxy and request on") final JavaType type) { + + gwtOperations.proxyAndRequestType(javaPackage, type); + } + + @CliCommand(value = "web gwt proxy type", help = "Creates a GWT proxy based on the specified type") + public void proxyType( + @CliOption(key = "package", mandatory = true, help = "The package in which created proxies will be placed") final JavaPackage javaPackage, + @CliOption(key = "type", mandatory = true, optionContext = PROJECT, help = "The type to base the created request on") final JavaType type) { + + gwtOperations.proxyType(javaPackage, type); + } + + @CliCommand(value = "web gwt request all", help = "Locates all entities in the project and creates GWT requests") + public void requestAll( + @CliOption(key = "package", mandatory = true, optionContext = PROJECT, help = "The package in which created requests will be placed") final JavaPackage javaPackage) { + + gwtOperations.requestAll(javaPackage); + } + + @CliCommand(value = "web gwt request type", help = "Creates a GWT proxy based on the specified type") + public void requestType( + @CliOption(key = "package", mandatory = true, help = "The package in which created requests will be placed") final JavaPackage javaPackage, + @CliOption(key = "type", mandatory = true, optionContext = PROJECT, help = "The type to base the created request on") final JavaType type) { + + gwtOperations.requestType(javaPackage, type); + } + + @CliCommand(value = "web gwt all", help = "Locates all entities in the project and creates GWT requests, proxies and creates the scaffold") + public void scaffoldAll( + @CliOption(key = "proxyPackage", mandatory = true, optionContext = PROJECT, help = "The package in which created proxies will be placed") final JavaPackage proxyPackage, + @CliOption(key = "requestPackage", mandatory = true, optionContext = PROJECT, help = "The package in which created requests will be placed") final JavaPackage requestPackage) { + + gwtOperations.scaffoldAll(proxyPackage, requestPackage); + } + + @CliCommand(value = "web gwt scaffold", help = "Creates a GWT request, proxy and scaffold for the specified") + public void scaffoldType( + @CliOption(key = "proxyPackage", mandatory = true, optionContext = PROJECT, help = "The package in which created proxies will be placed") final JavaPackage proxyPackage, + @CliOption(key = "requestPackage", mandatory = true, optionContext = PROJECT, help = "The package in which created requests will be placed") final JavaPackage requestPackage, + @CliOption(key = "type", mandatory = true, help = "The type to base the created scaffold on") final JavaType type) { + + gwtOperations.scaffoldType(proxyPackage, requestPackage, type); + } + + @CliCommand(value = "web gwt gae update", help = "Updates the GWT project to support GAE") + public void updateGaeConfiguration() { + gwtOperations.updateGaeConfiguration(); + } + + @CliCommand(value = "web gwt setup", help = "Install Google Web Toolkit (GWT) into your project") + public void webGwtSetup() { + gwtOperations.setup(); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtFileManager.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtFileManager.java new file mode 100644 index 000000000..51933a231 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtFileManager.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.gwt; + +import java.util.List; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; + +/** + * Provides a basic implementation of {@link GwtFileManager} which encapsulates + * the file management functionality required by + * {@link org.springframework.roo.addon.gwt.scaffold.GwtScaffoldMetadataProviderImpl} + * . + * + * @author James Tyrrell + * @since 1.1.1 + */ +public interface GwtFileManager { + + String write(ClassOrInterfaceTypeDetails typeDetails, boolean includeWarning); + + /** + * Writes the given Java type to disk in the user project + * + * @param typeDetails the type to write (required) + * @param warning any warning to appear at the top of the source file + * (cannot be null; include a trailing newline if + * not empty) + * @return the contents of the type (minus the warning) + */ + String write(ClassOrInterfaceTypeDetails typeDetails, String warning); + + void write(List typeDetails, + boolean includeWarning); + + void write(String destFile, String newContents); + + void delete(String file); + + void delete(ClassOrInterfaceTypeDetails typeDetails); + + boolean fileExists(ClassOrInterfaceTypeDetails typeDetails); + + boolean fileExists(String file); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtFileManagerImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtFileManagerImpl.java new file mode 100644 index 000000000..bc893f950 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtFileManagerImpl.java @@ -0,0 +1,117 @@ +package org.springframework.roo.addon.gwt; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeParsingService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.process.manager.FileManager; + +/** + * Implementation of {@link GwtFileManager}. + * + * @author James Tyrrell + * @since 1.1.1 + */ +@Component +@Service +public class GwtFileManagerImpl implements GwtFileManager { + + private static final String ROO_EDIT_WARNING = "// WARNING: DO NOT EDIT THIS FILE. THIS FILE IS MANAGED BY SPRING ROO.\n\n"; + + @Reference protected FileManager fileManager; + @Reference protected TypeLocationService typeLocationService; + @Reference protected TypeParsingService typeParsingService; + + @Override + public String write(final ClassOrInterfaceTypeDetails typeDetails, + boolean includeWarning) { + final String destFile = typeLocationService + .getPhysicalTypeCanonicalPath(typeDetails + .getDeclaredByMetadataId()); + includeWarning &= !destFile.endsWith(".xml"); + includeWarning |= destFile.endsWith("_Roo_Gwt.java"); + String fileContents = typeParsingService + .getCompilationUnitContents(typeDetails); + if (includeWarning) { + fileContents = ROO_EDIT_WARNING + fileContents; + } + else if (fileManager.exists(destFile)) { + return fileContents; + } + write(destFile, fileContents, includeWarning); + return fileContents; + } + + @Override + public String write(final ClassOrInterfaceTypeDetails typeDetails, + final String warning) { + final String destFile = typeLocationService + .getPhysicalTypeCanonicalPath(typeDetails + .getDeclaredByMetadataId()); + final String fileContents = typeParsingService + .getCompilationUnitContents(typeDetails); + fileManager.createOrUpdateTextFileIfRequired(destFile, warning + + fileContents, true); + return fileContents; + } + + @Override + public void write(final List typeDetails, + final boolean includeWarning) { + for (final ClassOrInterfaceTypeDetails typeDetail : typeDetails) { + write(typeDetail, includeWarning); + } + } + + @Override + public void write(final String destFile, final String newContents) { + write(destFile, newContents, true); + } + + private void write(final String destFile, final String newContents, + final boolean overwrite) { + // Write to disk, or update a file if it is already present and + // overwriting is allowed + if (!fileManager.exists(destFile) || overwrite) { + fileManager.createOrUpdateTextFileIfRequired(destFile, newContents, + true); + } + } + + @Override + public void delete(final ClassOrInterfaceTypeDetails typeDetails) { + final String file = typeLocationService + .getPhysicalTypeCanonicalPath(typeDetails + .getDeclaredByMetadataId()); + + delete(file); + } + + @Override + public void delete(final String file) { + // Write to disk, or update a file if it is already present and + // overwriting is allowed + + if (fileManager.exists(file)) { + fileManager.delete(file); + } + } + + @Override + public boolean fileExists(ClassOrInterfaceTypeDetails typeDetails) { + final String file = typeLocationService + .getPhysicalTypeCanonicalPath(typeDetails + .getDeclaredByMetadataId()); + return fileExists(file); + } + + @Override + public boolean fileExists(String file) { + // TODO Auto-generated method stub + return fileManager.exists(file); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtJavaType.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtJavaType.java new file mode 100644 index 000000000..5851ab0e9 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtJavaType.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.gwt; + +import org.springframework.roo.model.JavaType; + +public final class GwtJavaType { + + public static final JavaType ACCEPTS_ONE_WIDGET = new JavaType( + "com.google.gwt.user.client.ui.AcceptsOneWidget"); + public static final JavaType ENTITY_PROXY = new JavaType( + "com.google.web.bindery.requestfactory.shared.EntityProxy"); + public static final JavaType EVENT_BUS = new JavaType( + "com.google.gwt.event.shared.EventBus"); + public static final JavaType INSTANCE_REQUEST = new JavaType( + "com.google.web.bindery.requestfactory.shared.InstanceRequest"); + public static final JavaType LOCATOR = new JavaType( + "com.google.web.bindery.requestfactory.shared.Locator"); + public static final JavaType OLD_ENTITY_PROXY = new JavaType( + "com.google.gwt.requestfactory.shared.EntityProxy"); + public static final JavaType OLD_REQUEST_CONTEXT = new JavaType( + "com.google.gwt.requestfactory.shared.RequestContext"); + public static final JavaType PLACE = new JavaType( + "com.google.gwt.place.shared.Place"); + public static final JavaType PROXY_FOR = new JavaType( + "com.google.web.bindery.requestfactory.shared.ProxyFor"); + public static final JavaType PROXY_FOR_NAME = new JavaType( + "com.google.web.bindery.requestfactory.shared.ProxyForName"); + public static final JavaType RECEIVER = new JavaType( + "com.google.web.bindery.requestfactory.shared.Receiver"); + public static final JavaType REQUEST = new JavaType( + "com.google.web.bindery.requestfactory.shared.Request"); + public static final JavaType REQUEST_CONTEXT = new JavaType( + "com.google.web.bindery.requestfactory.shared.RequestContext"); + public static final JavaType SERVICE = new JavaType( + "com.google.web.bindery.requestfactory.shared.Service"); + public static final JavaType SERVICE_NAME = new JavaType( + "com.google.web.bindery.requestfactory.shared.ServiceName"); + + /** + * Constructor is private to prevent instantiation + */ + private GwtJavaType() { + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtOperations.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtOperations.java new file mode 100644 index 000000000..67c404287 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtOperations.java @@ -0,0 +1,46 @@ +package org.springframework.roo.addon.gwt; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Feature; + +/** + * Provides GWT operations. + * + * @author Ben Alex + * @author James Tyrrell + * @since 1.1 + */ +public interface GwtOperations extends Feature { + + /** + * The delimiter for multi-level paths specified by a " + * element in a module's *.gwt.xml file. + */ + String PATH_DELIMITER = "/"; + + boolean isGwtInstallationPossible(); + + boolean isScaffoldAvailable(); + + void proxyAll(JavaPackage proxyPackage); + + void proxyAndRequestAll(JavaPackage proxyAndRequestPackage); + + void proxyAndRequestType(JavaPackage proxyAndRequestPackage, JavaType type); + + void proxyType(JavaPackage proxyPackage, JavaType type); + + void requestAll(JavaPackage requestPackage); + + void requestType(JavaPackage requestPackage, JavaType type); + + void scaffoldAll(JavaPackage proxyPackage, JavaPackage requestPackage); + + void scaffoldType(JavaPackage proxyPackage, JavaPackage requestPackage, + JavaType type); + + void setup(); + + void updateGaeConfiguration(); +} \ No newline at end of file diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtOperationsImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtOperationsImpl.java new file mode 100644 index 000000000..38348695e --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtOperationsImpl.java @@ -0,0 +1,997 @@ +package org.springframework.roo.addon.gwt; + +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.addon.gwt.GwtJavaType.ENTITY_PROXY; +import static org.springframework.roo.addon.gwt.GwtJavaType.OLD_ENTITY_PROXY; +import static org.springframework.roo.addon.gwt.GwtJavaType.OLD_REQUEST_CONTEXT; +import static org.springframework.roo.addon.gwt.GwtJavaType.PROXY_FOR_NAME; +import static org.springframework.roo.addon.gwt.GwtJavaType.REQUEST_CONTEXT; +import static org.springframework.roo.classpath.PhysicalTypeCategory.INTERFACE; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_MIRRORED_FROM; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_PROXY; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_REQUEST; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_UNMANAGED_REQUEST; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; +import static org.springframework.roo.project.Path.ROOT; +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; +import static org.springframework.roo.project.Path.SRC_MAIN_WEBAPP; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.gwt.request.GwtRequestMetadata; +import org.springframework.roo.addon.web.mvc.controller.WebMvcOperations; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.Property; +import org.springframework.roo.project.Repository; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.WebXmlUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of {@link GwtOperations}. + * + * @author Ben Alex + * @author Alan Stewart + * @author Stefan Schmidt + * @author Ray Cromwell + * @author Amit Manjhi + * @since 1.1 + */ +@Component +@Service +public class GwtOperationsImpl implements GwtOperations { + + private static final String GWT_BUILD_COMMAND = "com.google.gwt.eclipse.core.gwtProjectValidator"; + private static final String GWT_PROJECT_NATURE = "com.google.gwt.eclipse.core.gwtNature"; + private static final String MAVEN_ECLIPSE_PLUGIN = "/project/build/plugins/plugin[artifactId = 'maven-eclipse-plugin']"; + private static final String OUTPUT_DIRECTORY = "${project.build.directory}/${project.build.finalName}/WEB-INF/classes"; + private static final JavaSymbolName VALUE = new JavaSymbolName("value"); + + @Reference protected FileManager fileManager; + @Reference protected GwtTemplateService gwtTemplateService; + @Reference protected GwtTypeService gwtTypeService; + @Reference protected MetadataService metadataService; + @Reference protected PersistenceMemberLocator persistenceMemberLocator; + @Reference protected ProjectOperations projectOperations; + @Reference protected TypeLocationService typeLocationService; + @Reference protected TypeManagementService typeManagementService; + @Reference protected WebMvcOperations webMvcOperations; + + private Boolean wasGaeEnabled; + private ComponentContext context; + + protected void activate(final ComponentContext context) { + this.context = context; + } + + private void addPackageToGwtXml(final JavaPackage sourcePackage) { + String gwtConfig = gwtTypeService.getGwtModuleXml(projectOperations + .getFocusedModuleName()); + gwtConfig = StringUtils.stripEnd(gwtConfig, File.separator); + final String moduleRoot = projectOperations.getPathResolver() + .getFocusedRoot(SRC_MAIN_JAVA); + final String topLevelPackage = gwtConfig.replace( + FileUtils.ensureTrailingSeparator(moduleRoot), "").replace( + File.separator, "."); + final String relativePackage = StringUtils.removeStart( + sourcePackage.getFullyQualifiedPackageName(), topLevelPackage + + "."); + gwtTypeService.addSourcePath( + relativePackage.replace(".", PATH_DELIMITER), + projectOperations.getFocusedModuleName()); + } + + private void copyDirectoryContents() { + for (final GwtPath path : GwtPath.values()) { + copyDirectoryContents(path); + } + } + + private void copyDirectoryContents(final GwtPath gwtPath) { + final String sourceAntPath = gwtPath.getSourceAntPath(); + if (sourceAntPath.contains("gae") + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE)) { + return; + } + final String targetDirectory = gwtPath == GwtPath.WEB ? projectOperations + .getPathResolver().getFocusedRoot(SRC_MAIN_WEBAPP) + : projectOperations.getPathResolver().getFocusedIdentifier( + SRC_MAIN_JAVA, + gwtPath.getPackagePath(projectOperations + .getFocusedTopLevelPackage())); + updateFile(sourceAntPath, targetDirectory, gwtPath.segmentPackage(), + false); + } + + private void createProxy(final ClassOrInterfaceTypeDetails entity, + final JavaPackage destinationPackage) { + final ClassOrInterfaceTypeDetails existingProxy = gwtTypeService + .lookupProxyFromEntity(entity); + if (existingProxy != null || entity.isAbstract()) { + return; + } + + final JavaType proxyType = new JavaType( + destinationPackage.getFullyQualifiedPackageName() + "." + + entity.getName().getSimpleTypeName() + "Proxy"); + final String focusedModule = projectOperations.getFocusedModuleName(); + final LogicalPath proxyLogicalPath = LogicalPath.getInstance( + SRC_MAIN_JAVA, focusedModule); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + PhysicalTypeIdentifier.createIdentifier(proxyType, + proxyLogicalPath)); + + cidBuilder.setName(proxyType); + cidBuilder.setExtendsTypes(Collections.singletonList(ENTITY_PROXY)); + cidBuilder.setPhysicalTypeCategory(INTERFACE); + cidBuilder.setModifier(PUBLIC); + final List> attributeValues = new ArrayList>(); + final StringAttributeValue stringAttributeValue = new StringAttributeValue( + VALUE, entity.getName().getFullyQualifiedTypeName()); + attributeValues.add(stringAttributeValue); + final String locator = projectOperations + .getTopLevelPackage(focusedModule) + + ".server.locator." + + entity.getName().getSimpleTypeName() + "Locator"; + final StringAttributeValue locatorAttributeValue = new StringAttributeValue( + new JavaSymbolName("locator"), locator); + attributeValues.add(locatorAttributeValue); + cidBuilder.updateTypeAnnotation(new AnnotationMetadataBuilder( + PROXY_FOR_NAME, attributeValues)); + attributeValues.remove(locatorAttributeValue); + final List readOnlyValues = new ArrayList(); + final FieldMetadata versionField = persistenceMemberLocator + .getVersionField(entity.getName()); + if (versionField != null) { + readOnlyValues.add(new StringAttributeValue(VALUE, versionField + .getFieldName().getSymbolName())); + } + final List idFields = persistenceMemberLocator + .getIdentifierFields(entity.getName()); + if (!CollectionUtils.isEmpty(idFields)) { + readOnlyValues.add(new StringAttributeValue(VALUE, idFields.get(0) + .getFieldName().getSymbolName())); + } + final ArrayAttributeValue readOnlyAttribute = new ArrayAttributeValue( + new JavaSymbolName("readOnly"), readOnlyValues); + attributeValues.add(readOnlyAttribute); + cidBuilder.updateTypeAnnotation(new AnnotationMetadataBuilder( + ROO_GWT_PROXY, attributeValues)); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + addPackageToGwtXml(destinationPackage); + } + + /** + * Builds the given entity's managed RequestContext interface. Note that we + * don't generate the entire interface here, only the @RooGwtRequest + * annotation; we then invoke the metadata provider, which takes over and + * generates the remaining code, namely the method declarations and the @ServiceName + * annotation. This is analogous to how ITD-based addons work, e.g. adding a + * trigger annotation and letting the metadata provider do the rest. This + * allows for the metadata provider to correctly respond to project changes. + * + * @param entity the entity for which to create the GWT request interface + * (required) + * @param destinationPackage the package in which to create the request + * interface (required) + */ + private void createRequestInterface( + final ClassOrInterfaceTypeDetails entity, + final JavaPackage destinationPackage) { + final JavaType requestType = new JavaType( + destinationPackage.getFullyQualifiedPackageName() + "." + + entity.getType().getSimpleTypeName() + + "Request_Roo_Gwt"); + final LogicalPath focusedSrcMainJava = LogicalPath.getInstance( + SRC_MAIN_JAVA, projectOperations.getFocusedModuleName()); + final ClassOrInterfaceTypeDetailsBuilder requestBuilder = new ClassOrInterfaceTypeDetailsBuilder( + PhysicalTypeIdentifier.createIdentifier(requestType, + focusedSrcMainJava)); + requestBuilder.setName(requestType); + requestBuilder.addExtendsTypes(REQUEST_CONTEXT); + requestBuilder.setPhysicalTypeCategory(INTERFACE); + requestBuilder.setModifier(PUBLIC); + requestBuilder.addAnnotation(getRooGwtRequestAnnotation(entity)); + typeManagementService.createOrUpdateTypeOnDisk(requestBuilder.build()); + addPackageToGwtXml(destinationPackage); + // Trigger the GwtRequestMetadataProvider to finish generating the code + metadataService.get(GwtRequestMetadata.createIdentifier(requestType, + focusedSrcMainJava)); + } + + /** + * Builds the given entity's unmanaged RequestContext interface used for + * adding custom methods. This interface extends the RequestContext + * interface managed by Roo. + * + * @param entity the entity for which to create the GWT request interface + * (required) + * @param destinationPackage the package in which to create the request + * interface (required) + */ + private void createUnmanagedRequestInterface( + final ClassOrInterfaceTypeDetails entity, + JavaPackage destinationPackage) { + final ClassOrInterfaceTypeDetails managedRequest = gwtTypeService + .lookupRequestFromEntity(entity); + + if (managedRequest == null) + return; + + final JavaType unmanagedRequestType = new JavaType( + destinationPackage.getFullyQualifiedPackageName() + "." + + entity.getType().getSimpleTypeName() + "Request"); + + final LogicalPath focusedSrcMainJava = LogicalPath.getInstance( + SRC_MAIN_JAVA, projectOperations.getFocusedModuleName()); + final ClassOrInterfaceTypeDetailsBuilder unmanagedRequestBuilder = new ClassOrInterfaceTypeDetailsBuilder( + PhysicalTypeIdentifier.createIdentifier(unmanagedRequestType, + focusedSrcMainJava)); + unmanagedRequestBuilder.setName(unmanagedRequestType); + unmanagedRequestBuilder.addExtendsTypes(managedRequest.getType()); + unmanagedRequestBuilder.setPhysicalTypeCategory(INTERFACE); + unmanagedRequestBuilder.setModifier(PUBLIC); + unmanagedRequestBuilder + .addAnnotation(getRooGwtUnmanagedRequestAnnotation(entity)); + unmanagedRequestBuilder.addAnnotation(managedRequest + .getAnnotation(GwtJavaType.SERVICE_NAME)); + typeManagementService.createOrUpdateTypeOnDisk(unmanagedRequestBuilder + .build()); + + } + + private void createRequestInterfaceIfNecessary( + final ClassOrInterfaceTypeDetails entity, + final JavaPackage destinationPackage) { + if (entity != null && !entity.isAbstract() + && gwtTypeService.lookupRequestFromEntity(entity) == null) { + createRequestInterface(entity, destinationPackage); + + createUnmanagedRequestInterface(entity, destinationPackage); + } + } + + private void createScaffold(final ClassOrInterfaceTypeDetails proxy) { + final AnnotationMetadata annotationMetadata = GwtUtils + .getFirstAnnotation(proxy, ROO_GWT_PROXY); + if (annotationMetadata != null) { + final AnnotationAttributeValue booleanAttributeValue = annotationMetadata + .getAttribute("scaffold"); + if (booleanAttributeValue == null + || !booleanAttributeValue.getValue()) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + proxy); + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder( + annotationMetadata); + annotationMetadataBuilder.addBooleanAttribute("scaffold", true); + for (final AnnotationMetadataBuilder existingAnnotation : cidBuilder + .getAnnotations()) { + if (existingAnnotation.getAnnotationType().equals( + annotationMetadata.getAnnotationType())) { + cidBuilder.getAnnotations().remove(existingAnnotation); + cidBuilder.getAnnotations().add( + annotationMetadataBuilder); + break; + } + } + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder + .build()); + } + } + } + + private void deleteUntouchedSetupFiles(final String sourceAntPath, + String targetDirectory) { + if (!targetDirectory.endsWith(File.separator)) { + targetDirectory += File.separator; + } + if (!fileManager.exists(targetDirectory)) { + fileManager.createDirectory(targetDirectory); + } + + final String path = FileUtils.getPath(getClass(), sourceAntPath); + final Iterable uris = OSGiUtils.findEntriesByPattern( + context.getBundleContext(), path); + Validate.notNull(uris, + "Could not search bundles for resources for Ant Path '%s'", + path); + + for (final URL url : uris) { + String fileName = url.getPath().substring( + url.getPath().lastIndexOf('/') + 1); + fileName = fileName.replace("-template", ""); + final String targetFilename = targetDirectory + fileName; + if (!fileManager.exists(targetFilename)) { + continue; + } + try { + String input = IOUtils.toString(url); + input = processTemplate(input, null); + final String existing = org.apache.commons.io.FileUtils + .readFileToString(new File(targetFilename)); + if (existing.equals(input)) { + // new File(targetFilename).delete(); + fileManager.delete(targetFilename); + } + } + catch (final IOException ignored) { + } + } + } + + private CharSequence getGaeHookup() { + final StringBuilder builder = new StringBuilder( + "// AppEngine user authentication\n\n"); + builder.append("new GaeLoginWidgetDriver(requestFactory).setWidget(shell.getLoginWidget());\n\n"); + builder.append("new ReloadOnAuthenticationFailure().register(eventBus);\n\n"); + return builder.toString(); + } + + @Override + public String getName() { + return FeatureNames.GWT; + } + + private String getPomPath() { + return projectOperations.getPathResolver().getFocusedIdentifier(ROOT, + "pom.xml"); + } + + private AnnotationMetadata getRooGwtRequestAnnotation( + final ClassOrInterfaceTypeDetails entity) { + // The GwtRequestMetadataProvider doesn't need to know excluded methods + // any more because it actively adds the required CRUD methods itself. + final StringAttributeValue entityAttributeValue = new StringAttributeValue( + VALUE, entity.getType().getFullyQualifiedTypeName()); + final List> gwtRequestAttributeValues = new ArrayList>(); + gwtRequestAttributeValues.add(entityAttributeValue); + return new AnnotationMetadataBuilder(ROO_GWT_REQUEST, + gwtRequestAttributeValues).build(); + } + + private AnnotationMetadata getRooGwtUnmanagedRequestAnnotation( + final ClassOrInterfaceTypeDetails entity) { + final StringAttributeValue entityAttributeValue = new StringAttributeValue( + VALUE, entity.getType().getFullyQualifiedTypeName()); + final List> gwtRequestAttributeValues = new ArrayList>(); + gwtRequestAttributeValues.add(entityAttributeValue); + return new AnnotationMetadataBuilder(ROO_GWT_UNMANAGED_REQUEST, + gwtRequestAttributeValues).build(); + } + + @Override + public boolean isGwtInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + @Override + public boolean isInstalledInModule(final String moduleName) { + final Pom pom = projectOperations.getPomFromModuleName(moduleName); + if (pom == null) { + return false; + } + for (final Plugin buildPlugin : pom.getBuildPlugins()) { + if ("gwt-maven-plugin".equals(buildPlugin.getArtifactId())) { + return true; + } + } + return false; + } + + @Override + public boolean isScaffoldAvailable() { + return isGwtInstallationPossible() + && isInstalledInModule(projectOperations.getFocusedModuleName()); + } + + private String processTemplate(String input, String segmentPackage) { + if (segmentPackage == null) { + segmentPackage = ""; + } + final String topLevelPackage = projectOperations.getTopLevelPackage( + projectOperations.getFocusedModuleName()) + .getFullyQualifiedPackageName(); + input = input.replace("__TOP_LEVEL_PACKAGE__", topLevelPackage); + input = input.replace("__SEGMENT_PACKAGE__", segmentPackage); + input = input.replace("__PROJECT_NAME__", projectOperations + .getProjectName(projectOperations.getFocusedModuleName())); + + if (projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE)) { + input = input.replace("__GAE_IMPORT__", "import " + topLevelPackage + + ".client.scaffold.gae.*;\n"); + input = input.replace("__GAE_HOOKUP__", getGaeHookup()); + input = input.replace("__GAE_REQUEST_TRANSPORT__", + ", new GaeAuthRequestTransport(eventBus)"); + } + else { + input = input.replace("__GAE_IMPORT__", ""); + input = input.replace("__GAE_HOOKUP__", ""); + input = input.replace("__GAE_REQUEST_TRANSPORT__", ""); + } + return input; + } + + @Override + public void proxyAll(final JavaPackage proxyPackage) { + for (final ClassOrInterfaceTypeDetails entity : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_JPA_ENTITY, + ROO_JPA_ACTIVE_RECORD)) { + createProxy(entity, proxyPackage); + } + copyDirectoryContents(GwtPath.LOCATOR); + } + + @Override + public void proxyAndRequestAll(final JavaPackage proxyAndRequestPackage) { + proxyAll(proxyAndRequestPackage); + requestAll(proxyAndRequestPackage); + } + + @Override + public void proxyAndRequestType(final JavaPackage proxyAndRequestPackage, + final JavaType type) { + proxyType(proxyAndRequestPackage, type); + requestType(proxyAndRequestPackage, type); + } + + @Override + public void proxyType(final JavaPackage proxyPackage, final JavaType type) { + final ClassOrInterfaceTypeDetails entity = typeLocationService + .getTypeDetails(type); + if (entity != null) { + createProxy(entity, proxyPackage); + } + copyDirectoryContents(GwtPath.LOCATOR); + } + + private void removeIfFound(final String xpath, final Element webXmlRoot) { + for (Element toRemove : XmlUtils.findElements(xpath, webXmlRoot)) { + if (toRemove != null) { + toRemove.getParentNode().removeChild(toRemove); + toRemove = null; + } + } + } + + @Override + public void requestAll(final JavaPackage proxyPackage) { + for (final ClassOrInterfaceTypeDetails entity : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_JPA_ENTITY, + ROO_JPA_ACTIVE_RECORD)) { + createRequestInterfaceIfNecessary(entity, proxyPackage); + } + } + + @Override + public void requestType(final JavaPackage requestPackage, + final JavaType type) { + createRequestInterfaceIfNecessary( + typeLocationService.getTypeDetails(type), requestPackage); + } + + @Override + public void scaffoldAll(final JavaPackage proxyPackage, + final JavaPackage requestPackage) { + updateScaffoldBoilerPlate(); + proxyAll(proxyPackage); + requestAll(requestPackage); + for (final ClassOrInterfaceTypeDetails proxy : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_GWT_PROXY)) { + final ClassOrInterfaceTypeDetails request = gwtTypeService + .lookupRequestFromProxy(proxy); + if (request == null) { + throw new IllegalStateException( + "In order to scaffold, an entity must have a request"); + } + createScaffold(proxy); + } + } + + @Override + public void scaffoldType(final JavaPackage proxyPackage, + final JavaPackage requestPackage, final JavaType type) { + proxyType(proxyPackage, type); + requestType(requestPackage, type); + final ClassOrInterfaceTypeDetails entity = typeLocationService + .getTypeDetails(type); + if (entity != null && !entity.isAbstract()) { + final ClassOrInterfaceTypeDetails proxy = gwtTypeService + .lookupProxyFromEntity(entity); + final ClassOrInterfaceTypeDetails request = gwtTypeService + .lookupRequestFromEntity(entity); + if (proxy == null || request == null) { + throw new IllegalStateException( + "In order to scaffold, an entity must have an associated proxy and request"); + } + updateScaffoldBoilerPlate(); + createScaffold(proxy); + } + } + + @Override + public void setup() { + // Install web pieces if not already installed + if (!fileManager.exists(projectOperations.getPathResolver() + .getFocusedIdentifier(SRC_MAIN_WEBAPP, "WEB-INF/web.xml"))) { + webMvcOperations.installAllWebMvcArtifacts(); + } + + final String topPackageName = projectOperations.getTopLevelPackage( + projectOperations.getFocusedModuleName()) + .getFullyQualifiedPackageName(); + final Set gwtConfigs = fileManager + .findMatchingAntPath(projectOperations.getPathResolver() + .getFocusedRoot(SRC_MAIN_JAVA) + + File.separatorChar + + topPackageName.replace('.', File.separatorChar) + + File.separator + "*.gwt.xml"); + final boolean gwtAlreadySetup = !gwtConfigs.isEmpty(); + + if (!gwtAlreadySetup) { + String sourceAntPath = "setup/*"; + final String targetDirectory = projectOperations.getPathResolver() + .getFocusedIdentifier(SRC_MAIN_JAVA, + topPackageName.replace('.', File.separatorChar)); + updateFile(sourceAntPath, targetDirectory, "", false); + + sourceAntPath = "setup/client/*"; + updateFile(sourceAntPath, targetDirectory + "/client", "", false); + } + + for (final ClassOrInterfaceTypeDetails proxyOrRequest : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_GWT_MIRRORED_FROM)) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + proxyOrRequest); + if (proxyOrRequest.extendsType(ENTITY_PROXY) + || proxyOrRequest.extendsType(OLD_ENTITY_PROXY)) { + final AnnotationMetadata annotationMetadata = MemberFindingUtils + .getAnnotationOfType(proxyOrRequest.getAnnotations(), + ROO_GWT_MIRRORED_FROM); + if (annotationMetadata != null) { + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder( + annotationMetadata); + annotationMetadataBuilder.setAnnotationType(ROO_GWT_PROXY); + cidBuilder.removeAnnotation(ROO_GWT_MIRRORED_FROM); + cidBuilder.addAnnotation(annotationMetadataBuilder); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder + .build()); + } + } + else if (proxyOrRequest.extendsType(REQUEST_CONTEXT) + || proxyOrRequest.extendsType(OLD_REQUEST_CONTEXT)) { + final AnnotationMetadata annotationMetadata = MemberFindingUtils + .getAnnotationOfType(proxyOrRequest.getAnnotations(), + ROO_GWT_MIRRORED_FROM); + if (annotationMetadata != null) { + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder( + annotationMetadata); + annotationMetadataBuilder + .setAnnotationType(ROO_GWT_REQUEST); + cidBuilder.removeAnnotation(ROO_GWT_MIRRORED_FROM); + cidBuilder.addAnnotation(annotationMetadataBuilder); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder + .build()); + } + } + } + + // Add GWT natures and builder names to maven eclipse plugin + updateEclipsePlugin(); + + // Add outputDirectory to build element of pom + updateBuildOutputDirectory(); + + final Element configuration = XmlUtils.getConfiguration(getClass()); + + // Add properties + updateProperties(configuration, + projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE)); + + // Add POM repositories + updateRepositories(configuration); + + // Add dependencies + updateDependencies(configuration); + + // Update web.xml + updateWebXml(); + + // Update gwt-maven-plugin and others + updateBuildPlugins(projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE)); + } + + /** + * Sets the POM's output directory to {@value #OUTPUT_DIRECTORY}, if it's + * not already set to something else. + */ + private void updateBuildOutputDirectory() { + // Read the POM + final String pom = getPomPath(); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom)); + final Element root = document.getDocumentElement(); + + Element outputDirectoryElement = XmlUtils.findFirstElement( + "/project/build/outputDirectory", root); + if (outputDirectoryElement == null) { + // Create it + final Element buildElement = XmlUtils.findRequiredElement( + "/project/build", root); + outputDirectoryElement = DomUtils.createChildElement( + "outputDirectory", buildElement, document); + } + outputDirectoryElement.setTextContent(OUTPUT_DIRECTORY); + + fileManager.createOrUpdateTextFileIfRequired(pom, + XmlUtils.nodeToString(document), false); + } + + private void updateBuildPlugins(final boolean isGaeEnabled) { + // Update the POM + final List plugins = new ArrayList(); + final String xPathExpression = "/configuration/" + + (isGaeEnabled ? "gae" : "gwt") + "/plugins/plugin"; + final List pluginElements = XmlUtils.findElements( + xPathExpression, XmlUtils.getConfiguration(getClass())); + for (final Element pluginElement : pluginElements) { + plugins.add(new Plugin(pluginElement)); + } + projectOperations.addBuildPlugins( + projectOperations.getFocusedModuleName(), plugins); + } + + private void updateDependencies(final Element configuration) { + final List dependencies = new ArrayList(); + final List gwtDependencies = XmlUtils.findElements( + "/configuration/gwt/dependencies/dependency", configuration); + for (final Element dependencyElement : gwtDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + projectOperations.addDependencies( + projectOperations.getFocusedModuleName(), dependencies); + } + + private void updateProperties(final Element configuration, + final boolean isGaeEnabled) { + // Update the POM + final String xPathExpression = "/configuration/" + + (isGaeEnabled ? "gae" : "gwt") + "/properties/*"; + final List propertyElements = XmlUtils.findElements( + xPathExpression, configuration); + for (final Element property : propertyElements) { + projectOperations.addProperty(projectOperations + .getFocusedModuleName(), new Property(property)); + } + } + + /** + * Updates the Eclipse plugin in the POM with the necessary GWT details + */ + private void updateEclipsePlugin() { + // Load the POM + final String pom = getPomPath(); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom)); + final Element root = document.getDocumentElement(); + + // Add the GWT "buildCommand" + final Element additionalBuildCommandsElement = XmlUtils + .findFirstElement(MAVEN_ECLIPSE_PLUGIN + + "/configuration/additionalBuildcommands", root); + Validate.notNull(additionalBuildCommandsElement, + "additionalBuildcommands element of the maven-eclipse-plugin required"); + Element gwtBuildCommandElement = XmlUtils.findFirstElement( + "buildCommand[name = '" + GWT_BUILD_COMMAND + "']", + additionalBuildCommandsElement); + if (gwtBuildCommandElement == null) { + gwtBuildCommandElement = DomUtils.createChildElement( + "buildCommand", additionalBuildCommandsElement, document); + final Element nameElement = DomUtils.createChildElement("name", + gwtBuildCommandElement, document); + nameElement.setTextContent(GWT_BUILD_COMMAND); + } + + // Add the GWT "projectnature" + final Element additionalProjectNaturesElement = XmlUtils + .findFirstElement(MAVEN_ECLIPSE_PLUGIN + + "/configuration/additionalProjectnatures", root); + Validate.notNull(additionalProjectNaturesElement, + "additionalProjectnatures element of the maven-eclipse-plugin required"); + Element gwtProjectNatureElement = null; + List gwtProjectNatureElements = XmlUtils.findElements( + "projectnature", additionalProjectNaturesElement); + for (Element element : gwtProjectNatureElements) { + if (GWT_PROJECT_NATURE.equals(element.getTextContent())) { + gwtProjectNatureElement = element; + break; + } + } + if (gwtProjectNatureElement == null) { + gwtProjectNatureElement = new XmlElementBuilder("projectnature", + document).setText(GWT_PROJECT_NATURE).build(); + additionalProjectNaturesElement + .appendChild(gwtProjectNatureElement); + } + + fileManager.createOrUpdateTextFileIfRequired(pom, + XmlUtils.nodeToString(document), false); + } + + private void updateFile(final String sourceAntPath, String targetDirectory, + final String segmentPackage, final boolean overwrite) { + if (!targetDirectory.endsWith(File.separator)) { + targetDirectory += File.separator; + } + if (!fileManager.exists(targetDirectory)) { + fileManager.createDirectory(targetDirectory); + } + + final String path = FileUtils.getPath(getClass(), sourceAntPath); + final Iterable urls = OSGiUtils.findEntriesByPattern( + context.getBundleContext(), path); + Validate.notNull(urls, + "Could not search bundles for resources for Ant Path '%s'", + path); + + for (final URL url : urls) { + String fileName = url.getPath().substring( + url.getPath().lastIndexOf('/') + 1); + fileName = fileName.replace("-template", ""); + final String targetFilename = targetDirectory + fileName; + + InputStream inputStream = null; + OutputStream outputStream = null; + try { + if (fileManager.exists(targetFilename) && !overwrite) { + continue; + } + if (targetFilename.endsWith("png")) { + inputStream = url.openStream(); + outputStream = fileManager.createFile(targetFilename) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + else { + // Read template and insert the user's package + String input = IOUtils.toString(url); + input = processTemplate(input, segmentPackage); + + // Output the file for the user + fileManager.createOrUpdateTextFileIfRequired( + targetFilename, input, true); + } + } + catch (final IOException e) { + throw new IllegalStateException("Unable to create '" + + targetFilename + "'", e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + @Override + public void updateGaeConfiguration() { + final boolean isGaeEnabled = projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE); + final boolean hasGaeStateChanged = wasGaeEnabled == null + || isGaeEnabled != wasGaeEnabled; + if (!isInstalledInModule(projectOperations.getFocusedModuleName()) + || !hasGaeStateChanged) { + return; + } + + wasGaeEnabled = isGaeEnabled; + + // Update the GaeHelper type + updateGaeHelper(); + + gwtTypeService.buildType(GwtType.APP_REQUEST_FACTORY, + gwtTemplateService.getStaticTemplateTypeDetails( + GwtType.APP_REQUEST_FACTORY, projectOperations + .getFocusedProjectMetadata().getModuleName()), + projectOperations.getFocusedModuleName()); + + // Ensure the gwt-maven-plugin appropriate to a GAE enabled or disabled + // environment is updated + updateBuildPlugins(isGaeEnabled); + + // If there is a class that could possibly import from the appengine + // sdk, denoted here as having Gae in the type name, + // then we need to add the appengine-api-1.0-sdk dependency to the + // pom.xml file + final String rootPath = projectOperations.getPathResolver() + .getFocusedRoot(ROOT); + final Set files = fileManager.findMatchingAntPath(rootPath + + "/**/*Gae*.java"); + if (!files.isEmpty()) { + final Element configuration = XmlUtils.getConfiguration(getClass()); + final Element gaeDependency = XmlUtils + .findFirstElement( + "/configuration/gae/dependencies/dependency", + configuration); + projectOperations.addDependency(projectOperations + .getFocusedModuleName(), new Dependency(gaeDependency)); + } + + // Copy across any missing files, only if GAE state has changed and is + // now enabled + if (isGaeEnabled) { + copyDirectoryContents(); + } + } + + private void updateGaeHelper() { + final String sourceAntPath = "module/client/scaffold/gae/GaeHelper-template.java"; + final String segmentPackage = "client.scaffold.gae"; + final String targetDirectory = projectOperations.getPathResolver() + .getFocusedIdentifier( + SRC_MAIN_JAVA, + projectOperations + .getTopLevelPackage( + projectOperations + .getFocusedModuleName()) + .getFullyQualifiedPackageName() + .replace('.', File.separatorChar) + + File.separator + + "client" + + File.separator + + "scaffold" + File.separator + "gae"); + updateFile(sourceAntPath, targetDirectory, segmentPackage, true); + } + + private void updateRepositories(final Element configuration) { + final List repositories = new ArrayList(); + + final List gwtRepositories = XmlUtils.findElements( + "/configuration/gwt/repositories/repository", configuration); + for (final Element repositoryElement : gwtRepositories) { + repositories.add(new Repository(repositoryElement)); + } + projectOperations.addRepositories( + projectOperations.getFocusedModuleName(), repositories); + + repositories.clear(); + final List gwtPluginRepositories = XmlUtils.findElements( + "/configuration/gwt/pluginRepositories/pluginRepository", + configuration); + for (final Element repositoryElement : gwtPluginRepositories) { + repositories.add(new Repository(repositoryElement)); + } + projectOperations.addPluginRepositories( + projectOperations.getFocusedModuleName(), repositories); + } + + private void updateScaffoldBoilerPlate() { + final String targetDirectory = projectOperations.getPathResolver() + .getFocusedIdentifier( + SRC_MAIN_JAVA, + projectOperations + .getTopLevelPackage( + projectOperations + .getFocusedModuleName()) + .getFullyQualifiedPackageName() + .replace('.', File.separatorChar)); + deleteUntouchedSetupFiles("setup/*", targetDirectory); + deleteUntouchedSetupFiles("setup/client/*", targetDirectory + "/client"); + copyDirectoryContents(); + updateGaeHelper(); + } + + private void updateWebXml() { + final String webXmlpath = projectOperations.getPathResolver() + .getFocusedIdentifier(SRC_MAIN_WEBAPP, "WEB-INF/web.xml"); + final Document webXml = XmlUtils.readXml(fileManager + .getInputStream(webXmlpath)); + final Element root = webXml.getDocumentElement(); + + WebXmlUtils.addServlet( + "requestFactory", + projectOperations.getTopLevelPackage(projectOperations + .getFocusedModuleName()) + + ".server.CustomRequestFactoryServlet", "/gwtRequest", + null, webXml, null); + if (projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE)) { + WebXmlUtils + .addFilter( + "GaeAuthFilter", + GwtPath.SERVER_GAE.packageName(projectOperations + .getTopLevelPackage(projectOperations + .getFocusedModuleName())) + + ".GaeAuthFilter", + "/gwtRequest/*", + webXml, + "This filter makes GAE authentication services visible to a RequestFactory client."); + final String displayName = "Redirect to the login page if needed before showing any html pages"; + final WebXmlUtils.WebResourceCollection webResourceCollection = new WebXmlUtils.WebResourceCollection( + "Login required", null, + Collections.singletonList("*.html"), + new ArrayList()); + final ArrayList roleNames = new ArrayList(); + roleNames.add("*"); + final String userDataConstraint = null; + WebXmlUtils.addSecurityConstraint(displayName, + Collections.singletonList(webResourceCollection), + roleNames, userDataConstraint, webXml, null); + } + else { + final Element filter = XmlUtils.findFirstElement( + "/web-app/filter[filter-name = 'GaeAuthFilter']", root); + if (filter != null) { + filter.getParentNode().removeChild(filter); + } + final Element filterMapping = XmlUtils.findFirstElement( + "/web-app/filter-mapping[filter-name = 'GaeAuthFilter']", + root); + if (filterMapping != null) { + filterMapping.getParentNode().removeChild(filterMapping); + } + final Element securityConstraint = XmlUtils.findFirstElement( + "security-constraint", root); + if (securityConstraint != null) { + securityConstraint.getParentNode().removeChild( + securityConstraint); + } + } + + removeIfFound("/web-app/error-page", root); + + fileManager.createOrUpdateTextFileIfRequired(webXmlpath, + XmlUtils.nodeToString(webXml), false); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtPath.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtPath.java new file mode 100644 index 000000000..de74ff6cf --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtPath.java @@ -0,0 +1,107 @@ +package org.springframework.roo.addon.gwt; + +import java.io.File; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaPackage; + +public enum GwtPath { + + CLIENT("/client", "module/client/" + GwtPath.templateSelector), GWT_ROOT( + "/", "module/" + GwtPath.templateSelector), IMAGES( + "/client/style/images", "module/client/style/images/" + + GwtPath.wildCardSelector), LOCATOR("/server/locator", + "module/server/locator/" + GwtPath.templateSelector), // GWT_REQUEST + MANAGED("/client/managed", "module/client/managed/" + + GwtPath.templateSelector), MANAGED_ACTIVITY( + "/client/managed/activity", "module/client/managed/activity/" + + GwtPath.templateSelector), // GWT_SCAFFOLD + MANAGED_REQUEST("/client/managed/request", "module/client/request/" + + GwtPath.templateSelector), // GWT_SCAFFOLD_GENERATED + MANAGED_UI("/client/managed/ui", "module/client/managed/ui/" + + GwtPath.templateSelector), // GWT_SCAFFOLD_UI + MANAGED_UI_DESKTOP("/client/managed/ui/desktop", + "module/client/managed/ui/desktop/" + GwtPath.templateSelector), // GWT_SCAFFOLD_UI + MANAGED_UI_MOBILE("/client/managed/ui/mobile", + "module/client/managed/ui/mobile/" + GwtPath.templateSelector), // GWT_SCAFFOLD_UI + MANAGED_UI_RENDERER("/client/managed/ui/renderer", + "module/client/managed/ui/renderer/" + GwtPath.templateSelector), // GWT_SCAFFOLD_UI + MANAGED_UI_EDITOR("/client/managed/ui/editor", + "module/client/managed/ui/editor/" + GwtPath.templateSelector), // GWT_SCAFFOLD_UI + SCAFFOLD("/client/scaffold", "module/client/scaffold/" + + GwtPath.templateSelector), SCAFFOLD_ACTIVITY( + "/client/scaffold/activity", "module/client/scaffold/activity/" + + GwtPath.templateSelector), SCAFFOLD_GAE( + "/client/scaffold/gae", "module/client/scaffold/gae/" + + GwtPath.templateSelector), SCAFFOLD_IOC( + "/client/scaffold/ioc", "module/client/scaffold/ioc/" + + GwtPath.templateSelector), SCAFFOLD_PLACE( + "/client/scaffold/place", "module/client/scaffold/place/" + + GwtPath.templateSelector), SCAFFOLD_REQUEST( + "/client/scaffold/request", "module/client/scaffold/request/" + + GwtPath.templateSelector), SCAFFOLD_UI( + "/client/scaffold/ui", "module/client/scaffold/ui/" + + GwtPath.templateSelector), SERVER("/server", + "module/server/" + GwtPath.templateSelector), // IOC + SERVER_GAE("/server/gae", "module/server/gae/" + GwtPath.templateSelector), // PLACE + SHARED("/shared", "module/shared/" + GwtPath.templateSelector), SHARED_GAE( + "/shared/gae", "module/shared/gae/" + GwtPath.templateSelector), SHARED_SCAFFOLD( + "/shared/scaffold", "module/shared/scaffold/" + + GwtPath.templateSelector), STYLE("/client/style", + "module/client/style/" + GwtPath.templateSelector), WEB("", + "webapp/" + GwtPath.wildCardSelector); + + private static final String templateSelector = "*-template.*"; + private static final String wildCardSelector = "*"; + + private final String segmentName; + private final String sourceAntPath; + + /** + * Constructor + * + * @param segmentName + * @param sourceAntPath the Ant-style path to the source files for this + * {@link GwtPath}, relative to the package in which this enum is + * located (required) + */ + GwtPath(final String segmentName, final String sourceAntPath) { + Validate.notBlank(sourceAntPath, "Source Ant path is required"); + this.segmentName = segmentName; + this.sourceAntPath = sourceAntPath; + } + + public String getPackagePath(final JavaPackage topLevelPackage) { + return topLevelPackage.getFullyQualifiedPackageName().replace('.', + File.separatorChar) + + segmentName.replace('/', File.separatorChar); + } + + /** + * Package access for benefit of unit test + * + * @return + */ + String getSegmentName() { + return segmentName; + } + + public String getSourceAntPath() { + return sourceAntPath; + } + + public String packageName(final JavaPackage topLevelPackage) { + if (WEB.equals(this)) { + return ""; + } + return topLevelPackage.getFullyQualifiedPackageName() + + segmentName.replace('/', '.'); + } + + public String segmentPackage() { + if (WEB.equals(this)) { + return ""; + } + return segmentName.substring(1).replace('/', '.'); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtProxyProperty.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtProxyProperty.java new file mode 100644 index 000000000..5ab6e459e --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtProxyProperty.java @@ -0,0 +1,401 @@ +package org.springframework.roo.addon.gwt; + +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.BIG_INTEGER; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JpaJavaType.EMBEDDABLE; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; +import static org.springframework.roo.model.SpringJavaType.NUMBER_FORMAT; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public class GwtProxyProperty { + + public static String getProxyRendererType( + final JavaPackage topLevelPackage, final JavaType javaType) { + return GwtType.EDIT_RENDERER.getPath().packageName(topLevelPackage) + + "." + javaType.getSimpleTypeName() + "Renderer"; + } + + private List annotations; + private String getter; + private String name; + private final ClassOrInterfaceTypeDetails ptmd; + private final JavaPackage topLevelPackage; + + private final JavaType type; + + public GwtProxyProperty(final JavaPackage topLevelPackage, + final ClassOrInterfaceTypeDetails ptmd, final JavaType type) { + Validate.notNull(type, "Type required"); + this.topLevelPackage = topLevelPackage; + this.ptmd = ptmd; + this.type = type; + } + + public GwtProxyProperty(final JavaPackage topLevelPackage, + final ClassOrInterfaceTypeDetails ptmd, final JavaType type, + final String name, final List annotations, + final String getter) { + this(topLevelPackage, ptmd, type); + this.name = name; + this.annotations = annotations; + this.getter = getter; + } + + public String forEditView() { + String initializer = ""; + + if (isBoolean()) { + initializer = " = " + getCheckboxSubtype(); + } + + if (isEnum() && !isCollection()) { + initializer = String.format(" = new ValueListBox<%s>(%s)", + type.getFullyQualifiedTypeName(), getRenderer()); + } + + if (isProxy()) { + initializer = String + .format(" = new ValueListBox<%1$s>(%2$s.instance(), new com.google.web.bindery.requestfactory.gwt.ui.client.EntityProxyKeyProvider<%1$s>())", + type.getFullyQualifiedTypeName(), + getProxyRendererType()); + } + + return String.format("@UiField %s %s %s", getEditor(), getName(), + initializer); + } + + public String forMobileListView(final String rendererName) { + return new StringBuilder("if (value.").append(getGetter()) + .append("() != null) {\n\t\t\t\tsb.appendEscaped(") + .append(rendererName).append(".render(value.") + .append(getGetter()).append("()));\n\t\t\t}").toString(); + } + + public String getBinder() { + if (type.equals(JavaType.DOUBLE_OBJECT)) { + return "g:DoubleBox"; + } + if (type.equals(LONG_OBJECT)) { + return "g:LongBox"; + } + if (type.equals(JavaType.INT_OBJECT)) { + return "g:IntegerBox"; + } + if (type.equals(JavaType.FLOAT_OBJECT)) { + return "r:FloatBox"; + } + if (type.equals(JavaType.BYTE_OBJECT)) { + return "r:ByteBox"; + } + if (type.equals(JavaType.SHORT_OBJECT)) { + return "r:ShortBox"; + } + if (type.equals(JavaType.CHAR_OBJECT)) { + return "r:CharBox"; + } + if (type.equals(BIG_DECIMAL)) { + return "r:BigDecimalBox"; + } + return isCollection() ? "e:" + getSetEditor() : isDate() ? "d:DateBox" + : isBoolean() ? "g:CheckBox" : isString() ? "g:TextBox" + : "g:ValueListBox"; + } + + public String getCheckboxSubtype() { + // TODO: Ugly hack, fix in M4 + return "new CheckBox() { public void setValue(Boolean value) { super.setValue(value == null ? Boolean.FALSE : value); } }"; + } + + public String getCollectionRenderer() { + JavaType arg = OBJECT; + if (type.getParameters().size() > 0) { + arg = type.getParameters().get(0); + } + return GwtPath.SCAFFOLD_PLACE.packageName(topLevelPackage) + + ".CollectionRenderer.of(" + + new GwtProxyProperty(topLevelPackage, ptmd, arg) + .getRenderer() + ")"; + } + + private String getDateTimeFormat() { + String format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_SHORT)"; + if (annotations == null || annotations.isEmpty()) { + return format; + } + + String style = ""; + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(annotations, DATE_TIME_FORMAT); + if (annotation != null) { + final AnnotationAttributeValue attr = annotation + .getAttribute(new JavaSymbolName("style")); + if (attr != null) { + style = (String) attr.getValue(); + } + } + if (StringUtils.isNotBlank(style)) { + if (style.equals("S")) { + format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_TIME_SHORT)"; + } + else if (style.equals("M")) { + format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_TIME_MEDIUM)"; + } + else if (style.equals("F")) { + format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_TIME_FULL)"; + } + else if (style.equals("S-")) { + format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_SHORT)"; + } + else if (style.equals("M-")) { + format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_MEDIUM)"; + } + else if (style.equals("F-")) { + format = "DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_FULL)"; + } + } + return format; + } + + private String getEditor() { + if (type.equals(JavaType.DOUBLE_OBJECT)) { + return "DoubleBox"; + } + if (type.equals(LONG_OBJECT)) { + return "LongBox"; + } + if (type.equals(JavaType.INT_OBJECT)) { + return "IntegerBox"; + } + if (type.equals(JavaType.FLOAT_OBJECT)) { + return "FloatBox"; + } + if (type.equals(JavaType.BYTE_OBJECT)) { + return "ByteBox"; + } + if (type.equals(JavaType.SHORT_OBJECT)) { + return "ShortBox"; + } + if (type.equals(JavaType.CHAR_OBJECT)) { + return "CharBox"; + } + if (type.equals(BIG_DECIMAL)) { + return "BigDecimalBox"; + } + if (isBoolean()) { + return "(provided = true) CheckBox"; + } + return isCollection() ? getSetEditor() : isDate() ? "DateBox" + : isString() ? "TextBox" : "(provided = true) ValueListBox<" + + type.getFullyQualifiedTypeName() + ">"; + } + + public String getFormatter() { + if (isCollectionOfProxy()) { + return getCollectionRenderer() + ".render"; + } + else if (isDate()) { + return getDateTimeFormat() + ".format"; + } + else if (type.equals(JavaType.INT_OBJECT) + || type.equals(JavaType.FLOAT_OBJECT) + || type.equals(JavaType.DOUBLE_OBJECT) + || type.equals(BIG_INTEGER) || type.equals(BIG_DECIMAL)) { + String formatter = "String.valueOf"; + if (annotations == null || annotations.isEmpty()) { + return formatter; + } + + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(annotations, NUMBER_FORMAT); + if (annotation != null) { + final AnnotationAttributeValue attr = annotation + .getAttribute(new JavaSymbolName("style")); + if (attr != null) { + final String style = attr.getValue().toString(); + if ("org.springframework.format.annotation.NumberFormat.Style.CURRENCY" + .equals(style)) { + formatter = "NumberFormat.getCurrencyFormat().format"; + } + else if ("org.springframework.format.annotation.NumberFormat.Style.PERCENT" + .equals(style)) { + formatter = "NumberFormat.getPercentFormat().format"; + } + else { + formatter = "NumberFormat.getDecimalFormat().format"; + } + } + else { + formatter = "NumberFormat.getDecimalFormat().format"; + } + } + return formatter; + } + else if (isProxy()) { + return getProxyRendererType() + ".instance().render"; + } + else { + return "String.valueOf"; + } + } + + public String getGetter() { + return getter; + } + + public String getName() { + return name; + } + + public JavaType getPropertyType() { + return type; + } + + String getProxyRendererType() { + return getProxyRendererType(topLevelPackage, + isCollectionOfProxy() ? type.getParameters().get(0) : type); + } + + public String getReadableName() { + return new JavaSymbolName(name).getReadableSymbolName(); + } + + public String getRenderer() { + return isCollection() ? getCollectionRenderer() + : isDate() ? "new DateTimeFormatRenderer(" + + getDateTimeFormat() + ")" + : isPrimitive() || isEnum() || isEmbeddable() + || type.equals(OBJECT) ? "new AbstractRenderer<" + + getType() + + ">() {\n public String render(" + + getType() + + " obj) {\n return obj == null ? \"\" : String.valueOf(obj);\n }\n }" + : getProxyRendererType() + ".instance()"; + } + + private String getSetEditor() { + String typeName = OBJECT.getFullyQualifiedTypeName(); + if (type.getParameters().size() > 0) { + typeName = type.getParameters().get(0).getSimpleTypeName(); + } + if (typeName.endsWith(GwtType.PROXY.getSuffix())) { + typeName = typeName.substring(0, typeName.length() + - GwtType.PROXY.getSuffix().length()); + } + return typeName + + (type.getSimpleTypeName().equals("Set") ? GwtType.SET_EDITOR + .getSuffix() : GwtType.LIST_EDITOR.getSuffix()); + } + + public JavaType getSetEditorType() { + return new JavaType(GwtType.SET_EDITOR.getPath().packageName( + topLevelPackage) + + "." + getSetEditor()); + } + + public String getSetValuePickerMethod() { + return "\tpublic void " + + getSetValuePickerMethodName() + + "(Collection<" + + (isCollection() ? type.getParameters().get(0) + .getSimpleTypeName() : type.getSimpleTypeName()) + + "> values) {\n" + "\t\t" + getName() + + ".setAcceptableValues(values);\n" + "\t}\n"; + } + + public String getSetEmptyValuePickerMethod() { + return "\tpublic void " + + getSetValuePickerMethodName() + + "(Collection<" + + (isCollection() ? type.getParameters().get(0) + .getSimpleTypeName() : type.getSimpleTypeName()) + + "> values) { }"; + } + + String getSetValuePickerMethodName() { + return "set" + StringUtils.capitalize(getName()) + "PickerValues"; + } + + public String getType() { + return type.getFullyQualifiedTypeName(); + } + + public JavaType getValueType() { + if (isCollection()) { + return type.getParameters().get(0); + } + return type; + } + + public boolean isBoolean() { + return type.equals(JavaType.BOOLEAN_OBJECT); + } + + public boolean isCollection() { + return type.isCommonCollectionType(); + } + + public boolean isCollectionOfProxy() { + return type.getParameters().size() != 0 + && isCollection() + && new GwtProxyProperty(topLevelPackage, ptmd, type + .getParameters().get(0)).isProxy(); + } + + public boolean isDate() { + return type.equals(DATE); + } + + public boolean isEmbeddable() { + if (ptmd != null) { + final List annotations = ptmd.getAnnotations(); + for (final AnnotationMetadata annotation : annotations) { + if (annotation.getAnnotationType().equals(EMBEDDABLE)) { + return true; + } + } + } + + return false; + } + + boolean isEnum() { + return ptmd != null + && ptmd.getPhysicalTypeCategory() == PhysicalTypeCategory.ENUMERATION; + } + + public boolean isPrimitive() { + return type.isPrimitive() || isDate() || isString() || isBoolean() + || type.equals(JavaType.DOUBLE_OBJECT) + || type.equals(LONG_OBJECT) || type.equals(JavaType.INT_OBJECT) + || type.equals(JavaType.FLOAT_OBJECT) + || type.equals(JavaType.BYTE_OBJECT) + || type.equals(JavaType.SHORT_OBJECT) + || type.equals(JavaType.CHAR_OBJECT) + || type.equals(BIG_DECIMAL); + } + + public boolean isProxy() { + return ptmd != null && !isDate() && !isString() && !isPrimitive() + && !isEnum() && !isCollection() && !isEmbeddable() + && !type.equals(OBJECT); + } + + public boolean isString() { + return type.equals(JavaType.STRING); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateDataHolder.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateDataHolder.java new file mode 100644 index 000000000..7915226a4 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateDataHolder.java @@ -0,0 +1,47 @@ +package org.springframework.roo.addon.gwt; + +import java.util.List; +import java.util.Map; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; + +/** + * Holder for types and xml files created via {@link GwtTemplateService}. + * + * @author James Tyrrell + * @since 1.1.2 + */ +public class GwtTemplateDataHolder { + + private final Map templateTypeDetailsMap; + private final List typeList; + private final Map xmlMap; + private final Map xmlTemplates; + + public GwtTemplateDataHolder( + final Map templateTypeDetailsMap, + final Map xmlTemplates, + final List typeList, + final Map xmlMap) { + this.templateTypeDetailsMap = templateTypeDetailsMap; + this.xmlTemplates = xmlTemplates; + this.typeList = typeList; + this.xmlMap = xmlMap; + } + + public Map getTemplateTypeDetailsMap() { + return templateTypeDetailsMap; + } + + public List getTypeList() { + return typeList; + } + + public Map getXmlMap() { + return xmlMap; + } + + public Map getXmlTemplates() { + return xmlTemplates; + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateService.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateService.java new file mode 100644 index 000000000..1786ea2be --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateService.java @@ -0,0 +1,35 @@ +package org.springframework.roo.addon.gwt; + +import java.util.List; +import java.util.Map; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Interface for {@link GwtTemplateServiceImpl}. + * + * @author James Tyrrell + * @since 1.1.2 + */ +public interface GwtTemplateService { + + String buildUiXml(String templateContents, String destFile, + List proxyMethods); + + GwtTemplateDataHolder getMirrorTemplateTypeDetails( + ClassOrInterfaceTypeDetails governorTypeDetails, + Map clientSideTypeMap, + String moduleName); + + List getStaticTemplateTypeDetails( + GwtType type, String moduleName); + + public void addLocatorToXmlConfiguration( + ClassOrInterfaceTypeDetails locator, JavaType service); + + public void removeLocatorFromXmlConfiguration( + ClassOrInterfaceTypeDetails locator); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateServiceImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateServiceImpl.java new file mode 100644 index 000000000..9ca3b5c6d --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTemplateServiceImpl.java @@ -0,0 +1,1420 @@ +package org.springframework.roo.addon.gwt; + +import static org.springframework.roo.addon.gwt.GwtJavaType.INSTANCE_REQUEST; +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; +import static org.springframework.roo.model.JdkJavaType.HASH_SET; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.SET; +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; +import hapax.Template; +import hapax.TemplateDataDictionary; +import hapax.TemplateDictionary; +import hapax.TemplateException; +import hapax.TemplateLoader; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.gwt.scaffold.GwtScaffoldMetadata; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeParsingService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Provides a basic implementation of {@link GwtTemplateService} which is used + * to create {@link ClassOrInterfaceTypeDetails} objects from source files + * created from templates. This class keeps all templating concerns in one + * place. + * + * @author James Tyrrell + * @since 1.1.2 + */ +@Component +@Service +public class GwtTemplateServiceImpl implements GwtTemplateService { + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + + @Reference GwtFileManager gwtFileManager; + @Reference GwtTypeService gwtTypeService; + @Reference LayerService layerService; + @Reference MetadataService metadataService; + @Reference PersistenceMemberLocator persistenceMemberLocator; + @Reference ProjectOperations projectOperations; + @Reference TypeLocationService typeLocationService; + @Reference TypeParsingService typeParsingService; + @Reference FileManager fileManager; + + @Override + public String buildUiXml(final String templateContents, + final String destFile, final List proxyMethods) { + FileReader fileReader = null; + try { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + builder.setEntityResolver(new EntityResolver() { + @Override + public InputSource resolveEntity(final String publicId, + final String systemId) throws SAXException, IOException { + if (systemId + .equals("http://dl.google.com/gwt/DTD/xhtml.ent")) { + return new InputSource(FileUtils.getInputStream( + GwtScaffoldMetadata.class, + "templates/xhtml.ent")); + } + + // Use the default behaviour + return null; + } + }); + + InputSource source = new InputSource(); + source.setCharacterStream(new StringReader(templateContents)); + + final Document templateDocument = builder.parse(source); + + if (!new File(destFile).exists()) { + return transformXml(templateDocument); + } + + source = new InputSource(); + fileReader = new FileReader(destFile); + source.setCharacterStream(fileReader); + final Document existingDocument = builder.parse(source); + + // Look for the element holder denoted by the 'debugId' attribute + // first + Element existingHoldingElement = XmlUtils.findFirstElement( + "//*[@debugId='" + "boundElementHolder" + "']", + existingDocument.getDocumentElement()); + Element templateHoldingElement = XmlUtils.findFirstElement( + "//*[@debugId='" + "boundElementHolder" + "']", + templateDocument.getDocumentElement()); + + // If holding element isn't found then the holding element is either + // not widget based or using the old convention of 'id' so look for + // the element holder with an 'id' attribute + if (existingHoldingElement == null) { + existingHoldingElement = XmlUtils.findFirstElement("//*[@id='" + + "boundElementHolder" + "']", + existingDocument.getDocumentElement()); + } + if (templateHoldingElement == null) { + templateHoldingElement = XmlUtils.findFirstElement("//*[@id='" + + "boundElementHolder" + "']", + templateDocument.getDocumentElement()); + } + + // If holding element isn't found then the holding element is either + // not widget based or using the old convention of 'id' so look for + // the element holder with an 'id' attribute + if (existingHoldingElement == null) { + existingHoldingElement = XmlUtils.findFirstElement( + "//*[@field]", existingDocument.getDocumentElement()); + } + if (templateHoldingElement == null) { + templateHoldingElement = XmlUtils.findFirstElement( + "//*[@field]", templateDocument.getDocumentElement()); + } + + if (existingHoldingElement != null) { + + if (existingHoldingElement.hasAttribute("update")) { + if (existingHoldingElement.getAttribute("update").equals( + "false")) + return transformXml(existingDocument); + } + + final Map templateElementMap = new LinkedHashMap(); + for (final Element element : XmlUtils.findElements("//*[@id]", + templateHoldingElement)) { + templateElementMap.put(element.getAttribute("id"), element); + } + + final Map existingElementMap = new LinkedHashMap(); + for (final Element element : XmlUtils.findElements("//*[@id]", + existingHoldingElement)) { + existingElementMap.put(element.getAttribute("id"), element); + } + + if (existingElementMap.keySet().containsAll( + templateElementMap.values())) { + return transformXml(existingDocument); + } + + final List elementsToAdd = new ArrayList(); + for (final Map.Entry entry : templateElementMap + .entrySet()) { + if (!existingElementMap.keySet().contains(entry.getKey())) { + elementsToAdd.add(entry.getValue()); + } + } + + final List elementsToRemove = new ArrayList(); + for (final Map.Entry entry : existingElementMap + .entrySet()) { + if (!templateElementMap.keySet().contains(entry.getKey())) { + elementsToRemove.add(entry.getValue()); + } + } + + for (final Element element : elementsToAdd) { + final Node importedNode = existingDocument.importNode( + element, true); + existingHoldingElement.appendChild(importedNode); + } + + for (final Element element : elementsToRemove) { + /** + * @todo Sometimes elements are added to elementsToRemove + * that are not in existingHoldingElements try/catch + * is a temporary fix + */ + try { + existingHoldingElement.removeChild(element); + } + catch (DOMException ex) { + System.out.println(ex.getMessage()); + } + } + + if (elementsToAdd.size() > 0) { + final List sortedElements = new ArrayList(); + for (final MethodMetadata method : proxyMethods) { + final String propertyName = StringUtils + .uncapitalize(BeanInfoUtils + .getPropertyNameForJavaBeanMethod( + method).getSymbolName()); + final Element element = XmlUtils.findFirstElement( + String.format("//*[@id='%s']", propertyName), + existingHoldingElement); + if (element != null) { + sortedElements.add(element); + } + } + for (final Element el : sortedElements) { + if (el.getParentNode() != null + && el.getParentNode().equals( + existingHoldingElement)) { + existingHoldingElement.removeChild(el); + } + } + for (final Element el : sortedElements) { + existingHoldingElement.appendChild(el); + } + } + + return transformXml(existingDocument); + } + + return transformXml(templateDocument); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(fileReader); + } + } + + @Override + public GwtTemplateDataHolder getMirrorTemplateTypeDetails( + final ClassOrInterfaceTypeDetails mirroredType, + final Map clientSideTypeMap, + final String moduleName) { + final ClassOrInterfaceTypeDetails proxy = gwtTypeService + .lookupProxyFromEntity(mirroredType); + final ClassOrInterfaceTypeDetails request = gwtTypeService + .lookupUnmanagedRequestFromEntity(mirroredType); + final JavaPackage topLevelPackage = projectOperations + .getTopLevelPackage(moduleName); + final Map mirrorTypeMap = GwtUtils.getMirrorTypeMap( + mirroredType.getName(), topLevelPackage); + mirrorTypeMap.put(GwtType.PROXY, proxy.getName()); + mirrorTypeMap.put(GwtType.REQUEST, request.getName()); + + final Map templateTypeDetailsMap = new LinkedHashMap(); + final Map xmlTemplates = new LinkedHashMap(); + for (final GwtType gwtType : GwtType.getMirrorTypes()) { + if (gwtType.getTemplate() == null) { + continue; + } + TemplateDataDictionary dataDictionary = buildMirrorDataDictionary( + gwtType, mirroredType, proxy, mirrorTypeMap, + clientSideTypeMap, moduleName); + gwtType.dynamicallyResolveFieldsToWatch(clientSideTypeMap); + gwtType.dynamicallyResolveMethodsToWatch(mirroredType.getName(), + clientSideTypeMap, topLevelPackage); + templateTypeDetailsMap.put( + gwtType, + getTemplateDetails(dataDictionary, gwtType.getTemplate(), + mirrorTypeMap.get(gwtType), moduleName)); + + if (gwtType.isCreateUiXml()) { + dataDictionary = buildMirrorDataDictionary(gwtType, + mirroredType, proxy, mirrorTypeMap, clientSideTypeMap, + moduleName); + final String contents = getTemplateContents( + gwtType.getTemplate() + "UiXml", dataDictionary); + xmlTemplates.put(gwtType, contents); + } + } + + final Map xmlMap = new LinkedHashMap(); + final List typeDetails = new ArrayList(); + for (final GwtProxyProperty proxyProperty : clientSideTypeMap.values()) { + if (!proxyProperty.isCollection() + || proxyProperty.isCollectionOfProxy()) { + continue; + } + + TemplateDataDictionary dataDictionary = TemplateDictionary.create(); + dataDictionary.setVariable("packageName", + GwtPath.MANAGED_UI_EDITOR.packageName(topLevelPackage)); + dataDictionary.setVariable("scaffoldUiPackage", + GwtPath.SCAFFOLD_UI.packageName(topLevelPackage)); + final JavaType collectionTypeImpl = getCollectionImplementation(proxyProperty + .getPropertyType()); + addImport(dataDictionary, collectionTypeImpl); + addImport(dataDictionary, proxyProperty.getPropertyType()); + + final String collectionType = proxyProperty.getPropertyType() + .getSimpleTypeName(); + final String boundCollectionType = proxyProperty.getPropertyType() + .getParameters().get(0).getSimpleTypeName(); + + dataDictionary.setVariable("collectionType", collectionType); + dataDictionary.setVariable("collectionTypeImpl", + collectionTypeImpl.getSimpleTypeName()); + dataDictionary.setVariable("boundCollectionType", + boundCollectionType); + + final JavaType collectionEditorType = new JavaType( + GwtPath.MANAGED_UI_EDITOR.packageName(topLevelPackage) + + "." + boundCollectionType + collectionType + + "Editor"); + typeDetails.add(getTemplateDetails(dataDictionary, + "CollectionEditor", collectionEditorType, moduleName)); + + dataDictionary = TemplateDictionary.create(); + dataDictionary.setVariable("packageName", + GwtPath.MANAGED_UI_EDITOR.packageName(topLevelPackage)); + dataDictionary.setVariable("scaffoldUiPackage", + GwtPath.SCAFFOLD_UI.packageName(topLevelPackage)); + dataDictionary.setVariable("collectionType", collectionType); + dataDictionary.setVariable("collectionTypeImpl", + collectionTypeImpl.getSimpleTypeName()); + dataDictionary.setVariable("boundCollectionType", + boundCollectionType); + addImport(dataDictionary, proxyProperty.getPropertyType()); + + String inputType; + boolean isSimpleType = false; + // If collection is a simple type, e.g. String, Double, Float, a + // ValueListBox will not work + if (boundCollectionType.equals("String")) { + inputType = "TextBox"; + isSimpleType = true; + } + else if (boundCollectionType.equals("Double")) { + inputType = "DoubleBox"; + isSimpleType = true; + } + else if (boundCollectionType.equals("Float")) { + inputType = "DoubleBox"; + isSimpleType = true; + } + else if (boundCollectionType.equals("Integer")) { + inputType = "IntegerBox"; + isSimpleType = true; + } + else if (boundCollectionType.equals("Long")) { + inputType = "LongBox"; + } + else { + inputType = "ValueListBox"; + } + + dataDictionary.setVariable("inputType", inputType); + + // Different templates for simple and complex editors + String editorType = isSimpleType ? "SimpleCollectionEditor" + : "CollectionEditor"; + + final String contents = getTemplateContents(editorType + "UiXml", + dataDictionary); + final String packagePath = projectOperations.getPathResolver() + .getFocusedIdentifier( + Path.SRC_MAIN_JAVA, + GwtPath.MANAGED_UI_EDITOR + .getPackagePath(topLevelPackage)); + xmlMap.put(packagePath + "/" + boundCollectionType + collectionType + + "Editor.ui.xml", contents); + } + + return new GwtTemplateDataHolder(templateTypeDetailsMap, xmlTemplates, + typeDetails, xmlMap); + } + + @Override + public List getStaticTemplateTypeDetails( + final GwtType type, final String moduleName) { + final List templateTypeDetails = new ArrayList(); + final TemplateDataDictionary dataDictionary = buildDictionary(type, + moduleName); + templateTypeDetails.add(getTemplateDetails(dataDictionary, + type.getTemplate(), getDestinationJavaType(type, moduleName), + moduleName)); + return templateTypeDetails; + } + + public ClassOrInterfaceTypeDetails getTemplateDetails( + final TemplateDataDictionary dataDictionary, + final String templateFile, final JavaType templateType, + final String moduleName) { + try { + final TemplateLoader templateLoader = TemplateResourceLoader + .create(); + final Template template = templateLoader.getTemplate(templateFile); + Validate.notNull(template, "Template required for '%s'", + templateFile); + final String templateContents = template + .renderToString(dataDictionary); + final String templateId = PhysicalTypeIdentifier.createIdentifier( + templateType, + LogicalPath.getInstance(Path.SRC_MAIN_JAVA, moduleName)); + return typeParsingService.getTypeFromString(templateContents, + templateId, templateType); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + private void addImport(final TemplateDataDictionary dataDictionary, + final JavaType type) { + dataDictionary.addSection("imports").setVariable("import", + type.getFullyQualifiedTypeName()); + for (final JavaType param : type.getParameters()) { + addImport(dataDictionary, param.getFullyQualifiedTypeName()); + } + } + + private void addImport(final TemplateDataDictionary dataDictionary, + final String importDeclaration) { + dataDictionary.addSection("imports").setVariable("import", + importDeclaration); + } + + private void addImport(final TemplateDataDictionary dataDictionary, + final String simpleName, final GwtType gwtType, + final String moduleName) { + addImport( + dataDictionary, + gwtType.getPath().packageName( + projectOperations.getTopLevelPackage(moduleName)) + + "." + simpleName + gwtType.getSuffix()); + } + + private void addReference(final TemplateDataDictionary dataDictionary, + final GwtType type, final Map mirrorTypeMap) { + addImport(dataDictionary, mirrorTypeMap.get(type) + .getFullyQualifiedTypeName()); + dataDictionary.setVariable(type.getName(), mirrorTypeMap.get(type) + .getSimpleTypeName()); + } + + private void addReference(final TemplateDataDictionary dataDictionary, + final GwtType type, final String moduleName) { + addImport(dataDictionary, getDestinationJavaType(type, moduleName) + .getFullyQualifiedTypeName()); + dataDictionary.setVariable(type.getName(), + getDestinationJavaType(type, moduleName).getSimpleTypeName()); + } + + private TemplateDataDictionary buildDictionary(final GwtType type, + final String moduleName) { + final Set proxies = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_GWT_PROXY); + final TemplateDataDictionary dataDictionary = buildStandardDataDictionary( + type, moduleName); + switch (type) { + case APP_ENTITY_TYPES_PROCESSOR: + for (final ClassOrInterfaceTypeDetails proxy : proxies) { + if (!GwtUtils.scaffoldProxy(proxy)) { + continue; + } + final String proxySimpleName = proxy.getName() + .getSimpleTypeName(); + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromProxy(proxy); + if (entity != null) { + final String entitySimpleName = entity.getName() + .getSimpleTypeName(); + + dataDictionary.addSection("proxys").setVariable("proxy", + proxySimpleName); + + final String entity1 = new StringBuilder("\t\tif (") + .append(proxySimpleName) + .append(".class.equals(clazz)) {\n\t\t\tprocessor.handle") + .append(entitySimpleName).append("((") + .append(proxySimpleName) + .append(") null);\n\t\t\treturn;\n\t\t}") + .toString(); + dataDictionary.addSection("entities1").setVariable( + "entity", entity1); + + final String entity2 = new StringBuilder( + "\t\tif (proxy instanceof ") + .append(proxySimpleName) + .append(") {\n\t\t\tprocessor.handle") + .append(entitySimpleName).append("((") + .append(proxySimpleName) + .append(") proxy);\n\t\t\treturn;\n\t\t}") + .toString(); + dataDictionary.addSection("entities2").setVariable( + "entity", entity2); + + final String entity3 = new StringBuilder( + "\tpublic abstract void handle") + .append(entitySimpleName).append("(") + .append(proxySimpleName).append(" proxy);") + .toString(); + dataDictionary.addSection("entities3").setVariable( + "entity", entity3); + addImport(dataDictionary, proxy.getName() + .getFullyQualifiedTypeName()); + } + } + break; + case MASTER_ACTIVITIES: + for (final ClassOrInterfaceTypeDetails proxy : proxies) { + if (!GwtUtils.scaffoldProxy(proxy)) { + continue; + } + final String proxySimpleName = proxy.getName() + .getSimpleTypeName(); + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromProxy(proxy); + if (entity != null + && !Modifier.isAbstract(entity.getModifier())) { + final String entitySimpleName = entity.getName() + .getSimpleTypeName(); + final TemplateDataDictionary section = dataDictionary + .addSection("entities"); + section.setVariable("entitySimpleName", entitySimpleName); + section.setVariable("entityFullPath", proxySimpleName); + addImport(dataDictionary, entitySimpleName, + GwtType.LIST_ACTIVITY, moduleName); + addImport(dataDictionary, proxy.getName() + .getFullyQualifiedTypeName()); + addImport(dataDictionary, entitySimpleName, + GwtType.DESKTOP_LIST_VIEW, moduleName); + addImport(dataDictionary, entitySimpleName, + GwtType.MOBILE_LIST_VIEW, moduleName); + } + } + break; + case APP_REQUEST_FACTORY: + for (final ClassOrInterfaceTypeDetails proxy : proxies) { + if (!GwtUtils.scaffoldProxy(proxy)) { + continue; + } + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromProxy(proxy); + if (entity != null + && !Modifier.isAbstract(entity.getModifier())) { + final String entitySimpleName = entity.getName() + .getSimpleTypeName(); + ClassOrInterfaceTypeDetails request = gwtTypeService + .lookupUnmanagedRequestFromProxy(proxy); + if (request == null) { + request = gwtTypeService.lookupRequestFromProxy(proxy); + } + if (request != null) { + final String requestExpression = new StringBuilder("\t") + .append(request.getName().getSimpleTypeName()) + .append(" ") + .append(StringUtils + .uncapitalize(entitySimpleName)) + .append("Request();").toString(); + dataDictionary.addSection("entities").setVariable( + "entity", requestExpression); + addImport(dataDictionary, request.getName() + .getFullyQualifiedTypeName()); + } + } + dataDictionary.setVariable("sharedScaffoldPackage", + GwtPath.SHARED_SCAFFOLD.packageName(projectOperations + .getTopLevelPackage(moduleName))); + } + + if (projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE)) { + dataDictionary.showSection("gae"); + } + break; + case LIST_PLACE_RENDERER: + for (final ClassOrInterfaceTypeDetails proxy : proxies) { + if (!GwtUtils.scaffoldProxy(proxy)) { + continue; + } + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromProxy(proxy); + if (entity != null) { + final String entitySimpleName = entity.getName() + .getSimpleTypeName(); + final String proxySimpleName = proxy.getName() + .getSimpleTypeName(); + final TemplateDataDictionary section = dataDictionary + .addSection("entities"); + section.setVariable("entitySimpleName", entitySimpleName); + section.setVariable("entityFullPath", proxySimpleName); + addImport(dataDictionary, proxy.getName() + .getFullyQualifiedTypeName()); + } + } + break; + case DETAILS_ACTIVITIES: + for (final ClassOrInterfaceTypeDetails proxy : proxies) { + if (!GwtUtils.scaffoldProxy(proxy)) { + continue; + } + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromProxy(proxy); + if (entity != null) { + final String proxySimpleName = proxy.getName() + .getSimpleTypeName(); + final String entitySimpleName = entity.getName() + .getSimpleTypeName(); + final String entityExpression = new StringBuilder( + "\t\t\tpublic void handle") + .append(entitySimpleName) + .append("(") + .append(proxySimpleName) + .append(" proxy) {\n") + .append("\t\t\t\tsetResult(new ") + .append(entitySimpleName) + .append("ActivitiesMapper(requests, placeController).getActivity(proxyPlace));\n\t\t\t}") + .toString(); + dataDictionary.addSection("entities").setVariable("entity", + entityExpression); + addImport(dataDictionary, proxy.getName() + .getFullyQualifiedTypeName()); + addImport( + dataDictionary, + GwtType.ACTIVITIES_MAPPER.getPath().packageName( + projectOperations + .getTopLevelPackage(moduleName)) + + "." + + entitySimpleName + + GwtType.ACTIVITIES_MAPPER.getSuffix()); + } + } + break; + case MOBILE_ACTIVITIES: + // Do nothing + break; + default: + break; + } + + return dataDictionary; + } + + private TemplateDataDictionary buildMirrorDataDictionary( + final GwtType type, final ClassOrInterfaceTypeDetails mirroredType, + final ClassOrInterfaceTypeDetails proxy, + final Map mirrorTypeMap, + final Map clientSideTypeMap, + final String moduleName) { + + final JavaType proxyType = proxy.getName(); + final JavaType javaType = mirrorTypeMap.get(type); + + final TemplateDataDictionary dataDictionary = TemplateDictionary + .create(); + + // Get my locator and + final JavaType entity = mirroredType.getName(); + final String entityName = entity.getFullyQualifiedTypeName(); + final String metadataIdentificationString = mirroredType + .getDeclaredByMetadataId(); + final JavaType idType = persistenceMemberLocator + .getIdentifierType(entity); + Validate.notNull(idType, + "Identifier type is not available for entity '%s'", entityName); + + final MethodParameter entityParameter = new MethodParameter(entity, + "proxy"); + final ClassOrInterfaceTypeDetails request = gwtTypeService + .lookupRequestFromProxy(proxy); + + final MemberTypeAdditions persistMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + CustomDataKeys.PERSIST_METHOD.name(), entity, idType, + LAYER_POSITION, entityParameter); + Validate.notNull(persistMethodAdditions, + "Persist method is not available for entity '%s'", entityName); + final String persistMethodSignature = getRequestMethodCall(request, + persistMethodAdditions); + dataDictionary.setVariable("persistMethodSignature", + persistMethodSignature); + + final MemberTypeAdditions removeMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + CustomDataKeys.REMOVE_METHOD.name(), entity, idType, + LAYER_POSITION, entityParameter); + Validate.notNull(removeMethodAdditions, + "Remove method is not available for entity '%s'", entityName); + final String removeMethodSignature = getRequestMethodCall(request, + removeMethodAdditions); + dataDictionary.setVariable("removeMethodSignature", + removeMethodSignature); + + final MemberTypeAdditions countMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + CustomDataKeys.COUNT_ALL_METHOD.name(), entity, idType, + LAYER_POSITION); + Validate.notNull(countMethodAdditions, + "Count method is not available for entity '%s'", entityName); + dataDictionary.setVariable("countEntitiesMethod", + countMethodAdditions.getMethodName()); + + for (final GwtType reference : type.getReferences()) { + addReference(dataDictionary, reference, mirrorTypeMap); + } + + addImport(dataDictionary, proxyType.getFullyQualifiedTypeName()); + + final String pluralMetadataKey = PluralMetadata.createIdentifier( + mirroredType.getName(), PhysicalTypeIdentifier + .getPath(mirroredType.getDeclaredByMetadataId())); + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(pluralMetadataKey); + final String plural = pluralMetadata.getPlural(); + + final String simpleTypeName = mirroredType.getName() + .getSimpleTypeName(); + final JavaPackage topLevelPackage = projectOperations + .getTopLevelPackage(moduleName); + dataDictionary.setVariable("className", javaType.getSimpleTypeName()); + dataDictionary.setVariable("packageName", javaType.getPackage() + .getFullyQualifiedPackageName()); + dataDictionary.setVariable("placePackage", + GwtPath.SCAFFOLD_PLACE.packageName(topLevelPackage)); + dataDictionary.setVariable("scaffoldUiPackage", + GwtPath.SCAFFOLD_UI.packageName(topLevelPackage)); + dataDictionary.setVariable("sharedScaffoldPackage", + GwtPath.SHARED_SCAFFOLD.packageName(topLevelPackage)); + dataDictionary.setVariable("uiPackage", + GwtPath.MANAGED_UI.packageName(topLevelPackage)); + dataDictionary.setVariable("uiEditorPackage", + GwtPath.MANAGED_UI_EDITOR.packageName(topLevelPackage)); + dataDictionary.setVariable("name", simpleTypeName); + dataDictionary.setVariable("pluralName", plural); + dataDictionary.setVariable("nameUncapitalized", + StringUtils.uncapitalize(simpleTypeName)); + dataDictionary.setVariable("proxy", proxyType.getSimpleTypeName()); + dataDictionary.setVariable("pluralName", plural); + dataDictionary.setVariable("proxyRenderer", GwtProxyProperty + .getProxyRendererType(topLevelPackage, proxyType)); + + String proxyFields = null; + GwtProxyProperty primaryProperty = null; + GwtProxyProperty secondaryProperty = null; + GwtProxyProperty dateProperty = null; + final Set importSet = new HashSet(); + + // cleanUpLegacyProjects(type, topLevelPackage, simpleTypeName); + + List existingEditViewFields = new ArrayList(); + + List existingDetailsViewFields = new ArrayList(); + + List fieldsInBothDesktopAndMobileEditView = new ArrayList(); + + if (type == GwtType.EDIT_ACTIVITY_WRAPPER + || type == GwtType.MOBILE_EDIT_VIEW + || type == GwtType.DESKTOP_EDIT_VIEW) { + List existingDesktopFields = new ArrayList(); + List existingMobileFields = new ArrayList(); + + try { + String className = GwtPath.MANAGED_UI_DESKTOP + .packageName(topLevelPackage) + + "." + + simpleTypeName + + GwtType.DESKTOP_EDIT_VIEW.getTemplate(); + + ClassOrInterfaceTypeDetails details = typeLocationService + .getTypeDetails(new JavaType(className)); + + if (details != null) { + for (final FieldMetadata field : details + .getDeclaredFields()) { + final JavaSymbolName fieldName = field.getFieldName(); + final String name = fieldName.toString(); + // Adds names of fields in DesktopEditView to + // existingDesktopFields list. These fields should not + // be added to the *MobileDetailsView_Roo_Gwt class + existingDesktopFields.add(name); + } + } + + className = GwtPath.MANAGED_UI_MOBILE + .packageName(topLevelPackage) + + "." + + simpleTypeName + + GwtType.MOBILE_EDIT_VIEW.getTemplate(); + + details = typeLocationService.getTypeDetails(new JavaType( + className)); + + if (details != null) { + for (FieldMetadata field : details.getDeclaredFields()) { + JavaSymbolName fieldName = field.getFieldName(); + String name = fieldName.toString(); + // Adds names of fields in MobileEditView to + // existingMobileFields list. These fields should not be + // added to the *MobileDetailsView_Roo_Gwt class + existingMobileFields.add(name); + } + } + + if (type == GwtType.MOBILE_EDIT_VIEW) + existingEditViewFields = existingMobileFields; + + if (type == GwtType.DESKTOP_EDIT_VIEW) + existingEditViewFields = existingDesktopFields; + + } + catch (final Exception e) { + throw new IllegalArgumentException(e); + } + + for (String mobileViewField : existingMobileFields) { + for (String viewField : existingDesktopFields) { + if (viewField.equals(mobileViewField)) { + fieldsInBothDesktopAndMobileEditView.add(viewField); + break; + } + } + } + } + + if (type == GwtType.MOBILE_DETAILS_VIEW + || type == GwtType.DESKTOP_DETAILS_VIEW) { + List existingDesktopFields = new ArrayList(); + List existingMobileFields = new ArrayList(); + + try { + String className = GwtPath.MANAGED_UI_DESKTOP + .packageName(topLevelPackage) + + "." + + simpleTypeName + + GwtType.DESKTOP_DETAILS_VIEW.getTemplate(); + + ClassOrInterfaceTypeDetails details = typeLocationService + .getTypeDetails(new JavaType(className)); + + if (details != null) { + for (final FieldMetadata field : details + .getDeclaredFields()) { + final JavaSymbolName fieldName = field.getFieldName(); + final String name = fieldName.toString(); + existingDesktopFields.add(name); + } + } + + className = GwtPath.MANAGED_UI_MOBILE + .packageName(topLevelPackage) + + "." + + simpleTypeName + + GwtType.MOBILE_DETAILS_VIEW.getTemplate(); + + details = typeLocationService.getTypeDetails(new JavaType( + className)); + + if (details != null) { + for (FieldMetadata field : details.getDeclaredFields()) { + JavaSymbolName fieldName = field.getFieldName(); + String name = fieldName.toString(); + existingMobileFields.add(name); + } + } + + // Adds names of fields in MobileDetailsView to existingFields + // list + if (type == GwtType.MOBILE_DETAILS_VIEW) + existingDetailsViewFields = existingMobileFields; + + // Adds names of fields in DesktopDetailsView to existingFields + // list + if (type == GwtType.DESKTOP_DETAILS_VIEW) + existingDetailsViewFields = existingDesktopFields; + + } + catch (final Exception e) { + throw new IllegalArgumentException(e); + } + } + + for (final GwtProxyProperty gwtProxyProperty : clientSideTypeMap + .values()) { + // Determine if this is the primary property. + if (primaryProperty == null) { + // Choose the first available field. + primaryProperty = gwtProxyProperty; + } + else if (gwtProxyProperty.isString() && !primaryProperty.isString()) { + // Favor String properties over other types. + secondaryProperty = primaryProperty; + primaryProperty = gwtProxyProperty; + } + else if (secondaryProperty == null) { + // Choose the next available property. + secondaryProperty = gwtProxyProperty; + } + else if (gwtProxyProperty.isString() + && !secondaryProperty.isString()) { + // Favor String properties over other types. + secondaryProperty = gwtProxyProperty; + } + + // Determine if this is the first date property. + if (dateProperty == null && gwtProxyProperty.isDate()) { + dateProperty = gwtProxyProperty; + } + + if (gwtProxyProperty.isProxy() + || gwtProxyProperty.isCollectionOfProxy()) { + if (proxyFields != null) { + proxyFields += ", "; + } + else { + proxyFields = ""; + } + proxyFields += "\"" + gwtProxyProperty.getName() + "\""; + } + + // if the property is in the existingFields list, do not add it + if (!existingDetailsViewFields.contains(gwtProxyProperty.getName())) { + dataDictionary.addSection("fields").setVariable("field", + gwtProxyProperty.getName()); + + final TemplateDataDictionary managedPropertiesSection = dataDictionary + .addSection("managedProperties"); + managedPropertiesSection.setVariable("prop", + gwtProxyProperty.getName()); + managedPropertiesSection.setVariable( + "propId", + proxyType.getSimpleTypeName() + "_" + + gwtProxyProperty.getName()); + managedPropertiesSection.setVariable("propGetter", + gwtProxyProperty.getGetter()); + managedPropertiesSection.setVariable("propType", + gwtProxyProperty.getType()); + managedPropertiesSection.setVariable("propFormatter", + gwtProxyProperty.getFormatter()); + managedPropertiesSection.setVariable("propRenderer", + gwtProxyProperty.getRenderer()); + managedPropertiesSection.setVariable("propReadable", + gwtProxyProperty.getReadableName()); + } + + final TemplateDataDictionary propertiesSection = dataDictionary + .addSection("properties"); + propertiesSection.setVariable("prop", gwtProxyProperty.getName()); + propertiesSection.setVariable( + "propId", + proxyType.getSimpleTypeName() + "_" + + gwtProxyProperty.getName()); + propertiesSection.setVariable("propGetter", + gwtProxyProperty.getGetter()); + propertiesSection.setVariable("propType", + gwtProxyProperty.getType()); + propertiesSection.setVariable("propFormatter", + gwtProxyProperty.getFormatter()); + propertiesSection.setVariable("propRenderer", + gwtProxyProperty.getRenderer()); + propertiesSection.setVariable("propReadable", + gwtProxyProperty.getReadableName()); + + if (!isReadOnly(gwtProxyProperty.getName(), mirroredType)) { + // if the property is in the existingFields list, do not add it + if (!existingEditViewFields + .contains(gwtProxyProperty.getName())) + dataDictionary.addSection("editViewProps").setVariable( + "prop", gwtProxyProperty.forEditView()); + + final TemplateDataDictionary editableSection = dataDictionary + .addSection("editableProperties"); + editableSection.setVariable("prop", gwtProxyProperty.getName()); + editableSection.setVariable( + "propId", + proxyType.getSimpleTypeName() + "_" + + gwtProxyProperty.getName()); + editableSection.setVariable("propGetter", + gwtProxyProperty.getGetter()); + editableSection.setVariable("propType", + gwtProxyProperty.getType()); + editableSection.setVariable("propFormatter", + gwtProxyProperty.getFormatter()); + editableSection.setVariable("propRenderer", + gwtProxyProperty.getRenderer()); + editableSection.setVariable("propBinder", + gwtProxyProperty.getBinder()); + editableSection.setVariable("propReadable", + gwtProxyProperty.getReadableName()); + } + + dataDictionary.setVariable("proxyRendererType", + proxyType.getSimpleTypeName() + "Renderer"); + + // Adds import statements for primitive list editors + if (gwtProxyProperty.isCollection() + && !gwtProxyProperty.isCollectionOfProxy()) { + maybeAddImport(dataDictionary, importSet, + gwtProxyProperty.getSetEditorType()); + } + + // If the field is not added to the managed MobileEditView and the + // managed EditView then it there is no reason to add it to the + // interface nor the start method in the EditActivityWrapper + if (!fieldsInBothDesktopAndMobileEditView.contains(gwtProxyProperty + .getName()) + && !isReadOnly(gwtProxyProperty.getName(), mirroredType)) { + if (gwtProxyProperty.isProxy() || gwtProxyProperty.isEnum() + || gwtProxyProperty.isCollectionOfProxy()) { + final TemplateDataDictionary section = dataDictionary + .addSection(gwtProxyProperty.isEnum() ? "setEnumValuePickers" + : "setProxyValuePickers"); + // The methods is required to satisfy the interface. + // However, if the field is in the existingFields lists, the + // method must be empty because the field will not be added + // to the managed view. + section.setVariable( + "setValuePicker", + existingEditViewFields.contains(gwtProxyProperty + .getName()) ? gwtProxyProperty + .getSetEmptyValuePickerMethod() + : gwtProxyProperty + .getSetValuePickerMethod()); + section.setVariable("setValuePickerName", + gwtProxyProperty.getSetValuePickerMethodName()); + section.setVariable("valueType", gwtProxyProperty + .getValueType().getSimpleTypeName()); + section.setVariable("rendererType", + gwtProxyProperty.getProxyRendererType()); + if (gwtProxyProperty.isProxy() + || gwtProxyProperty.isCollectionOfProxy()) { + String propTypeName = StringUtils + .uncapitalize(gwtProxyProperty + .isCollectionOfProxy() ? gwtProxyProperty + .getPropertyType().getParameters() + .get(0).getSimpleTypeName() + : gwtProxyProperty.getPropertyType() + .getSimpleTypeName()); + propTypeName = propTypeName.substring(0, + propTypeName.indexOf("Proxy")); + section.setVariable("requestInterface", propTypeName + + "Request"); + section.setVariable("findMethod", + "find" + StringUtils.capitalize(propTypeName) + + "Entries(0, 50)"); + } + maybeAddImport(dataDictionary, importSet, + gwtProxyProperty.getPropertyType()); + maybeAddImport(dataDictionary, importSet, + gwtProxyProperty.getValueType()); + if (gwtProxyProperty.isCollectionOfProxy()) { + maybeAddImport(dataDictionary, importSet, + gwtProxyProperty.getPropertyType() + .getParameters().get(0)); + maybeAddImport(dataDictionary, importSet, + gwtProxyProperty.getSetEditorType()); + } + } + } + } + + dataDictionary.setVariable("proxyFields", proxyFields); + + // Add a section for the mobile properties. + if (primaryProperty != null) { + dataDictionary + .setVariable("primaryProp", primaryProperty.getName()); + dataDictionary.setVariable("primaryPropGetter", + primaryProperty.getGetter()); + String primaryPropBuilder = new StringBuilder( + "if (value != null) {\n\t\t\t\tsb.appendEscaped(") + .append("primaryRenderer") + .append(".render(value));\n\t\t\t}").toString(); + dataDictionary + .setVariable("primaryPropBuilder", primaryPropBuilder); + + // dataDictionary.setVariable("primaryPropBuilder", + // primaryProperty.forMobileListView("primaryRenderer")); + // final TemplateDataDictionary section = dataDictionary + // .addSection("mobileProperties"); + // section.setVariable("prop", primaryProperty.getName()); + // section.setVariable("propGetter", primaryProperty.getGetter()); + // section.setVariable("propType", primaryProperty.getType()); + // section.setVariable("propRenderer", + // primaryProperty.getRenderer()); + // section.setVariable("propRendererName", "primaryRenderer"); + } + else { + dataDictionary.setVariable("primaryProp", "id"); + dataDictionary.setVariable("primaryPropGetter", "getId"); + dataDictionary.setVariable("primaryPropBuilder", ""); + } + if (secondaryProperty != null) { + dataDictionary.setVariable("secondaryPropBuilder", + secondaryProperty.forMobileListView("secondaryRenderer")); + final TemplateDataDictionary section = dataDictionary + .addSection("mobileProperties"); + section.setVariable("prop", secondaryProperty.getName()); + section.setVariable("propGetter", secondaryProperty.getGetter()); + section.setVariable("propType", secondaryProperty.getType()); + section.setVariable("propRenderer", secondaryProperty.getRenderer()); + section.setVariable("propRendererName", "secondaryRenderer"); + } + else { + dataDictionary.setVariable("secondaryPropBuilder", ""); + } + if (dateProperty != null) { + dataDictionary.setVariable("datePropBuilder", + dateProperty.forMobileListView("dateRenderer")); + final TemplateDataDictionary section = dataDictionary + .addSection("mobileProperties"); + section.setVariable("prop", dateProperty.getName()); + section.setVariable("propGetter", dateProperty.getGetter()); + section.setVariable("propType", dateProperty.getType()); + section.setVariable("propRenderer", dateProperty.getRenderer()); + section.setVariable("propRendererName", "dateRenderer"); + } + else { + dataDictionary.setVariable("datePropBuilder", ""); + } + return dataDictionary; + } + + private TemplateDataDictionary buildStandardDataDictionary( + final GwtType type, final String moduleName) { + final JavaType javaType = new JavaType(getFullyQualifiedTypeName(type, + moduleName)); + final TemplateDataDictionary dataDictionary = TemplateDictionary + .create(); + for (final GwtType reference : type.getReferences()) { + addReference(dataDictionary, reference, moduleName); + } + dataDictionary.setVariable("className", javaType.getSimpleTypeName()); + dataDictionary.setVariable("packageName", javaType.getPackage() + .getFullyQualifiedPackageName()); + dataDictionary.setVariable("placePackage", GwtPath.SCAFFOLD_PLACE + .packageName(projectOperations.getTopLevelPackage(moduleName))); + dataDictionary.setVariable("sharedScaffoldPackage", + GwtPath.SHARED_SCAFFOLD.packageName(projectOperations + .getTopLevelPackage(moduleName))); + dataDictionary.setVariable("sharedGaePackage", GwtPath.SHARED_GAE + .packageName(projectOperations.getTopLevelPackage(moduleName))); + return dataDictionary; + } + + private JavaType getCollectionImplementation(final JavaType javaType) { + if (isSameBaseType(javaType, SET)) { + return new JavaType(HASH_SET.getFullyQualifiedTypeName(), + javaType.getArray(), javaType.getDataType(), + javaType.getArgName(), javaType.getParameters()); + } + if (isSameBaseType(javaType, LIST)) { + return new JavaType(ARRAY_LIST.getFullyQualifiedTypeName(), + javaType.getArray(), javaType.getDataType(), + javaType.getArgName(), javaType.getParameters()); + } + return javaType; + } + + private JavaType getDestinationJavaType(final GwtType destType, + final String moduleName) { + return new JavaType(getFullyQualifiedTypeName(destType, moduleName)); + } + + private String getFullyQualifiedTypeName(final GwtType gwtType, + final String moduleName) { + return gwtType.getPath().packageName( + projectOperations.getTopLevelPackage(moduleName)) + + "." + gwtType.getTemplate(); + } + + /** + * If the attribute useXmlConfiguration is set to true in @RooGwtLocator, + * this method creates a file called a applicationContext-locators.xml (if + * it does not exist) and adds the associated Locator if it has not yet been + * added. + * + * @param locator The locator to which the annotations applies + * @param service The service to be injected (if one exists) + */ + @Override + public void addLocatorToXmlConfiguration( + ClassOrInterfaceTypeDetails locator, JavaType service) { + final PathResolver pathResolver = projectOperations.getPathResolver(); + + final String fileIdentifier = pathResolver.getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, "applicationContext-locators.xml"); + + if (!fileManager.exists(fileIdentifier)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "applicationContext-locators-template.xml"); + outputStream = fileManager.createFile(fileIdentifier) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + try { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + + InputSource source = new InputSource(); + FileReader fileReader = new FileReader(fileIdentifier); + source.setCharacterStream(fileReader); + final Document document = builder.parse(source); + + final String locatorName = StringUtils.uncapitalize(locator + .getType().getSimpleTypeName()); + + Element locatorElement = XmlUtils.findFirstElement("//*[@id='" + + locatorName + "']", document.getDocumentElement()); + + if (locatorElement != null) + return; + + locatorElement = document.createElement("bean"); + locatorElement.setAttribute("id", locatorName); + locatorElement.setAttribute("class", locator.getType() + .getFullyQualifiedTypeName()); + if (service != null) { + final String serviceName = StringUtils.uncapitalize(service + .getSimpleTypeName()); + Element serviceElement = document.createElement("property"); + serviceElement.setAttribute("name", serviceName); + serviceElement.setAttribute("ref", serviceName); + + locatorElement.appendChild(serviceElement); + } + + Node beansNode = document.getElementsByTagName("beans").item(0); + if (beansNode.getNodeType() == Node.ELEMENT_NODE) { + Element beansElement = (Element) beansNode; + beansElement.appendChild(locatorElement); + // final Transformer transformer = + // XmlUtils.createIndentingTransformer(); + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer transformer = transfac.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + final DOMSource domSource = new DOMSource(document); + final StreamResult result = new StreamResult(new StringWriter()); + transformer.transform(domSource, result); + String output = result.getWriter().toString(); + + fileManager.createOrUpdateTextFileIfRequired(fileIdentifier, + output, true); + } + + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public void removeLocatorFromXmlConfiguration( + ClassOrInterfaceTypeDetails locator) { + final PathResolver pathResolver = projectOperations.getPathResolver(); + + final String fileIdentifier = pathResolver.getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, "applicationContext-locators.xml"); + + if (!fileManager.exists(fileIdentifier)) { + return; + } + + try { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + + InputSource source = new InputSource(); + FileReader fileReader = new FileReader(fileIdentifier); + source.setCharacterStream(fileReader); + final Document document = builder.parse(source); + + final String locatorName = StringUtils.uncapitalize(locator + .getType().getSimpleTypeName()); + + Element locatorElement = XmlUtils.findFirstElement("//*[@id='" + + locatorName + "']", document.getDocumentElement()); + + if (locatorElement == null) + return; + + Node beansNode = document.getElementsByTagName("beans").item(0); + if (beansNode.getNodeType() == Node.ELEMENT_NODE) { + Element beansElement = (Element) beansNode; + beansElement.removeChild(locatorElement); + // final Transformer transformer = + // XmlUtils.createIndentingTransformer(); + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer transformer = transfac.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + final DOMSource domSource = new DOMSource(document); + final StreamResult result = new StreamResult(new StringWriter()); + transformer.transform(domSource, result); + String output = result.getWriter().toString(); + + fileManager.createOrUpdateTextFileIfRequired(fileIdentifier, + output, true); + } + + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + + } + + private String getRequestMethodCall( + final ClassOrInterfaceTypeDetails request, + final MemberTypeAdditions memberTypeAdditions) { + final String methodName = memberTypeAdditions.getMethodName(); + final MethodMetadata requestMethod = MemberFindingUtils.getMethod( + request, methodName); + String requestMethodCall = memberTypeAdditions.getMethodName(); + if (requestMethod != null) { + if (INSTANCE_REQUEST.getFullyQualifiedTypeName().equals( + requestMethod.getReturnType().getFullyQualifiedTypeName())) { + requestMethodCall = requestMethodCall + "().using"; + } + } + return requestMethodCall; + } + + private String getTemplateContents(final String templateName, + final TemplateDataDictionary dataDictionary) { + try { + final TemplateLoader templateLoader = TemplateResourceLoader + .create(); + final Template template = templateLoader.getTemplate(templateName); + return template.renderToString(dataDictionary); + } + catch (final TemplateException e) { + throw new IllegalStateException(e); + } + } + + private boolean isReadOnly(final String name, + final ClassOrInterfaceTypeDetails governorTypeDetails) { + final List readOnly = new ArrayList(); + final ClassOrInterfaceTypeDetails proxy = gwtTypeService + .lookupProxyFromEntity(governorTypeDetails); + if (proxy != null) { + readOnly.addAll(GwtUtils.getAnnotationValues(proxy, + RooJavaType.ROO_GWT_PROXY, "readOnly")); + } + + return readOnly.contains(name); + } + + private boolean isSameBaseType(final JavaType type1, final JavaType type2) { + return type1.getFullyQualifiedTypeName().equals( + type2.getFullyQualifiedTypeName()); + } + + private void maybeAddImport(final TemplateDataDictionary dataDictionary, + final Set importSet, final JavaType type) { + if (!importSet.contains(type.getFullyQualifiedTypeName())) { + addImport(dataDictionary, type.getFullyQualifiedTypeName()); + importSet.add(type.getFullyQualifiedTypeName()); + } + } + + private String transformXml(final Document document) + throws TransformerException { + final Transformer transformer = XmlUtils.createIndentingTransformer(); + final DOMSource source = new DOMSource(document); + final StreamResult result = new StreamResult(new StringWriter()); + transformer.transform(source, result); + return result.getWriter().toString(); + } + +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtType.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtType.java new file mode 100644 index 000000000..5bd6dd4be --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtType.java @@ -0,0 +1,431 @@ +package org.springframework.roo.addon.gwt; + +import static org.springframework.roo.addon.gwt.GwtJavaType.ACCEPTS_ONE_WIDGET; +import static org.springframework.roo.addon.gwt.GwtJavaType.ENTITY_PROXY; +import static org.springframework.roo.addon.gwt.GwtJavaType.EVENT_BUS; +import static org.springframework.roo.addon.gwt.GwtJavaType.PLACE; +import static org.springframework.roo.addon.gwt.GwtJavaType.RECEIVER; +import static org.springframework.roo.model.JdkJavaType.COLLECTION; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public enum GwtType { + + ACTIVITIES_MAPPER(GwtPath.MANAGED_ACTIVITY, true, "ActivitiesMapper", + "activitiesMapper", "ActivitiesMapper", false, true, false), + + // Represents shared types. There is one such type per application using + // Roo's GWT support + APP_ENTITY_TYPES_PROCESSOR(GwtPath.MANAGED_REQUEST, false, "", + "entityTypes", "ApplicationEntityTypesProcessor", false, false, + true), APP_REQUEST_FACTORY(GwtPath.MANAGED_REQUEST, false, "", + "requestFactory", "ApplicationRequestFactory", false, false, true), DETAIL_ACTIVITY( + GwtPath.MANAGED_ACTIVITY, true, "DetailsActivity", + "detailsActivity", "DetailsActivity", false, true, false), DETAILS_ACTIVITIES( + GwtPath.MANAGED_ACTIVITY, false, "", "detailsActivities", + "ApplicationDetailsActivities", false, true, false), DETAILS_VIEW( + GwtPath.MANAGED_UI, true, "DetailsView", "detailsView", + "DetailsView", false, false, false), DESKTOP_DETAILS_VIEW( + GwtPath.MANAGED_UI_DESKTOP, true, "DesktopDetailsView", + "desktopDetailsView", "DesktopDetailsView", true, true, false), EDIT_ACTIVITY( + GwtPath.MANAGED_ACTIVITY, true, "EditActivity", "editActivity", + "EditActivity", false, false, false), EDIT_ACTIVITY_WRAPPER( + GwtPath.MANAGED_ACTIVITY, true, "EditActivityWrapper", + "editActivityWrapper", "EditActivityWrapper", false, true, false), EDIT_RENDERER( + GwtPath.MANAGED_UI_RENDERER, true, "ProxyRenderer", "renderer", + "EditRenderer", false, false, false), EDIT_VIEW(GwtPath.MANAGED_UI, + true, "EditView", "editView", "EditView", false, false, false), DESKTOP_EDIT_VIEW( + GwtPath.MANAGED_UI_DESKTOP, true, "DesktopEditView", + "desktopEditView", "DesktopEditView", true, true, false), IS_SCAFFOLD_MOBILE_ACTIVITY( + GwtPath.SCAFFOLD_ACTIVITY, false, "", "isScaffoldMobileActivity", + "IsScaffoldMobileActivity", false, false, false), LIST_ACTIVITY( + GwtPath.MANAGED_ACTIVITY, true, "ListActivity", "listActivity", + "ListActivity", false, true, false), LIST_EDITOR( + GwtPath.MANAGED_UI_EDITOR, true, "ListEditor", "listEditor", + "ListEditor", true, true, false), LIST_PLACE_RENDERER( + GwtPath.MANAGED_UI_RENDERER, false, "", "listPlaceRenderer", + "ApplicationListPlaceRenderer", false, true, false), DESKTOP_LIST_VIEW( + GwtPath.MANAGED_UI_DESKTOP, true, "DesktopListView", + "desktopListView", "DesktopListView", true, true, false), MASTER_ACTIVITIES( + GwtPath.MANAGED_ACTIVITY, false, "", "masterActivities", + "ApplicationMasterActivities", false, true, false), + + MOBILE_ACTIVITIES(GwtPath.MANAGED_ACTIVITY, false, "", "mobileActivities", + "ScaffoldMobileActivities", false, false, false), MOBILE_DETAILS_VIEW( + GwtPath.MANAGED_UI_MOBILE, true, "MobileDetailsView", + "mobileDetailsView", "MobileDetailsView", true, true, false), MOBILE_EDIT_VIEW( + GwtPath.MANAGED_UI_MOBILE, true, "MobileEditView", + "mobileEditView", "MobileEditView", true, true, false), MOBILE_LIST_VIEW( + GwtPath.MANAGED_UI_MOBILE, true, "MobileListView", + "mobileListView", "MobileListView", false, true, false), MOBILE_PROXY_LIST_VIEW( + GwtPath.SCAFFOLD_UI, false, "", "mobileProxyListView", + "MobileProxyListView", false, false, false), + + // Represents mirror types classes. There are one of these for each entity + // mirrored by Roo. + PROXY(GwtPath.MANAGED_REQUEST, true, "Proxy", "proxy", null, false, false, + true), REQUEST(GwtPath.MANAGED_REQUEST, true, "Request", "request", + null, false, false, true), SCAFFOLD_APP(GwtPath.SCAFFOLD, false, + "", "scaffoldApp", "ScaffoldApp", false, false, false), SCAFFOLD_MOBILE_APP( + GwtPath.SCAFFOLD, false, "", "scaffoldMobileApp", + "ScaffoldMobileApp", false, false, false), SET_EDITOR( + GwtPath.MANAGED_UI_EDITOR, true, "SetEditor", "setEditor", + "SetEditor", true, true, false); + + public static List getMirrorTypes() { + final List mirrorTypes = new ArrayList(); + for (final GwtType gwtType : GwtType.values()) { + if (gwtType.isMirrorType()) { + mirrorTypes.add(gwtType); + } + } + return mirrorTypes; + } + + private boolean createAbstract = false; + private final boolean createUiXml; + private boolean mirrorType = false; + private final String name; + private boolean overwriteConcrete = false; + private final GwtPath path; + private final String suffix; + private final String template; + private List watchedFieldNames = new ArrayList(); + + private Map> watchedMethods = new LinkedHashMap>(); + + private GwtType(final GwtPath path, final boolean mirrorType, + final String suffix, final String name, final String template, + final boolean createUiXml, final boolean createAbstract, + final boolean overwriteConcrete) { + this.path = path; + this.mirrorType = mirrorType; + this.suffix = suffix; + this.name = name; + this.template = template; + this.createUiXml = createUiXml; + this.createAbstract = createAbstract; + this.overwriteConcrete = overwriteConcrete; + } + + private List convertToJavaSymbolNames(final String... names) { + final List javaSymbolNames = new ArrayList(); + for (final String name : names) { + if (!javaSymbolNames.contains(new JavaSymbolName(name))) { + javaSymbolNames.add(new JavaSymbolName(name)); + } + } + return javaSymbolNames; + } + + public void dynamicallyResolveFieldsToWatch( + final Map proxyFieldTypeMap) { + watchedFieldNames = resolveWatchedFieldNames(this); + switch (this) { + case DESKTOP_DETAILS_VIEW: + watchedFieldNames.addAll(proxyFieldTypeMap.keySet()); + watchedFieldNames.addAll(convertToJavaSymbolNames("proxy", + "displayRenderer")); + break; + case MOBILE_DETAILS_VIEW: + watchedFieldNames.addAll(proxyFieldTypeMap.keySet()); + watchedFieldNames.addAll(convertToJavaSymbolNames("proxy", + "displayRenderer")); + break; + case DESKTOP_EDIT_VIEW: + watchedFieldNames.addAll(proxyFieldTypeMap.keySet()); + break; + case MOBILE_EDIT_VIEW: + watchedFieldNames.addAll(proxyFieldTypeMap.keySet()); + break; + default: + break; + } + } + + public void dynamicallyResolveMethodsToWatch(final JavaType proxy, + final Map proxyFieldTypeMap, + final JavaPackage topLevelPackage) { + watchedMethods = resolveMethodsToWatch(this); + switch (this) { + case DESKTOP_DETAILS_VIEW: + watchedMethods.put(new JavaSymbolName("setValue"), + Collections.singletonList(proxy)); + break; + case MOBILE_DETAILS_VIEW: + watchedMethods.put(new JavaSymbolName("setValue"), + Collections.singletonList(proxy)); + break; + case DESKTOP_EDIT_VIEW: + for (final GwtProxyProperty property : proxyFieldTypeMap.values()) { + if (property.isEnum() || property.isProxy() + || property.isEmbeddable() + || property.isCollectionOfProxy()) { + final List params = new ArrayList(); + final JavaType param = new JavaType( + COLLECTION.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, + Collections.singletonList(property.getValueType())); + params.add(param); + watchedMethods.put( + new JavaSymbolName(property + .getSetValuePickerMethodName()), params); + } + } + break; + case MOBILE_EDIT_VIEW: + for (final GwtProxyProperty property : proxyFieldTypeMap.values()) { + if (property.isEnum() || property.isProxy() + || property.isEmbeddable() + || property.isCollectionOfProxy()) { + final List params = new ArrayList(); + final JavaType param = new JavaType( + COLLECTION.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, + Collections.singletonList(property.getValueType())); + params.add(param); + watchedMethods.put( + new JavaSymbolName(property + .getSetValuePickerMethodName()), params); + } + } + break; + case LIST_PLACE_RENDERER: + for (final GwtProxyProperty property : proxyFieldTypeMap.values()) { + if (property.isEnum() || property.isProxy() + || property.isEmbeddable() + || property.isCollectionOfProxy()) { + final List params = new ArrayList(); + final JavaType param = new JavaType( + COLLECTION.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, + Collections.singletonList(property.getValueType())); + params.add(param); + watchedMethods.put( + new JavaSymbolName(property + .getSetValuePickerMethodName()), params); + } + } + watchedMethods.put(new JavaSymbolName("render"), Collections + .singletonList(new JavaType(topLevelPackage + .getFullyQualifiedPackageName() + + ".client.scaffold.place.ProxyListPlace"))); + break; + case ACTIVITIES_MAPPER: + final List params = new ArrayList(); + params.add(new JavaType(topLevelPackage + .getFullyQualifiedPackageName() + + ".client.scaffold.place.ProxyPlace")); + watchedMethods.put(new JavaSymbolName("makeEditActivity"), params); + watchedMethods.put(new JavaSymbolName("coerceId"), params); + watchedMethods.put(new JavaSymbolName("makeCreateActivity"), + new ArrayList()); + break; + case EDIT_RENDERER: + watchedMethods.put(new JavaSymbolName("render"), + Collections.singletonList(proxy)); + break; + default: + break; + } + } + + public String getName() { + return name; + } + + public GwtPath getPath() { + return path; + } + + public List getReferences() { + return resolveReferences(this); + } + + public String getSuffix() { + return suffix; + } + + public String getTemplate() { + return template; + } + + public List getWatchedFieldNames() { + return watchedFieldNames; + } + + public List getWatchedInnerTypes() { + return resolveInnerTypesToWatch(this); + } + + public Map> getWatchedMethods() { + return watchedMethods; + } + + public boolean isCreateAbstract() { + return createAbstract; + } + + public boolean isCreateUiXml() { + return createUiXml; + } + + public boolean isMirrorType() { + return mirrorType; + } + + public boolean isOverwriteConcrete() { + return overwriteConcrete; + } + + private List resolveInnerTypesToWatch(final GwtType type) { + switch (type) { + case EDIT_ACTIVITY_WRAPPER: + return Arrays.asList(new JavaType("View")); + default: + return new ArrayList(); + } + } + + public Map> resolveMethodsToWatch( + final GwtType type) { + watchedMethods = new HashMap>(); + switch (type) { + case EDIT_ACTIVITY_WRAPPER: + watchedMethods.put(new JavaSymbolName("start"), + Arrays.asList(ACCEPTS_ONE_WIDGET, EVENT_BUS)); + break; + case DETAIL_ACTIVITY: + watchedMethods.put(new JavaSymbolName("find"), + Arrays.asList(GwtUtils.getReceiverType(ENTITY_PROXY))); + watchedMethods.put(new JavaSymbolName("deleteClicked"), + new ArrayList()); + break; + case MOBILE_LIST_VIEW: + watchedMethods.put(new JavaSymbolName("init"), + new ArrayList()); + break; + case DESKTOP_LIST_VIEW: + watchedMethods.put(new JavaSymbolName("init"), + new ArrayList()); + break; + case MASTER_ACTIVITIES: + watchedMethods.put(new JavaSymbolName("getActivity"), + Collections.singletonList(PLACE)); + break; + case DETAILS_ACTIVITIES: + watchedMethods.put(new JavaSymbolName("getActivity"), + Collections.singletonList(PLACE)); + break; + case LIST_ACTIVITY: + watchedMethods.put(new JavaSymbolName("fireCountRequest"), + Collections.singletonList(RECEIVER)); + break; + default: + break; + } + return watchedMethods; + } + + private List resolveReferences(final GwtType type) { + switch (type) { + case ACTIVITIES_MAPPER: + return Arrays.asList(GwtType.APP_REQUEST_FACTORY, + GwtType.SCAFFOLD_APP, GwtType.DETAIL_ACTIVITY, + GwtType.EDIT_ACTIVITY, GwtType.EDIT_ACTIVITY_WRAPPER, + GwtType.DESKTOP_LIST_VIEW, GwtType.DESKTOP_DETAILS_VIEW, + GwtType.MOBILE_DETAILS_VIEW, GwtType.DESKTOP_EDIT_VIEW, + GwtType.MOBILE_EDIT_VIEW, GwtType.REQUEST); + case DETAIL_ACTIVITY: + return Arrays.asList(GwtType.APP_REQUEST_FACTORY, + GwtType.IS_SCAFFOLD_MOBILE_ACTIVITY, GwtType.DETAILS_VIEW); + case EDIT_ACTIVITY: + return Arrays.asList(GwtType.EDIT_VIEW, + GwtType.APP_REQUEST_FACTORY, GwtType.REQUEST); + case EDIT_ACTIVITY_WRAPPER: + return Arrays.asList(GwtType.APP_REQUEST_FACTORY, + GwtType.IS_SCAFFOLD_MOBILE_ACTIVITY, GwtType.EDIT_VIEW); + case LIST_ACTIVITY: + return Arrays.asList(GwtType.APP_REQUEST_FACTORY, + GwtType.IS_SCAFFOLD_MOBILE_ACTIVITY, + GwtType.SCAFFOLD_MOBILE_APP); + case MOBILE_LIST_VIEW: + return Arrays.asList(GwtType.MOBILE_PROXY_LIST_VIEW, + GwtType.SCAFFOLD_MOBILE_APP); + case DESKTOP_EDIT_VIEW: + return Arrays.asList(GwtType.EDIT_ACTIVITY_WRAPPER, + GwtType.EDIT_VIEW); + case MOBILE_EDIT_VIEW: + return Arrays.asList(GwtType.EDIT_ACTIVITY_WRAPPER, + GwtType.EDIT_VIEW); + case DESKTOP_DETAILS_VIEW: + return Arrays.asList(GwtType.DETAILS_VIEW); + case MOBILE_DETAILS_VIEW: + return Arrays.asList(GwtType.DETAILS_VIEW); + case LIST_PLACE_RENDERER: + return Arrays.asList(GwtType.APP_ENTITY_TYPES_PROCESSOR); + case MASTER_ACTIVITIES: + return Arrays.asList(GwtType.APP_REQUEST_FACTORY, + GwtType.APP_ENTITY_TYPES_PROCESSOR, GwtType.SCAFFOLD_APP); + case DETAILS_ACTIVITIES: + return Arrays.asList(GwtType.APP_REQUEST_FACTORY, + GwtType.APP_ENTITY_TYPES_PROCESSOR); + default: + return new ArrayList(); + } + } + + public List resolveWatchedFieldNames(final GwtType type) { + watchedFieldNames = new ArrayList(); + switch (type) { + case ACTIVITIES_MAPPER: + watchedFieldNames = convertToJavaSymbolNames("factory", + "placeController"); + break; + case EDIT_ACTIVITY_WRAPPER: + watchedFieldNames = convertToJavaSymbolNames("wrapped", "view", + "requests"); + break; + case DETAIL_ACTIVITY: + watchedFieldNames = convertToJavaSymbolNames("requests", "proxyId", + "placeController", "display", "view"); + break; + case LIST_ACTIVITY: + watchedFieldNames = convertToJavaSymbolNames("requests"); + break; + case MOBILE_LIST_VIEW: + watchedFieldNames = convertToJavaSymbolNames("paths"); + break; + case DESKTOP_LIST_VIEW: + watchedFieldNames = convertToJavaSymbolNames("table", "paths"); + break; + case MASTER_ACTIVITIES: + watchedFieldNames = convertToJavaSymbolNames("requests", + "placeController"); + break; + case DETAILS_ACTIVITIES: + watchedFieldNames = convertToJavaSymbolNames("requests", + "placeController"); + break; + default: + watchedFieldNames = new ArrayList(); + } + return watchedFieldNames; + } + + public void setWatchedMethods( + final Map> watchedMethods) { + this.watchedMethods = watchedMethods; + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTypeService.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTypeService.java new file mode 100644 index 000000000..176ef360b --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTypeService.java @@ -0,0 +1,116 @@ +package org.springframework.roo.addon.gwt; + +import java.util.Collection; +import java.util.List; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Interface for {@link GwtTypeServiceImpl}. + * + * @author James Tyrrell + * @since 1.1.2 + */ +public interface GwtTypeService { + + /** + * Adds the given GWT source path to the given module's gwt.xml file. + * + * @param sourcePath the path relative to the gwt.xml file, delimited by + * {@link GwtOperations#PATH_DELIMITER} + * @param moduleName the project module whose gwt.xml file is to be updated + */ + void addSourcePath(String sourcePath, String moduleName); + + List buildType(GwtType destType, + ClassOrInterfaceTypeDetails templateClass, + List extendsTypes, String moduleName); + + void buildType(GwtType destType, + List templateTypeDetails, + String moduleName); + + List getExtendsTypes( + ClassOrInterfaceTypeDetails childType); + + String getGwtModuleXml(String moduleName); + + JavaType getGwtSideLeafType(JavaType returnType, JavaType governorType, + boolean requestType, boolean convertPrimitive); + + List getProxyMethods( + ClassOrInterfaceTypeDetails governorTypeDetails); + + /** + * Returns the project type in the given module that implements + * {@link com.google.web.bindery.requestfactory.shared.ServiceLocator}. + * There is no guarantee that this type actually exists. + * + * @param moduleName the module in which to find the locator (can't be + * null) + * @return a non-null type + * @since 1.2.0 + */ + JavaType getServiceLocator(String moduleName); + + /** + * Returns the Java packages within the given module that contain GWT source + * + * @param moduleName the name of the module (empty for the root or only + * module) + * @return a non-null collection + */ + Collection getSourcePackages(String moduleName); + + boolean isDomainObject(JavaType type); + + /** + * Indicates whether the given method's return type resides within any of + * the given GWT source packages + * + * @param method + * @param memberHoldingTypeDetail + * @param sourcePackages + * @return + */ + boolean isMethodReturnTypeInSourcePath(MethodMetadata method, + MemberHoldingTypeDetails memberHoldingTypeDetail, + Iterable sourcePackages); + + ClassOrInterfaceTypeDetails lookupEntityFromLocator( + ClassOrInterfaceTypeDetails request); + + ClassOrInterfaceTypeDetails lookupEntityFromProxy( + ClassOrInterfaceTypeDetails proxy); + + ClassOrInterfaceTypeDetails lookupEntityFromRequest( + ClassOrInterfaceTypeDetails request); + + ClassOrInterfaceTypeDetails lookupProxyFromEntity( + ClassOrInterfaceTypeDetails entity); + + ClassOrInterfaceTypeDetails lookupProxyFromRequest( + ClassOrInterfaceTypeDetails request); + + ClassOrInterfaceTypeDetails lookupRequestFromEntity( + ClassOrInterfaceTypeDetails entity); + + ClassOrInterfaceTypeDetails lookupRequestFromProxy( + ClassOrInterfaceTypeDetails proxy); + + ClassOrInterfaceTypeDetails lookupTargetServiceFromRequest( + ClassOrInterfaceTypeDetails request); + + ClassOrInterfaceTypeDetails lookupUnmanagedRequestFromProxy( + ClassOrInterfaceTypeDetails proxy); + + ClassOrInterfaceTypeDetails lookupUnmanagedRequestFromEntity( + ClassOrInterfaceTypeDetails entity); + + ClassOrInterfaceTypeDetails lookupLocatorFromEntity( + ClassOrInterfaceTypeDetails entity); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTypeServiceImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTypeServiceImpl.java new file mode 100644 index 000000000..121279f78 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtTypeServiceImpl.java @@ -0,0 +1,899 @@ +package org.springframework.roo.addon.gwt; + +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.SET; +import static org.springframework.roo.model.JpaJavaType.EMBEDDABLE; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.logging.Logger; + +import javax.xml.parsers.DocumentBuilder; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.gwt.scaffold.GwtScaffoldMetadata; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.AbstractIdentifiableAnnotatedJavaStructureBuilder; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.IdentifiableAnnotatedJavaStructure; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Provides a basic implementation of {@link GwtTypeService}. + * + * @author James Tyrrell + * @since 1.1.2 + */ +@Component +@Service +public class GwtTypeServiceImpl implements GwtTypeService { + + private static final Logger LOGGER = HandlerUtils + .getLogger(GwtTypeServiceImpl.class); + private static final String PATH = "path"; + + @Reference private FileManager fileManager; + @Reference private GwtFileManager gwtFileManager; + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private MetadataService metadataService; + @Reference private PersistenceMemberLocator persistenceMemberLocator; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + + private final Set warnings = new LinkedHashSet(); + private final Timer warningTimer = new Timer(); + + public void addSourcePath(final String sourcePath, final String moduleName) { + final String gwtXmlPath = getGwtModuleXml(moduleName); + Validate.notBlank(gwtXmlPath, "gwt.xml could not be found for module '" + + moduleName + "'"); + final Document gwtXmlDoc = getGwtXmlDocument(gwtXmlPath); + final Element gwtXmlRoot = gwtXmlDoc.getDocumentElement(); + final List sourceElements = XmlUtils.findElements( + "/module/source", gwtXmlRoot); + if (!anyExistingSourcePathsIncludePath(sourcePath, sourceElements)) { + final Element firstSourceElement = sourceElements.get(0); + final Element newSourceElement = gwtXmlDoc.createElement("source"); + newSourceElement.setAttribute(PATH, sourcePath); + gwtXmlRoot.insertBefore(newSourceElement, firstSourceElement); + fileManager.createOrUpdateTextFileIfRequired(gwtXmlPath, + XmlUtils.nodeToString(gwtXmlDoc), + "Added source paths to gwt.xml file", true); + } + } + + private boolean anyExistingSourcePathsIncludePath(final String sourcePath, + final Iterable sourceElements) { + for (final Element sourceElement : sourceElements) { + if (sourcePath.startsWith(sourceElement.getAttribute(PATH))) { + return true; + } + } + return false; + } + + public List buildType(final GwtType destType, + final ClassOrInterfaceTypeDetails templateClass, + final List extendsTypes, + final String moduleName) { + try { + // A type may consist of a concrete type which depend on + final List types = new ArrayList(); + final ClassOrInterfaceTypeDetailsBuilder templateClassBuilder = new ClassOrInterfaceTypeDetailsBuilder( + templateClass); + + if (destType.isCreateAbstract()) { + final ClassOrInterfaceTypeDetailsBuilder abstractClassBuilder = createAbstractBuilder( + templateClassBuilder, extendsTypes, moduleName); + + final ArrayList fieldsToRemove = new ArrayList(); + for (final JavaSymbolName fieldName : destType + .getWatchedFieldNames()) { + for (final FieldMetadataBuilder fieldBuilder : templateClassBuilder + .getDeclaredFields()) { + if (fieldBuilder.getFieldName().equals(fieldName)) { + final FieldMetadataBuilder abstractFieldBuilder = new FieldMetadataBuilder( + abstractClassBuilder + .getDeclaredByMetadataId(), + fieldBuilder.build()); + abstractClassBuilder + .addField(convertModifier(abstractFieldBuilder)); + fieldsToRemove.add(fieldBuilder); + break; + } + } + } + + templateClassBuilder.getDeclaredFields().removeAll( + fieldsToRemove); + + final List methodsToRemove = new ArrayList(); + for (final JavaSymbolName methodName : destType + .getWatchedMethods().keySet()) { + for (final MethodMetadataBuilder methodBuilder : templateClassBuilder + .getDeclaredMethods()) { + final List params = new ArrayList(); + for (final AnnotatedJavaType param : methodBuilder + .getParameterTypes()) { + params.add(new JavaType(param.getJavaType() + .getFullyQualifiedTypeName())); + } + if (methodBuilder.getMethodName().equals(methodName)) { + if (destType.getWatchedMethods().get(methodName) + .containsAll(params)) { + final MethodMetadataBuilder abstractMethodBuilder = new MethodMetadataBuilder( + abstractClassBuilder + .getDeclaredByMetadataId(), + methodBuilder.build()); + abstractClassBuilder + .addMethod(convertModifier(abstractMethodBuilder)); + methodsToRemove.add(methodBuilder); + break; + } + } + } + } + + templateClassBuilder.removeAll(methodsToRemove); + + for (final JavaType innerTypeName : destType + .getWatchedInnerTypes()) { + for (final ClassOrInterfaceTypeDetailsBuilder innerTypeBuilder : templateClassBuilder + .getDeclaredInnerTypes()) { + if (innerTypeBuilder.getName().getSimpleTypeName() + .equals(innerTypeName.getSimpleTypeName())) { + final ClassOrInterfaceTypeDetailsBuilder builder = new ClassOrInterfaceTypeDetailsBuilder( + abstractClassBuilder + .getDeclaredByMetadataId(), + innerTypeBuilder.build()); + builder.setName(new JavaType( + innerTypeBuilder.getName() + .getSimpleTypeName() + "_Roo_Gwt", + 0, DataType.TYPE, null, innerTypeBuilder + .getName().getParameters())); + + templateClassBuilder.getDeclaredInnerTypes() + .remove(innerTypeBuilder); + if (innerTypeBuilder.getPhysicalTypeCategory() + .equals(PhysicalTypeCategory.INTERFACE)) { + final ClassOrInterfaceTypeDetailsBuilder interfaceInnerTypeBuilder = new ClassOrInterfaceTypeDetailsBuilder( + innerTypeBuilder.build()); + abstractClassBuilder.addInnerType(builder); + templateClassBuilder.getDeclaredInnerTypes() + .remove(innerTypeBuilder); + interfaceInnerTypeBuilder + .clearDeclaredMethods(); + interfaceInnerTypeBuilder + .getDeclaredInnerTypes().clear(); + interfaceInnerTypeBuilder.getExtendsTypes() + .clear(); + interfaceInnerTypeBuilder + .getExtendsTypes() + .add(new JavaType( + builder.getName() + .getSimpleTypeName(), + 0, + DataType.TYPE, + null, + Collections + .singletonList(new JavaType( + "V", + 0, + DataType.VARIABLE, + null, + new ArrayList())))); + templateClassBuilder.getDeclaredInnerTypes() + .add(interfaceInnerTypeBuilder); + } + break; + } + } + } + + abstractClassBuilder.setImplementsTypes(templateClass + .getImplementsTypes()); + templateClassBuilder.getImplementsTypes().clear(); + templateClassBuilder.getExtendsTypes().clear(); + templateClassBuilder.getExtendsTypes().add( + abstractClassBuilder.getName()); + types.add(abstractClassBuilder.build()); + } + + types.add(templateClassBuilder.build()); + + return types; + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + public void buildType(final GwtType type, + final List templateTypeDetails, + final String moduleName) { + if (GwtType.LIST_PLACE_RENDERER.equals(type)) { + final Map> watchedMethods = new HashMap>(); + watchedMethods.put(new JavaSymbolName("render"), Collections + .singletonList(new JavaType(projectOperations + .getTopLevelPackage(moduleName) + .getFullyQualifiedPackageName() + + ".client.scaffold.place.ProxyListPlace"))); + type.setWatchedMethods(watchedMethods); + } + else { + type.resolveMethodsToWatch(type); + } + + type.resolveWatchedFieldNames(type); + final List typesToBeWritten = new ArrayList(); + for (final ClassOrInterfaceTypeDetails templateTypeDetail : templateTypeDetails) { + typesToBeWritten.addAll(buildType(type, templateTypeDetail, + getExtendsTypes(templateTypeDetail), moduleName)); + } + gwtFileManager.write(typesToBeWritten, type.isOverwriteConcrete()); + } + + private void checkPrimitive(final JavaType type) { + if (type.isPrimitive() && !JavaType.VOID_PRIMITIVE.equals(type)) { + final String to = type.getSimpleTypeName(); + final String from = to.toLowerCase(); + throw new IllegalStateException( + "GWT does not currently support primitive types in an entity. Please change any '" + + from + + "' entity property types to 'java.lang." + + to + "'."); + } + } + + private > T convertModifier( + final T builder) { + if (Modifier.isPrivate(builder.getModifier())) { + builder.setModifier(Modifier.PROTECTED); + } + return builder; + } + + private ClassOrInterfaceTypeDetailsBuilder createAbstractBuilder( + final ClassOrInterfaceTypeDetailsBuilder concreteClass, + final List extendsTypesDetails, + final String moduleName) { + final JavaType concreteType = concreteClass.getName(); + String abstractName = concreteType.getSimpleTypeName() + "_Roo_Gwt"; + abstractName = concreteType.getPackage().getFullyQualifiedPackageName() + + '.' + abstractName; + final JavaType abstractType = new JavaType(abstractName); + final String abstractId = PhysicalTypeIdentifier.createIdentifier( + abstractType, + LogicalPath.getInstance(Path.SRC_MAIN_JAVA, moduleName)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + abstractId); + cidBuilder.setPhysicalTypeCategory(PhysicalTypeCategory.CLASS); + cidBuilder.setName(abstractType); + cidBuilder.setModifier(Modifier.ABSTRACT | Modifier.PUBLIC); + cidBuilder.getExtendsTypes().addAll(concreteClass.getExtendsTypes()); + cidBuilder.add(concreteClass.getRegisteredImports()); + + for (final MemberHoldingTypeDetails extendsTypeDetails : extendsTypesDetails) { + for (final ConstructorMetadata constructor : extendsTypeDetails + .getDeclaredConstructors()) { + final ConstructorMetadataBuilder abstractConstructor = new ConstructorMetadataBuilder( + abstractId); + abstractConstructor.setModifier(constructor.getModifier()); + + final Map typeMap = resolveTypes( + extendsTypeDetails.getName(), concreteClass + .getExtendsTypes().get(0)); + for (final AnnotatedJavaType type : constructor + .getParameterTypes()) { + JavaType newType = type.getJavaType(); + if (type.getJavaType().getParameters().size() > 0) { + final ArrayList parameterTypes = new ArrayList(); + for (final JavaType typeType : type.getJavaType() + .getParameters()) { + final JavaType typeParam = typeMap + .get(new JavaSymbolName(typeType.toString())); + if (typeParam != null) { + parameterTypes.add(typeParam); + } + } + newType = new JavaType(type.getJavaType() + .getFullyQualifiedTypeName(), type + .getJavaType().getArray(), type.getJavaType() + .getDataType(), + type.getJavaType().getArgName(), parameterTypes); + } + abstractConstructor.getParameterTypes().add( + new AnnotatedJavaType(newType)); + } + abstractConstructor.setParameterNames(constructor + .getParameterNames()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.newLine().indent().append("super("); + + int i = 0; + for (final JavaSymbolName paramName : abstractConstructor + .getParameterNames()) { + bodyBuilder.append(" ").append(paramName.getSymbolName()); + if (abstractConstructor.getParameterTypes().size() > i + 1) { + bodyBuilder.append(", "); + } + i++; + } + + bodyBuilder.append(");"); + + bodyBuilder.newLine().indentRemove(); + abstractConstructor.setBodyBuilder(bodyBuilder); + cidBuilder.getDeclaredConstructors().add(abstractConstructor); + } + } + return cidBuilder; + } + + private void displayWarning(final String warning) { + if (!warnings.contains(warning)) { + warnings.add(warning); + LOGGER.severe(warning); + warningTimer.schedule(new TimerTask() { + @Override + public void run() { + warnings.clear(); + } + }, 15000); + } + } + + public List getExtendsTypes( + final ClassOrInterfaceTypeDetails childType) { + final List extendsTypes = new ArrayList(); + if (childType != null) { + for (final JavaType javaType : childType.getExtendsTypes()) { + final String superTypeId = typeLocationService + .getPhysicalTypeIdentifier(javaType); + if (superTypeId == null + || metadataService.get(superTypeId) == null) { + continue; + } + final MemberHoldingTypeDetails superType = ((PhysicalTypeMetadata) metadataService + .get(superTypeId)).getMemberHoldingTypeDetails(); + extendsTypes.add(superType); + } + } + return extendsTypes; + } + + public String getGwtModuleXml(final String moduleName) { + final LogicalPath logicalPath = LogicalPath.getInstance( + Path.SRC_MAIN_JAVA, moduleName); + final String gwtModuleXml = projectOperations.getPathResolver() + .getRoot(logicalPath) + + File.separatorChar + + projectOperations.getTopLevelPackage(moduleName) + .getFullyQualifiedPackageName() + .replace('.', File.separatorChar) + + File.separator + + "*.gwt.xml"; + final Set paths = new LinkedHashSet(); + for (final FileDetails fileDetails : fileManager + .findMatchingAntPath(gwtModuleXml)) { + paths.add(fileDetails.getCanonicalPath()); + } + if (paths.isEmpty()) { + throw new IllegalStateException( + "Each module must have a gwt.xml file"); + } + if (paths.size() > 1) { + throw new IllegalStateException( + "Each module can only have only gwt.xml file: " + + paths.size()); + } + return paths.iterator().next(); + } + + /** + * Return the type arg for the client side method, given the domain method + * return type. If domain method return type is List or + * Set, returns the same. If domain method return type is + * List, return List + * + * @param returnType + * @param projectMetadata + * @param governorType + * @return the GWT side leaf type as a JavaType + */ + + public JavaType getGwtSideLeafType(final JavaType returnType, + final JavaType governorType, final boolean requestType, + final boolean convertPrimitive) { + if (returnType.isPrimitive() && convertPrimitive) { + if (!requestType) { + checkPrimitive(returnType); + } + return GwtUtils.convertPrimitiveType(returnType, requestType); + } + + if (isTypeCommon(returnType)) { + return returnType; + } + + if (isCollectionType(returnType)) { + final List args = returnType.getParameters(); + if (args != null && args.size() == 1) { + final JavaType elementType = args.get(0); + final JavaType convertedJavaType = getGwtSideLeafType( + elementType, governorType, requestType, + convertPrimitive); + if (convertedJavaType == null) { + return null; + } + return new JavaType(returnType.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, Arrays.asList(convertedJavaType)); + } + return returnType; + } + + final ClassOrInterfaceTypeDetails ptmd = typeLocationService + .getTypeDetails(returnType); + if (isDomainObject(returnType, ptmd)) { + if (isEmbeddable(ptmd)) { + throw new IllegalStateException( + "GWT does not currently support embedding objects in entities, such as '" + + returnType.getSimpleTypeName() + "' in '" + + governorType.getSimpleTypeName() + "'."); + } + final ClassOrInterfaceTypeDetails typeDetails = typeLocationService + .getTypeDetails(returnType); + if (typeDetails == null) { + return null; + } + final ClassOrInterfaceTypeDetails proxy = lookupProxyFromEntity(typeDetails); + if (proxy == null) { + return null; + } + return proxy.getName(); + } + return returnType; + } + + public Document getGwtXmlDocument(final String gwtModuleCanonicalPath) { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + builder.setEntityResolver(new EntityResolver() { + public InputSource resolveEntity(final String publicId, + final String systemId) throws SAXException, IOException { + if (systemId.endsWith("gwt-module.dtd")) { + return new InputSource(FileUtils.getInputStream( + GwtScaffoldMetadata.class, + "templates/gwt-module.dtd")); + } + // Use the default behaviour + return null; + } + }); + + InputStream inputStream = null; + try { + inputStream = fileManager.getInputStream(gwtModuleCanonicalPath); + return builder.parse(inputStream); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + public List getProxyMethods( + final ClassOrInterfaceTypeDetails governorTypeDetails) { + final List proxyMethods = new ArrayList(); + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(GwtTypeServiceImpl.class.getName(), + governorTypeDetails); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails + .getDetails()) { + for (final MethodMetadata method : memberDetails.getMethods()) { + if (!proxyMethods.contains(method) + && isPublicAccessor(method) + && isValidMethodReturnType(method, + memberHoldingTypeDetails)) { + if (method + .getCustomData() + .keySet() + .contains(CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD)) { + proxyMethods.add(0, method); + } + else { + proxyMethods.add(method); + } + } + } + } + return proxyMethods; + } + + public JavaType getServiceLocator(final String moduleName) { + return new JavaType(projectOperations.getTopLevelPackage(moduleName) + + ".server.locator.GwtServiceLocator"); + } + + public Collection getSourcePackages(final String moduleName) { + final Document gwtXmlDoc = getGwtXmlDocument(getGwtModuleXml(moduleName)); + final Element gwtXmlRoot = gwtXmlDoc.getDocumentElement(); + final JavaPackage topLevelPackage = projectOperations + .getTopLevelPackage(moduleName); + final Collection sourcePackages = new HashSet(); + for (final Element sourcePathElement : XmlUtils.findElements( + "/module/source", gwtXmlRoot)) { + final String relativePackage = sourcePathElement.getAttribute(PATH) + .replace(GwtOperations.PATH_DELIMITER, "."); + sourcePackages.add(new JavaPackage(topLevelPackage + "." + + relativePackage)); + } + return sourcePackages; + } + + private boolean isAllowableReturnType(final JavaType type) { + return isCommonType(type) || isEntity(type) || isEnum(type); + } + + private boolean isAllowableReturnType(final MethodMetadata method) { + return isAllowableReturnType(method.getReturnType()); + } + + private boolean isCollectionType(final JavaType returnType) { + return returnType.getFullyQualifiedTypeName().equals( + LIST.getFullyQualifiedTypeName()) + || returnType.getFullyQualifiedTypeName().equals( + SET.getFullyQualifiedTypeName()); + } + + private boolean isCommonType(final JavaType type) { + return isTypeCommon(type) || isCollectionType(type) + && type.getParameters().size() == 1 + && isAllowableReturnType(type.getParameters().get(0)); + } + + public boolean isDomainObject(final JavaType type) { + final ClassOrInterfaceTypeDetails ptmd = typeLocationService + .getTypeDetails(type); + return isDomainObject(type, ptmd); + } + + private boolean isDomainObject(final JavaType returnType, + final ClassOrInterfaceTypeDetails ptmd) { + return !isEnum(ptmd) && isEntity(returnType) + && !isRequestFactoryCompatible(returnType) + && !isEmbeddable(ptmd); + } + + private boolean isEmbeddable(final ClassOrInterfaceTypeDetails ptmd) { + if (ptmd == null) { + return false; + } + final AnnotationMetadata annotationMetadata = ptmd + .getAnnotation(EMBEDDABLE); + return annotationMetadata != null; + } + + private boolean isEntity(final JavaType type) { + return persistenceMemberLocator.getIdentifierFields(type).size() == 1; + } + + private boolean isEnum(final ClassOrInterfaceTypeDetails ptmd) { + return ptmd != null + && ptmd.getPhysicalTypeCategory() == PhysicalTypeCategory.ENUMERATION; + } + + private boolean isEnum(final JavaType type) { + return isEnum(typeLocationService.getTypeDetails(type)); + } + + public boolean isMethodReturnTypeInSourcePath(final MethodMetadata method, + final MemberHoldingTypeDetails memberHoldingTypeDetail, + final Iterable sourcePackages) { + final JavaType returnType = method.getReturnType(); + final boolean inSourcePath = isTypeInAnySourcePackage(returnType, + sourcePackages); + if (!inSourcePath + && !isCommonType(returnType) + && !JavaType.VOID_PRIMITIVE.getFullyQualifiedTypeName().equals( + returnType.getFullyQualifiedTypeName())) { + displayWarning("The path to type " + + returnType.getFullyQualifiedTypeName() + + " which is used in type " + + memberHoldingTypeDetail.getName() + + " by the field '" + + method.getMethodName().getSymbolName() + + "' needs to be added to the module's gwt.xml file in order to be used in a Proxy."); + return false; + } + return true; + } + + private boolean isPrimitive(final JavaType type) { + return type.isPrimitive() || isCollectionType(type) + && type.getParameters().size() == 1 + && isPrimitive(type.getParameters().get(0)); + } + + private boolean isPublicAccessor(final MethodMetadata method) { + return Modifier.isPublic(method.getModifier()) + && !method.getReturnType().equals(JavaType.VOID_PRIMITIVE) + && method.getParameterTypes().isEmpty() + && method.getMethodName().getSymbolName().startsWith("get"); + } + + private boolean isRequestFactoryCompatible(final JavaType type) { + return isCommonType(type) || isCollectionType(type); + } + + private boolean isTypeCommon(final JavaType type) { + return JavaType.BOOLEAN_OBJECT.equals(type) + || JavaType.CHAR_OBJECT.equals(type) + || JavaType.BYTE_OBJECT.equals(type) + || JavaType.SHORT_OBJECT.equals(type) + || JavaType.INT_OBJECT.equals(type) + || LONG_OBJECT.equals(type) + || JavaType.FLOAT_OBJECT.equals(type) + || JavaType.DOUBLE_OBJECT.equals(type) + || JavaType.STRING.equals(type) + || DATE.equals(type) + || BIG_DECIMAL.equals(type) + || type.isPrimitive() + && !JavaType.VOID_PRIMITIVE.getFullyQualifiedTypeName().equals( + type.getFullyQualifiedTypeName()); + } + + private boolean isTypeInAnySourcePackage(final JavaType type, + final Iterable sourcePackages) { + for (final JavaPackage sourcePackage : sourcePackages) { + if (type.getPackage().isWithin(sourcePackage)) { + return true; // It's a project type + } + if (isCollectionType(type) + && type.getParameters().size() == 1 + && type.getParameters().get(0).getPackage() + .isWithin(sourcePackage)) { + return true; // It's a collection of a project type + } + } + return false; + } + + private boolean isValidMethodReturnType(final MethodMetadata method, + final MemberHoldingTypeDetails memberHoldingTypeDetail) { + final JavaType returnType = method.getReturnType(); + if (isPrimitive(returnType)) { + displayWarning("The primitive field type, " + + method.getReturnType().getSimpleTypeName().toLowerCase() + + " of '" + + method.getMethodName().getSymbolName() + + "' in type " + + memberHoldingTypeDetail.getName().getSimpleTypeName() + + " is not currently support by GWT and will not be added to the scaffolded application."); + return false; + } + + final JavaSymbolName propertyName = new JavaSymbolName( + StringUtils.uncapitalize(BeanInfoUtils + .getPropertyNameForJavaBeanMethod(method) + .getSymbolName())); + if (!isAllowableReturnType(method)) { + displayWarning("The field type " + + method.getReturnType().getFullyQualifiedTypeName() + + " of '" + + method.getMethodName().getSymbolName() + + "' in type " + + memberHoldingTypeDetail.getName().getSimpleTypeName() + + " is not currently support by GWT and will not be added to the scaffolded application."); + return false; + } + if (propertyName.getSymbolName().equals("owner")) { + displayWarning("'owner' is not allowed to be used as field name as it is currently reserved by GWT. Please rename the field 'owner' in type " + + memberHoldingTypeDetail.getName().getSimpleTypeName() + + "."); + return false; + } + + return true; + } + + public ClassOrInterfaceTypeDetails lookupEntityFromLocator( + final ClassOrInterfaceTypeDetails locator) { + Validate.notNull(locator, "Locator is required"); + return lookupTargetFromX(locator, RooJavaType.ROO_GWT_LOCATOR); + } + + public ClassOrInterfaceTypeDetails lookupEntityFromProxy( + final ClassOrInterfaceTypeDetails proxy) { + Validate.notNull(proxy, "Proxy is required"); + return lookupTargetFromX(proxy, RooJavaType.ROO_GWT_PROXY); + } + + public ClassOrInterfaceTypeDetails lookupEntityFromRequest( + final ClassOrInterfaceTypeDetails request) { + Validate.notNull(request, "Request is required"); + return lookupTargetFromX(request, RooJavaType.ROO_GWT_REQUEST); + } + + public ClassOrInterfaceTypeDetails lookupProxyFromEntity( + final ClassOrInterfaceTypeDetails entity) { + return lookupXFromEntity(entity, RooJavaType.ROO_GWT_PROXY); + } + + public ClassOrInterfaceTypeDetails lookupProxyFromRequest( + final ClassOrInterfaceTypeDetails request) { + final AnnotationMetadata annotation = GwtUtils.getFirstAnnotation( + request, RooJavaType.ROO_GWT_REQUEST); + Validate.notNull(annotation, "Request '%s' isn't annotated with '%s'", + request.getName(), RooJavaType.ROO_GWT_REQUEST); + final AnnotationAttributeValue attributeValue = annotation + .getAttribute("value"); + final JavaType proxyType = new JavaType( + GwtUtils.getStringValue(attributeValue)); + return lookupProxyFromEntity(typeLocationService + .getTypeDetails(proxyType)); + } + + public ClassOrInterfaceTypeDetails lookupRequestFromEntity( + final ClassOrInterfaceTypeDetails entity) { + return lookupXFromEntity(entity, RooJavaType.ROO_GWT_REQUEST); + } + + public ClassOrInterfaceTypeDetails lookupRequestFromProxy( + final ClassOrInterfaceTypeDetails proxy) { + final AnnotationMetadata annotation = GwtUtils.getFirstAnnotation( + proxy, RooJavaType.ROO_GWT_PROXY); + Validate.notNull(annotation, "Proxy '%s' isn't annotated with '%s'", + proxy.getName(), RooJavaType.ROO_GWT_PROXY); + final AnnotationAttributeValue attributeValue = annotation + .getAttribute("value"); + final JavaType serviceNameType = new JavaType( + GwtUtils.getStringValue(attributeValue)); + return lookupRequestFromEntity(typeLocationService + .getTypeDetails(serviceNameType)); + } + + public ClassOrInterfaceTypeDetails lookupUnmanagedRequestFromProxy( + final ClassOrInterfaceTypeDetails proxy) { + final AnnotationMetadata annotation = GwtUtils.getFirstAnnotation( + proxy, RooJavaType.ROO_GWT_PROXY); + Validate.notNull(annotation, "Proxy '%s' isn't annotated with '%s'", + proxy.getName(), RooJavaType.ROO_GWT_PROXY); + final AnnotationAttributeValue attributeValue = annotation + .getAttribute("value"); + final JavaType serviceNameType = new JavaType( + GwtUtils.getStringValue(attributeValue)); + return lookupUnmanagedRequestFromEntity(typeLocationService + .getTypeDetails(serviceNameType)); + } + + public ClassOrInterfaceTypeDetails lookupUnmanagedRequestFromEntity( + final ClassOrInterfaceTypeDetails entity) { + return lookupXFromEntity(entity, RooJavaType.ROO_GWT_UNMANAGED_REQUEST); + } + + public ClassOrInterfaceTypeDetails lookupLocatorFromEntity( + final ClassOrInterfaceTypeDetails entity) { + return lookupXFromEntity(entity, RooJavaType.ROO_GWT_LOCATOR); + } + + public ClassOrInterfaceTypeDetails lookupTargetFromX( + final ClassOrInterfaceTypeDetails annotatedType, + final JavaType... annotations) { + final AnnotationMetadata annotation = GwtUtils.getFirstAnnotation( + annotatedType, annotations); + Validate.notNull(annotation, + "Type '" + annotatedType.getName() + "' isn't annotated with '" + + StringUtils.join(Arrays.asList(annotations), ",") + + "'"); + final AnnotationAttributeValue attributeValue = annotation + .getAttribute("value"); + final JavaType targetType = new JavaType( + GwtUtils.getStringValue(attributeValue)); + return typeLocationService.getTypeDetails(targetType); + } + + public ClassOrInterfaceTypeDetails lookupTargetServiceFromRequest( + final ClassOrInterfaceTypeDetails request) { + Validate.notNull(request, "Request is required"); + return lookupTargetFromX(request, GwtUtils.REQUEST_ANNOTATIONS); + } + + public ClassOrInterfaceTypeDetails lookupXFromEntity( + final ClassOrInterfaceTypeDetails entity, + final JavaType... annotations) { + Validate.notNull(entity, "Entity not found"); + for (final ClassOrInterfaceTypeDetails cid : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(annotations)) { + final AnnotationMetadata annotationMetadata = GwtUtils + .getFirstAnnotation(cid, annotations); + if (annotationMetadata != null) { + final AnnotationAttributeValue attributeValue = annotationMetadata + .getAttribute("value"); + final String value = GwtUtils.getStringValue(attributeValue); + if (entity.getName().getFullyQualifiedTypeName().equals(value)) { + return cid; + } + } + } + return null; + } + + private Map resolveTypes(final JavaType generic, + final JavaType typed) { + final Map typeMap = new LinkedHashMap(); + final boolean typeCountMatch = generic.getParameters().size() == typed + .getParameters().size(); + Validate.isTrue(typeCountMatch, "Type count must match."); + + int i = 0; + for (final JavaType genericParamType : generic.getParameters()) { + typeMap.put(genericParamType.getArgName(), typed.getParameters() + .get(i)); + i++; + } + return typeMap; + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtUtils.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtUtils.java new file mode 100644 index 000000000..49c0f3a17 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/GwtUtils.java @@ -0,0 +1,217 @@ +package org.springframework.roo.addon.gwt; + +import static org.springframework.roo.addon.gwt.GwtJavaType.PROXY_FOR; +import static org.springframework.roo.addon.gwt.GwtJavaType.PROXY_FOR_NAME; +import static org.springframework.roo.addon.gwt.GwtJavaType.RECEIVER; +import static org.springframework.roo.addon.gwt.GwtJavaType.SERVICE; +import static org.springframework.roo.addon.gwt.GwtJavaType.SERVICE_NAME; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_MIRRORED_FROM; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_PROXY; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_REQUEST; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Utility methods used in the GWT Add-On. + * + * @author James Tyrrell + * @since 1.1.2 + */ +public final class GwtUtils { + + public static final JavaType[] PROXY_ANNOTATIONS = { PROXY_FOR, + PROXY_FOR_NAME }; + public static final String PROXY_REQUEST_WARNING = "// WARNING: THIS FILE IS MANAGED BY SPRING ROO.\n\n"; + public static final JavaType[] REQUEST_ANNOTATIONS = { SERVICE, + SERVICE_NAME }; + public static final JavaType[] ROO_PROXY_REQUEST_ANNOTATIONS = { + ROO_GWT_PROXY, ROO_GWT_REQUEST, ROO_GWT_MIRRORED_FROM }; + + public static JavaType convertGovernorTypeNameIntoKeyTypeName( + final JavaType governorType, final GwtType type, + final JavaPackage topLevelPackage) { + final String destinationPackage = type.getPath().packageName( + topLevelPackage); + String typeName; + if (type.isMirrorType()) { + final String simple = governorType.getSimpleTypeName(); + typeName = destinationPackage + "." + simple + type.getSuffix(); + } + else { + typeName = destinationPackage + "." + type.getTemplate(); + } + return new JavaType(typeName); + } + + public static JavaType convertPrimitiveType(final JavaType type, + final boolean convertVoid) { + if (!convertVoid && JavaType.VOID_PRIMITIVE.equals(type)) { + return type; + } + if (type != null && type.isPrimitive()) { + return new JavaType(type.getFullyQualifiedTypeName()); + } + return type; + } + + public static List getAnnotationValues( + final ClassOrInterfaceTypeDetails target, + final JavaType annotationType, final String attributeName) { + final List values = new ArrayList(); + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(target.getAnnotations(), annotationType); + if (annotation == null) { + return values; + } + final AnnotationAttributeValue attributeValue = annotation + .getAttribute(attributeName); + if (attributeValue != null + && attributeValue instanceof ArrayAttributeValue) { + @SuppressWarnings("unchecked") + final ArrayAttributeValue arrayAttributeValue = (ArrayAttributeValue) attributeValue; + for (final StringAttributeValue value : arrayAttributeValue + .getValue()) { + values.add(value.getValue()); + } + } + else if (attributeValue != null + && attributeValue instanceof StringAttributeValue) { + final StringAttributeValue stringAttributeVale = (StringAttributeValue) attributeValue; + values.add(stringAttributeVale.getValue()); + } + return values; + } + + public static boolean getBooleanAnnotationValue( + final ClassOrInterfaceTypeDetails target, + final JavaType annotationType, final String attributeName, + final boolean valueIfNull) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(target.getAnnotations(), annotationType); + if (annotation == null) { + return valueIfNull; + } + final AnnotationAttributeValue attributeValue = annotation + .getAttribute(attributeName); + if (attributeValue != null + && attributeValue instanceof BooleanAttributeValue) { + final BooleanAttributeValue booleanAttributeValue = (BooleanAttributeValue) attributeValue; + return booleanAttributeValue.getValue(); + } + return valueIfNull; + } + + public static AnnotationMetadata getFirstAnnotation( + final ClassOrInterfaceTypeDetails cid, + final JavaType... annotationTypes) { + for (final JavaType annotationType : annotationTypes) { + final AnnotationMetadata annotationMetadata = MemberFindingUtils + .getAnnotationOfType(cid.getAnnotations(), annotationType); + if (annotationMetadata != null) { + return annotationMetadata; + } + } + return null; + } + + public static Map getMirrorTypeMap( + final JavaType governorType, final JavaPackage topLevelPackage) { + final Map mirrorTypeMap = new HashMap(); + for (final GwtType mirrorType : GwtType.values()) { + mirrorTypeMap.put( + mirrorType, + convertGovernorTypeNameIntoKeyTypeName(governorType, + mirrorType, topLevelPackage)); + } + return mirrorTypeMap; + } + + /** + * Returns the {@link #RECEIVER} Java type, generically typed to the given + * type. + * + * @param genericType (required) + * @return a non-null type + */ + public static JavaType getReceiverType(final JavaType genericType) { + return new JavaType(RECEIVER.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, Collections.singletonList(genericType)); + } + + public static String getStringValue( + final AnnotationAttributeValue attributeValue) { + if (attributeValue instanceof StringAttributeValue) { + return ((StringAttributeValue) attributeValue).getValue(); + } + else if (attributeValue instanceof ClassAttributeValue) { + return ((ClassAttributeValue) attributeValue).getValue() + .getFullyQualifiedTypeName(); + } + return null; + } + + public static JavaType lookupProxyTargetType( + final ClassOrInterfaceTypeDetails proxyType) { + return lookupTargetType(proxyType, PROXY_FOR, PROXY_FOR_NAME); + } + + public static JavaType lookupRequestTargetType( + final ClassOrInterfaceTypeDetails requestType) { + return lookupTargetType(requestType, SERVICE, SERVICE_NAME); + } + + private static JavaType lookupTargetType( + final ClassOrInterfaceTypeDetails annotatedType, + final JavaType classBasedAnnotationType, + final JavaType stringBasedAnnotationType) { + final AnnotationMetadata stringBasedAnnotation = annotatedType + .getAnnotation(stringBasedAnnotationType); + if (stringBasedAnnotation != null) { + final AnnotationAttributeValue targetTypeAttributeValue = stringBasedAnnotation + .getAttribute("value"); + if (targetTypeAttributeValue != null) { + return new JavaType(targetTypeAttributeValue.getValue()); + } + } + + final AnnotationMetadata classBasedAnnotation = annotatedType + .getAnnotation(classBasedAnnotationType); + if (classBasedAnnotation != null) { + final AnnotationAttributeValue targetTypeAttributeValue = classBasedAnnotation + .getAttribute("value"); + if (targetTypeAttributeValue != null) { + return targetTypeAttributeValue.getValue(); + } + } + + return null; + } + + public static boolean scaffoldProxy(final ClassOrInterfaceTypeDetails proxy) { + return GwtUtils.getBooleanAnnotationValue(proxy, + RooJavaType.ROO_GWT_PROXY, "scaffold", false); + } + + /** + * Constructor is private to prevent instantiation + */ + private GwtUtils() { + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtLocator.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtLocator.java new file mode 100644 index 000000000..a6a4cd3fe --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtLocator.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.gwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooGwtLocator { + + /** + * @return the fully-qualified type name this key instance was mirrored from + */ + String value(); + + /** + * Indicates whether the annotated locator should be instantiated using XML + * configuration + * + * @return see above + */ + boolean useXmlConfiguration() default false; + +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtMirroredFrom.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtMirroredFrom.java new file mode 100644 index 000000000..0d4698054 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtMirroredFrom.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.gwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooGwtMirroredFrom { + + boolean dontIncludeProxyMethods() default true; + + String[] exclude() default {}; + + boolean ignoreProxyExclusions() default false; + + boolean ignoreProxyReadOnly() default false; + + String[] readOnly() default {}; + + boolean scaffold() default false; + + String value(); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtProxy.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtProxy.java new file mode 100644 index 000000000..f5675894a --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtProxy.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.gwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooGwtProxy { + + String[] exclude() default {}; + + String[] readOnly() default {}; + + boolean scaffold() default false; + + /** + * @return the fully-qualified type name this key instance was mirrored from + */ + String value(); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtRequest.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtRequest.java new file mode 100644 index 000000000..a9aaa23c1 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtRequest.java @@ -0,0 +1,31 @@ +package org.springframework.roo.addon.gwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooGwtRequest { + + boolean dontIncludeProxyMethods() default true; + + /** + * Entity methods to exclude from the request interface. + * + * @return + * @deprecated ignored by the GWT addon + */ + @Deprecated + String[] exclude() default {}; + + boolean ignoreProxyExclusions() default false; + + boolean ignoreProxyReadOnly() default false; + + /** + * @return the fully-qualified type name this key instance was mirrored from + */ + String value(); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtUnmanagedRequest.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtUnmanagedRequest.java new file mode 100755 index 000000000..342d075cb --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/RooGwtUnmanagedRequest.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.gwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooGwtUnmanagedRequest { + String value(); +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/TemplateResourceLoader.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/TemplateResourceLoader.java new file mode 100644 index 000000000..cb22b1fa9 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/TemplateResourceLoader.java @@ -0,0 +1,106 @@ +package org.springframework.roo.addon.gwt; + +import hapax.Template; +import hapax.TemplateException; +import hapax.TemplateLoader; +import hapax.parser.TemplateParser; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; + +/** + * Loads hapax templates from the classpath. + * + * @author Alan Stewart + * @since 1.1 + */ +public class TemplateResourceLoader implements TemplateLoader { + + private static final Map cache = new HashMap(); + private static final String TEMPLATE_DIR = "org/springframework/roo/addon/gwt/scaffold/templates/"; + + /** + * Creates a TemplateLoader for CTemplate language using the default + * template directory + */ + public static TemplateLoader create() { + return new TemplateResourceLoader(TEMPLATE_DIR); + } + + /** + * Creates a TemplateLoader for CTemplate language + */ + public static TemplateLoader create(final String base_path) { + return new TemplateResourceLoader(base_path); + } + + /** + * Creates a TemplateLoader using the argument parser. + */ + public static TemplateLoader createForParser(final String base_path, + final TemplateParser parser) { + return new TemplateResourceLoader(base_path, parser); + } + + protected final String baseDir; + + protected final TemplateParser parser; + + public TemplateResourceLoader(final String baseDir) { + this(baseDir, null); + } + + public TemplateResourceLoader(final String baseDir, + final TemplateParser parser) { + this.baseDir = baseDir; + this.parser = parser; + } + + public Template getTemplate(final String resource) throws TemplateException { + return getTemplate(new TemplateLoader.Context(this, baseDir), resource); + } + + public Template getTemplate(final TemplateLoader context, String resource) + throws TemplateException { + if (!resource.endsWith(".xtm")) { + resource += ".xtm"; + } + + final String templatePath = baseDir + resource; + if (cache.containsKey(templatePath)) { + return cache.get(templatePath); + } + + final InputStream inputStream = getClass().getClassLoader() + .getResourceAsStream(templatePath); + Validate.notNull(inputStream, "template path required"); + String contents; + try { + contents = IOUtils.toString(inputStream); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + + final Template template = parser == null ? new Template(contents, + context) : new Template(parser, contents, context); + + synchronized (cache) { + cache.put(templatePath, template); + } + + return template; + } + + public String getTemplateDirectory() { + return baseDir; + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadata.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadata.java new file mode 100644 index 000000000..133ea1b8a --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadata.java @@ -0,0 +1,60 @@ +package org.springframework.roo.addon.gwt.locator; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +public class GwtLocatorMetadata extends AbstractMetadataItem { + + private static final String PROVIDES_TYPE_STRING = GwtLocatorMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentifierType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final String proxyTypeContents; + + public GwtLocatorMetadata(final String id, final String proxyTypeContents) { + super(id); + this.proxyTypeContents = proxyTypeContents; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final GwtLocatorMetadata that = (GwtLocatorMetadata) o; + return StringUtils.equals(proxyTypeContents, that.proxyTypeContents); + } + + @Override + public int hashCode() { + return proxyTypeContents == null ? 0 : proxyTypeContents.hashCode(); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadataProvider.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadataProvider.java new file mode 100644 index 000000000..b29b10fe8 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadataProvider.java @@ -0,0 +1,8 @@ +package org.springframework.roo.addon.gwt.locator; + +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataProvider; + +public interface GwtLocatorMetadataProvider extends MetadataProvider, + MetadataNotificationListener { +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadataProviderImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadataProviderImpl.java new file mode 100644 index 000000000..22b3e3fa6 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/locator/GwtLocatorMetadataProviderImpl.java @@ -0,0 +1,421 @@ +package org.springframework.roo.addon.gwt.locator; + +import static org.springframework.roo.addon.gwt.GwtJavaType.LOCATOR; +import static org.springframework.roo.model.JavaType.CLASS; + +import java.lang.reflect.Modifier; +import java.util.Arrays; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.gwt.GwtTemplateService; +import org.springframework.roo.addon.gwt.GwtTypeService; +import org.springframework.roo.addon.gwt.GwtUtils; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; + +@Component +@Service +public class GwtLocatorMetadataProviderImpl implements + GwtLocatorMetadataProvider { + + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + + @Reference GwtTypeService gwtTypeService; + @Reference GwtTemplateService gwtTemplateService; + @Reference LayerService layerService; + @Reference MetadataDependencyRegistry metadataDependencyRegistry; + @Reference MetadataService metadataService; + @Reference PersistenceMemberLocator persistenceMemberLocator; + @Reference ProjectOperations projectOperations; + @Reference TypeLocationService typeLocationService; + @Reference TypeManagementService typeManagementService; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + public MetadataItem get(final String metadataIdentificationString) { + final ClassOrInterfaceTypeDetails proxy = getGovernor(metadataIdentificationString); + if (proxy == null) { + return null; + } + + final AnnotationMetadata proxyAnnotation = GwtUtils.getFirstAnnotation( + proxy, GwtUtils.PROXY_ANNOTATIONS); + if (proxyAnnotation == null) { + return null; + } + + final String locatorType = GwtUtils.getStringValue(proxyAnnotation + .getAttribute("locator")); + if (StringUtils.isBlank(locatorType)) { + return null; + } + + final ClassOrInterfaceTypeDetails entityType = gwtTypeService + .lookupEntityFromProxy(proxy); + if (entityType == null || Modifier.isAbstract(entityType.getModifier())) { + return null; + } + + boolean useXmlConfiguration = false; + final ClassOrInterfaceTypeDetails existingLocator = gwtTypeService + .lookupLocatorFromEntity(entityType); + if (existingLocator != null) { + AnnotationMetadata annotation = existingLocator + .getAnnotation(RooJavaType.ROO_GWT_LOCATOR); + AnnotationAttributeValue attribute = annotation + .getAttribute("useXmlConfiguration"); + if (attribute != null) + useXmlConfiguration = attribute.getValue(); + } + + final JavaType entity = entityType.getName(); + final MethodMetadata identifierAccessor = persistenceMemberLocator + .getIdentifierAccessor(entity); + final MethodMetadata versionAccessor = persistenceMemberLocator + .getVersionAccessor(entity); + if (identifierAccessor == null || versionAccessor == null) { + return null; + } + + final JavaType identifierType = GwtUtils.convertPrimitiveType( + identifierAccessor.getReturnType(), true); + final String locatorPhysicalTypeId = PhysicalTypeIdentifier + .createIdentifier(new JavaType(locatorType), + PhysicalTypeIdentifier.getPath(proxy + .getDeclaredByMetadataId())); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + locatorPhysicalTypeId); + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder( + RooJavaType.ROO_GWT_LOCATOR); + annotationMetadataBuilder.addStringAttribute("value", + entity.getFullyQualifiedTypeName()); + + if (useXmlConfiguration) { + annotationMetadataBuilder.addBooleanAttribute( + "useXmlConfiguration", true); + } + cidBuilder.addAnnotation(annotationMetadataBuilder); + + if (!useXmlConfiguration) { + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + SpringJavaType.COMPONENT)); + } + cidBuilder.setName(new JavaType(locatorType)); + cidBuilder.setModifier(Modifier.PUBLIC); + cidBuilder.setPhysicalTypeCategory(PhysicalTypeCategory.CLASS); + cidBuilder.addExtendsTypes(new JavaType(LOCATOR + .getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays + .asList(entity, identifierType))); + cidBuilder.addMethod(getCreateMethod(locatorPhysicalTypeId, entity)); + + final MemberTypeAdditions findMethodAdditions = layerService + .getMemberTypeAdditions(locatorPhysicalTypeId, + CustomDataKeys.FIND_METHOD.name(), entity, + identifierType, LAYER_POSITION, !useXmlConfiguration, + new MethodParameter(identifierType, "id")); + + JavaType potentialService = null; + FieldMetadata fieldMetadata = findMethodAdditions.getInvokedField(); + if (fieldMetadata != null) { + potentialService = fieldMetadata.getFieldType(); + } + + Validate.notNull(findMethodAdditions, + "Find method not available for entity '%s'", + entity.getFullyQualifiedTypeName()); + cidBuilder.addMethod(getFindMethod(findMethodAdditions, cidBuilder, + locatorPhysicalTypeId, entity, identifierType)); + + cidBuilder + .addMethod(getDomainTypeMethod(locatorPhysicalTypeId, entity)); + cidBuilder.addMethod(getIdMethod(locatorPhysicalTypeId, entity, + identifierAccessor)); + cidBuilder.addMethod(getIdTypeMethod(locatorPhysicalTypeId, entity, + identifierType)); + cidBuilder.addMethod(getVersionMethod(locatorPhysicalTypeId, entity, + versionAccessor)); + + ClassOrInterfaceTypeDetails locator = cidBuilder.build(); + + // Adds or removes locator from XML configuration + if (useXmlConfiguration) { + gwtTemplateService.addLocatorToXmlConfiguration(locator, + potentialService); + } + else { + gwtTemplateService.removeLocatorFromXmlConfiguration(locator); + } + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + return null; + } + + private MethodMetadataBuilder getCreateMethod(final String declaredById, + final JavaType targetType) { + final InvocableMemberBodyBuilder invocableMemberBodyBuilder = InvocableMemberBodyBuilder + .getInstance(); + invocableMemberBodyBuilder.append("return new " + + targetType.getSimpleTypeName() + "();"); + final MethodMetadataBuilder createMethodBuilder = new MethodMetadataBuilder( + declaredById, Modifier.PUBLIC, new JavaSymbolName("create"), + targetType, invocableMemberBodyBuilder); + final JavaType wildEntityType = new JavaType( + targetType.getFullyQualifiedTypeName(), 0, DataType.VARIABLE, + JavaType.WILDCARD_EXTENDS, null); + final JavaType classParameterType = new JavaType( + JavaType.CLASS.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(wildEntityType)); + createMethodBuilder.addParameter("clazz", classParameterType); + return createMethodBuilder; + } + + private MethodMetadataBuilder getDomainTypeMethod( + final String declaredById, final JavaType targetType) { + final InvocableMemberBodyBuilder invocableMemberBodyBuilder = InvocableMemberBodyBuilder + .getInstance(); + invocableMemberBodyBuilder.append("return " + + targetType.getSimpleTypeName() + ".class;"); + final JavaType returnType = new JavaType( + CLASS.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(targetType)); + return new MethodMetadataBuilder(declaredById, Modifier.PUBLIC, + new JavaSymbolName("getDomainType"), returnType, + invocableMemberBodyBuilder); + } + + private MethodMetadataBuilder getFindMethod( + final MemberTypeAdditions findMethodAdditions, + final ClassOrInterfaceTypeDetailsBuilder locatorBuilder, + final String declaredById, final JavaType targetType, + final JavaType idType) { + final InvocableMemberBodyBuilder invocableMemberBodyBuilder = InvocableMemberBodyBuilder + .getInstance(); + invocableMemberBodyBuilder.append("return ") + .append(findMethodAdditions.getMethodCall()).append(";"); + findMethodAdditions.copyAdditionsTo(locatorBuilder, + locatorBuilder.build()); + final MethodMetadataBuilder findMethodBuilder = new MethodMetadataBuilder( + declaredById, Modifier.PUBLIC, new JavaSymbolName("find"), + targetType, invocableMemberBodyBuilder); + final JavaType wildEntityType = new JavaType( + targetType.getFullyQualifiedTypeName(), 0, DataType.VARIABLE, + JavaType.WILDCARD_EXTENDS, null); + final JavaType classParameterType = new JavaType( + JavaType.CLASS.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(wildEntityType)); + findMethodBuilder.addParameter("clazz", classParameterType); + findMethodBuilder.addParameter("id", idType); + return findMethodBuilder; + } + + private ClassOrInterfaceTypeDetails getGovernor( + final String metadataIdentificationString) { + final JavaType governorTypeName = GwtLocatorMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath governorTypePath = GwtLocatorMetadata + .getPath(metadataIdentificationString); + final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier( + governorTypeName, governorTypePath); + return typeLocationService.getTypeDetails(physicalTypeId); + } + + private MethodMetadataBuilder getIdMethod(final String declaredById, + final JavaType targetType, final MethodMetadata idAccessor) { + final InvocableMemberBodyBuilder invocableMemberBodyBuilder = InvocableMemberBodyBuilder + .getInstance(); + invocableMemberBodyBuilder.append("return " + + StringUtils.uncapitalize(targetType.getSimpleTypeName()) + + "." + idAccessor.getMethodName() + "();"); + final MethodMetadataBuilder getIdMethod = new MethodMetadataBuilder( + declaredById, + Modifier.PUBLIC, + new JavaSymbolName("getId"), + GwtUtils.convertPrimitiveType(idAccessor.getReturnType(), true), + invocableMemberBodyBuilder); + getIdMethod.addParameter( + StringUtils.uncapitalize(targetType.getSimpleTypeName()), + targetType); + return getIdMethod; + } + + private MethodMetadataBuilder getIdTypeMethod(final String declaredById, + final JavaType targetType, final JavaType idType) { + final InvocableMemberBodyBuilder invocableMemberBodyBuilder = InvocableMemberBodyBuilder + .getInstance(); + invocableMemberBodyBuilder.append("return " + + idType.getSimpleTypeName() + ".class;"); + final JavaType returnType = new JavaType( + JavaType.CLASS.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(idType)); + return new MethodMetadataBuilder(declaredById, Modifier.PUBLIC, + new JavaSymbolName("getIdType"), returnType, + invocableMemberBodyBuilder); + } + + public String getProvidesType() { + return GwtLocatorMetadata.getMetadataIdentifierType(); + } + + private MethodMetadataBuilder getVersionMethod(final String declaredById, + final JavaType targetType, final MethodMetadata versionAccessor) { + final InvocableMemberBodyBuilder invocableMemberBodyBuilder = InvocableMemberBodyBuilder + .getInstance(); + invocableMemberBodyBuilder.append("return " + + StringUtils.uncapitalize(targetType.getSimpleTypeName()) + + "." + versionAccessor.getMethodName() + "();"); + final MethodMetadataBuilder getIdMethodBuilder = new MethodMetadataBuilder( + declaredById, Modifier.PUBLIC, + new JavaSymbolName("getVersion"), JavaType.OBJECT, + invocableMemberBodyBuilder); + getIdMethodBuilder.addParameter( + StringUtils.uncapitalize(targetType.getSimpleTypeName()), + targetType); + return getIdMethodBuilder; + } + + public void notify(final String upstreamDependency, + String downstreamDependency) { + if (MetadataIdentificationUtils + .isIdentifyingClass(downstreamDependency)) { + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + upstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(PhysicalTypeIdentifier + .getMetadataIdentiferType())), + "Expected class-level notifications only for PhysicalTypeIdentifier (not '%s')", + upstreamDependency); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(upstreamDependency); + if (cid == null) { + return; + } + boolean processed = false; + if (cid.getAnnotation(RooJavaType.ROO_GWT_REQUEST) != null) { + final ClassOrInterfaceTypeDetails proxy = gwtTypeService + .lookupProxyFromRequest(cid); + if (proxy != null) { + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(proxy.getDeclaredByMetadataId()); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(proxy.getDeclaredByMetadataId()); + downstreamDependency = GwtLocatorMetadata.createIdentifier( + typeName, typePath); + processed = true; + } + } + if (!processed + && cid.getAnnotation(RooJavaType.ROO_GWT_PROXY) == null) { + boolean found = false; + for (final ClassOrInterfaceTypeDetails proxyCid : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_GWT_PROXY)) { + final AnnotationMetadata annotationMetadata = GwtUtils + .getFirstAnnotation(proxyCid, + GwtUtils.ROO_PROXY_REQUEST_ANNOTATIONS); + if (annotationMetadata != null) { + final AnnotationAttributeValue attributeValue = annotationMetadata + .getAttribute("value"); + if (attributeValue != null) { + final String mirrorName = GwtUtils + .getStringValue(attributeValue); + if (mirrorName != null + && cid.getName() + .getFullyQualifiedTypeName() + .equals(attributeValue.getValue())) { + found = true; + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(proxyCid + .getDeclaredByMetadataId()); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(proxyCid + .getDeclaredByMetadataId()); + downstreamDependency = GwtLocatorMetadata + .createIdentifier(typeName, typePath); + break; + } + } + } + } + if (!found) { + return; + } + } + else if (!processed) { + // A physical Java type has changed, and determine what the + // corresponding local metadata identification string would have + // been + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(upstreamDependency); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(upstreamDependency); + downstreamDependency = GwtLocatorMetadata.createIdentifier( + typeName, typePath); + } + + // We only need to proceed if the downstream dependency relationship + // is not already registered + // (if it's already registered, the event will be delivered directly + // later on) + if (metadataDependencyRegistry.getDownstream(upstreamDependency) + .contains(downstreamDependency)) { + return; + } + } + + // We should now have an instance-specific "downstream dependency" that + // can be processed by this class + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + downstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Unexpected downstream notification for '%s' to this provider (which uses '%s')", + downstreamDependency, getProvidesType()); + + metadataService.evictAndGet(downstreamDependency); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadata.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadata.java new file mode 100644 index 000000000..588485ea2 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadata.java @@ -0,0 +1,61 @@ +package org.springframework.roo.addon.gwt.proxy; + +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +public class GwtProxyMetadata extends AbstractMetadataItem { + + private static final String PROVIDES_TYPE_STRING = GwtProxyMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentifierType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final String proxyTypeContents; + + public GwtProxyMetadata(final String id, final String proxyTypeContents) { + super(id); + this.proxyTypeContents = proxyTypeContents; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final GwtProxyMetadata that = (GwtProxyMetadata) o; + return !(proxyTypeContents != null ? !proxyTypeContents + .equals(that.proxyTypeContents) + : that.proxyTypeContents != null); + } + + @Override + public int hashCode() { + return proxyTypeContents != null ? proxyTypeContents.hashCode() : 0; + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadataProvider.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadataProvider.java new file mode 100644 index 000000000..ba6eb22b4 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadataProvider.java @@ -0,0 +1,8 @@ +package org.springframework.roo.addon.gwt.proxy; + +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataProvider; + +public interface GwtProxyMetadataProvider extends MetadataProvider, + MetadataNotificationListener { +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadataProviderImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadataProviderImpl.java new file mode 100644 index 000000000..c4a08aeb7 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/proxy/GwtProxyMetadataProviderImpl.java @@ -0,0 +1,326 @@ +package org.springframework.roo.addon.gwt.proxy; + +import static org.springframework.roo.addon.gwt.GwtJavaType.ENTITY_PROXY; +import static org.springframework.roo.addon.gwt.GwtJavaType.OLD_ENTITY_PROXY; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.gwt.GwtFileManager; +import org.springframework.roo.addon.gwt.GwtTypeService; +import org.springframework.roo.addon.gwt.GwtUtils; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; + +@Component +@Service +public class GwtProxyMetadataProviderImpl extends + AbstractHashCodeTrackingMetadataNotifier implements + GwtProxyMetadataProvider { + + @Reference protected GwtFileManager gwtFileManager; + @Reference protected GwtTypeService gwtTypeService; + @Reference protected ProjectOperations projectOperations; + @Reference protected TypeLocationService typeLocationService; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + public MetadataItem get(final String metadataIdentificationString) { + final ClassOrInterfaceTypeDetails proxy = getGovernor(metadataIdentificationString); + if (proxy == null) { + return null; + } + + final AnnotationMetadata mirrorAnnotation = MemberFindingUtils + .getAnnotationOfType(proxy.getAnnotations(), + RooJavaType.ROO_GWT_PROXY); + if (mirrorAnnotation == null) { + return null; + } + + final JavaType mirroredType = GwtUtils.lookupProxyTargetType(proxy); + if (mirroredType == null) { + return null; + } + + final List exclusionList = new ArrayList(); + final AnnotationAttributeValue excludeAttribute = mirrorAnnotation + .getAttribute("exclude"); + if (excludeAttribute != null + && excludeAttribute instanceof ArrayAttributeValue) { + @SuppressWarnings("unchecked") + final ArrayAttributeValue excludeArrayAttribute = (ArrayAttributeValue) excludeAttribute; + for (final StringAttributeValue attributeValue : excludeArrayAttribute + .getValue()) { + exclusionList.add(attributeValue.getValue()); + } + } + else if (excludeAttribute != null + && excludeAttribute instanceof StringAttributeValue) { + final StringAttributeValue excludeStringAttribute = (StringAttributeValue) excludeAttribute; + exclusionList.add(excludeStringAttribute.getValue()); + } + + final List readOnlyList = new ArrayList(); + final AnnotationAttributeValue readOnlyAttribute = mirrorAnnotation + .getAttribute("readOnly"); + if (readOnlyAttribute != null + && readOnlyAttribute instanceof ArrayAttributeValue) { + @SuppressWarnings("unchecked") + final ArrayAttributeValue readOnlyArrayAttribute = (ArrayAttributeValue) readOnlyAttribute; + for (final StringAttributeValue attributeValue : readOnlyArrayAttribute + .getValue()) { + readOnlyList.add(attributeValue.getValue()); + } + } + else if (readOnlyAttribute != null + && readOnlyAttribute instanceof StringAttributeValue) { + final StringAttributeValue readOnlyStringAttribute = (StringAttributeValue) readOnlyAttribute; + readOnlyList.add(readOnlyStringAttribute.getValue()); + } + + final ClassOrInterfaceTypeDetails mirroredDetails = typeLocationService + .getTypeDetails(mirroredType); + if (mirroredDetails == null + || Modifier.isAbstract(mirroredDetails.getModifier())) { + return null; + } + + final String moduleName = PhysicalTypeIdentifier.getPath( + proxy.getDeclaredByMetadataId()).getModule(); + final List proxyMethods = gwtTypeService + .getProxyMethods(mirroredDetails); + final List convertedProxyMethods = new ArrayList(); + final Collection sourcePackages = gwtTypeService + .getSourcePackages(moduleName); + for (final MethodMetadata method : proxyMethods) { + final JavaType gwtType = gwtTypeService.getGwtSideLeafType( + method.getReturnType(), mirroredDetails.getName(), false, + true); + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + method); + // Remove all annotations from proxy method + methodBuilder.setAnnotations(new ArrayList()); + methodBuilder.setReturnType(gwtType); + final MethodMetadata convertedMethod = methodBuilder.build(); + if (gwtTypeService.isMethodReturnTypeInSourcePath(convertedMethod, + mirroredDetails, sourcePackages)) { + convertedProxyMethods.add(methodBuilder.build()); + } + } + final GwtProxyMetadata metadata = new GwtProxyMetadata( + metadataIdentificationString, updateProxy(proxy, + convertedProxyMethods, exclusionList, readOnlyList)); + notifyIfRequired(metadata); + return metadata; + } + + private ClassOrInterfaceTypeDetails getGovernor( + final String metadataIdentificationString) { + final JavaType governorTypeName = GwtProxyMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath governorTypePath = GwtProxyMetadata + .getPath(metadataIdentificationString); + + final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier( + governorTypeName, governorTypePath); + return typeLocationService.getTypeDetails(physicalTypeId); + } + + public String getProvidesType() { + return GwtProxyMetadata.getMetadataIdentifierType(); + } + + public void notify(final String upstreamDependency, + String downstreamDependency) { + if (MetadataIdentificationUtils + .isIdentifyingClass(downstreamDependency)) { + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + upstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(PhysicalTypeIdentifier + .getMetadataIdentiferType())), + "Expected class-level notifications only for PhysicalTypeIdentifier (not '%s')", + upstreamDependency); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(upstreamDependency); + if (cid == null) { + return; + } + if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), + RooJavaType.ROO_GWT_PROXY) == null) { + boolean found = false; + for (final ClassOrInterfaceTypeDetails proxyCid : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_GWT_PROXY)) { + final AnnotationMetadata annotationMetadata = GwtUtils + .getFirstAnnotation(proxyCid, + GwtUtils.ROO_PROXY_REQUEST_ANNOTATIONS); + if (annotationMetadata != null) { + final AnnotationAttributeValue attributeValue = annotationMetadata + .getAttribute("value"); + if (attributeValue != null) { + final String mirrorName = GwtUtils + .getStringValue(attributeValue); + if (mirrorName != null + && cid.getName() + .getFullyQualifiedTypeName() + .equals(attributeValue.getValue())) { + found = true; + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(proxyCid + .getDeclaredByMetadataId()); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(proxyCid + .getDeclaredByMetadataId()); + downstreamDependency = GwtProxyMetadata + .createIdentifier(typeName, typePath); + break; + } + } + } + } + if (!found) { + return; + } + } + else { + // A physical Java type has changed, and determine what the + // corresponding local metadata identification string would have + // been + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(upstreamDependency); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(upstreamDependency); + downstreamDependency = GwtProxyMetadata.createIdentifier( + typeName, typePath); + } + + // We only need to proceed if the downstream dependency relationship + // is not already registered + // (if it's already registered, the event will be delivered directly + // later on) + if (metadataDependencyRegistry.getDownstream(upstreamDependency) + .contains(downstreamDependency)) { + return; + } + } + + // We should now have an instance-specific "downstream dependency" that + // can be processed by this class + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + downstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Unexpected downstream notification for '%s' to this provider (which uses '%s')", + downstreamDependency, getProvidesType()); + + metadataService.evictAndGet(downstreamDependency); + } + + private String updateProxy(final ClassOrInterfaceTypeDetails proxy, + final List proxyMethods, + final List exclusionList, final List readOnlyList) { + // Create a new ClassOrInterfaceTypeDetailsBuilder for the Proxy, will + // be overridden if the Proxy has already been created + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + proxy); + + // Only inherit from EntityProxy if extension is not already defined + if (!cidBuilder.getExtendsTypes().contains(OLD_ENTITY_PROXY) + && !cidBuilder.getExtendsTypes().contains(ENTITY_PROXY)) { + cidBuilder.addExtendsTypes(ENTITY_PROXY); + } + + if (!cidBuilder.getExtendsTypes().contains(ENTITY_PROXY)) { + cidBuilder.addExtendsTypes(ENTITY_PROXY); + } + + final String destinationMetadataId = proxy.getDeclaredByMetadataId(); + final List methods = new ArrayList(); + for (final MethodMetadata method : proxyMethods) { + if (exclusionList.contains(method.getMethodName().getSymbolName())) { + continue; + } + final String propertyName = StringUtils.uncapitalize(BeanInfoUtils + .getPropertyNameForJavaBeanMethod(method).getSymbolName()); + if (exclusionList.contains(propertyName)) { + continue; + } + + final MethodMetadataBuilder abstractAccessorMethodBuilder = new MethodMetadataBuilder( + destinationMetadataId, method); + abstractAccessorMethodBuilder + .setBodyBuilder(new InvocableMemberBodyBuilder()); + abstractAccessorMethodBuilder.setModifier(Modifier.ABSTRACT); + methods.add(abstractAccessorMethodBuilder); + + if (readOnlyList.contains(propertyName)) { + continue; + } + final MethodMetadataBuilder abstractMutatorMethodBuilder = new MethodMetadataBuilder( + destinationMetadataId, method); + abstractMutatorMethodBuilder + .setBodyBuilder(new InvocableMemberBodyBuilder()); + abstractMutatorMethodBuilder.setModifier(Modifier.ABSTRACT); + abstractMutatorMethodBuilder.setReturnType(JavaType.VOID_PRIMITIVE); + abstractMutatorMethodBuilder + .setParameterTypes(AnnotatedJavaType + .convertFromJavaTypes(Arrays.asList(method + .getReturnType()))); + abstractMutatorMethodBuilder.setParameterNames(Arrays + .asList(new JavaSymbolName(StringUtils + .uncapitalize(propertyName)))); + abstractMutatorMethodBuilder.setMethodName(new JavaSymbolName( + method.getMethodName().getSymbolName() + .replaceFirst("get", "set"))); + methods.add(abstractMutatorMethodBuilder); + } + + cidBuilder.setDeclaredMethods(methods); + return gwtFileManager.write(cidBuilder.build(), + GwtUtils.PROXY_REQUEST_WARNING); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadata.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadata.java new file mode 100644 index 000000000..e9f065281 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadata.java @@ -0,0 +1,72 @@ +package org.springframework.roo.addon.gwt.request; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +public class GwtRequestMetadata extends AbstractMetadataItem { + + private static final String PROVIDES_TYPE_STRING = GwtRequestMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentifierType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final String requestTypeContents; + + /** + * Constructor + * + * @param id the id of this + * {@link org.springframework.roo.metadata.MetadataItem} + * @param requestTypeContents the Java source code for the entity-specific + * Request interface (required) + */ + public GwtRequestMetadata(final String id, final String requestTypeContents) { + super(id); + Validate.notBlank(requestTypeContents, "Invalid contents '%s'", + requestTypeContents); + this.requestTypeContents = requestTypeContents; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof GwtRequestMetadata)) { + return false; + } + final GwtRequestMetadata other = (GwtRequestMetadata) obj; + return StringUtils.equals(requestTypeContents, + other.requestTypeContents); + } + + @Override + public int hashCode() { + return requestTypeContents.hashCode(); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataNotificationListener.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataNotificationListener.java new file mode 100644 index 000000000..9e9f385f5 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataNotificationListener.java @@ -0,0 +1,150 @@ +package org.springframework.roo.addon.gwt.request; + +import static org.springframework.roo.model.RooJavaType.ROO_GWT_REQUEST; + +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.gwt.GwtTypeService; +import org.springframework.roo.addon.gwt.GwtUtils; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Triggers the generation of {@link GwtRequestMetadata} upon being notified of + * changes to {@link PhysicalTypeMetadata} within the user project. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class GwtRequestMetadataNotificationListener implements + MetadataNotificationListener { + + @Reference GwtTypeService gwtTypeService; + @Reference MetadataDependencyRegistry metadataDependencyRegistry; + @Reference MetadataService metadataService; + @Reference TypeLocationService typeLocationService; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.addNotificationListener(this); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.removeNotificationListener(this); + } + + private String getDownstreamInstanceId(final String upstreamDependency) { + final ClassOrInterfaceTypeDetails upstreamType = typeLocationService + .getTypeDetails(upstreamDependency); + if (upstreamType == null) { + return null; + } + + final String downstreamOfGwtEntityLayerComponent = getDownstreamOfGwtEntityLayerComponent(upstreamType); + if (downstreamOfGwtEntityLayerComponent != null) { + return downstreamOfGwtEntityLayerComponent; + } + + if (upstreamType.getAnnotation(ROO_GWT_REQUEST) == null) { + final String downstreamOfGwtEntity = getDownstreamOfGwtEntity(upstreamType + .getType()); + if (downstreamOfGwtEntity != null) { + return downstreamOfGwtEntity; + } + } + + return null; + } + + private String getDownstreamOfGwtEntity(final JavaType upstreamType) { + for (final ClassOrInterfaceTypeDetails request : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_GWT_REQUEST)) { + final AnnotationMetadata gwtRequestAnnotation = request + .getAnnotation(ROO_GWT_REQUEST); + if (gwtRequestAnnotation != null) { + final AnnotationAttributeValue attributeValue = gwtRequestAnnotation + .getAttribute("value"); + Validate.validState(attributeValue != null, + "The x annotation should have a '%s' attribute", + "value"); + final String entityClass = GwtUtils + .getStringValue(attributeValue); + if (upstreamType.getFullyQualifiedTypeName() + .equals(entityClass)) { + // The upstream type is an entity with an associated GWT + // request; make that request the downstream + return getLocalMid(request.getDeclaredByMetadataId()); + } + } + } + return null; + } + + private String getDownstreamOfGwtEntityLayerComponent( + final MemberHoldingTypeDetails upstreamType) { + final List layerEntities = upstreamType.getLayerEntities(); + if (!layerEntities.isEmpty()) { + // Look for a GWT request that manages one of these entities + for (final ClassOrInterfaceTypeDetails request : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_GWT_REQUEST)) { + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromRequest(request); + if (entity != null && layerEntities.contains(entity.getType())) { + // This layer component has an associated GWT request; make + // that request the downstream + return getLocalMid(request.getDeclaredByMetadataId()); + } + } + } + return null; + } + + private String getLocalMid(final String physicalTypeId) { + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(physicalTypeId); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(physicalTypeId); + return GwtRequestMetadata.createIdentifier(typeName, typePath); + } + + public void notify(final String upstreamMID, final String downstreamMID) { + if (!PhysicalTypeIdentifier.isValid(upstreamMID)) { + return; + } + + final String downstreamInstanceId; + if (MetadataIdentificationUtils.isIdentifyingInstance(downstreamMID)) { + downstreamInstanceId = downstreamMID; + } + else { + downstreamInstanceId = getDownstreamInstanceId(upstreamMID); + if (downstreamInstanceId == null) { + return; + } + } + + // We should now have an instance-specific "downstream dependency" that + // can be processed by this class + Validate.isTrue(PhysicalTypeIdentifierNamingUtils.isValid( + GwtRequestMetadata.class.getName(), downstreamInstanceId)); + metadataService.evictAndGet(downstreamInstanceId); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataProvider.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataProvider.java new file mode 100644 index 000000000..36eb12450 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataProvider.java @@ -0,0 +1,6 @@ +package org.springframework.roo.addon.gwt.request; + +import org.springframework.roo.metadata.MetadataProvider; + +public interface GwtRequestMetadataProvider extends MetadataProvider { +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataProviderImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataProviderImpl.java new file mode 100644 index 000000000..965d4a75f --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataProviderImpl.java @@ -0,0 +1,394 @@ +package org.springframework.roo.addon.gwt.request; + +import static java.lang.reflect.Modifier.ABSTRACT; +import static java.lang.reflect.Modifier.STATIC; +import static org.springframework.roo.addon.gwt.GwtJavaType.INSTANCE_REQUEST; +import static org.springframework.roo.addon.gwt.GwtJavaType.OLD_REQUEST_CONTEXT; +import static org.springframework.roo.addon.gwt.GwtJavaType.REQUEST; +import static org.springframework.roo.addon.gwt.GwtJavaType.REQUEST_CONTEXT; +import static org.springframework.roo.addon.gwt.GwtJavaType.SERVICE_NAME; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.LONG_PRIMITIVE; +import static org.springframework.roo.model.JavaType.VOID_PRIMITIVE; +import static org.springframework.roo.model.RooJavaType.ROO_GWT_REQUEST; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.gwt.GwtFileManager; +import org.springframework.roo.addon.gwt.GwtTypeService; +import org.springframework.roo.addon.gwt.GwtUtils; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; + +@Component +@Service +public class GwtRequestMetadataProviderImpl extends + AbstractHashCodeTrackingMetadataNotifier implements + GwtRequestMetadataProvider { + + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + + @Reference GwtFileManager gwtFileManager; + @Reference GwtTypeService gwtTypeService; + @Reference LayerService layerService; + @Reference MemberDetailsScanner memberDetailsScanner; + @Reference PersistenceMemberLocator persistenceMemberLocator; + @Reference ProjectOperations projectOperations; + @Reference TypeLocationService typeLocationService; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + public MetadataItem get(final String requestMetadataId) { + final ProjectMetadata projectMetadata = projectOperations + .getProjectMetadata(PhysicalTypeIdentifierNamingUtils + .getModule(requestMetadataId)); + if (projectMetadata == null) { + return null; + } + + final ClassOrInterfaceTypeDetails requestInterface = getGovernor(requestMetadataId); + if (requestInterface == null) { + return null; + } + + final AnnotationMetadata gwtRequestAnnotation = requestInterface + .getAnnotation(ROO_GWT_REQUEST); + if (gwtRequestAnnotation == null) { + return null; + } + + final JavaType entityType = new JavaType((String) gwtRequestAnnotation + .getAttribute("value").getValue()); + + // Get the methods to be invoked and the type(s) that provide them + // (should only be one such type, or null) + final Map requestMethods = getRequestMethodsAndInvokedTypes( + entityType, requestMetadataId); + if (requestMethods == null) { + return null; + } + + final JavaType invokedType = getInvokedType(requestMethods.values()); + final String requestTypeContents = writeRequestInterface( + requestInterface, invokedType, requestMethods.keySet(), + entityType, requestMetadataId); + final GwtRequestMetadata gwtRequestMetadata = new GwtRequestMetadata( + requestMetadataId, requestTypeContents); + notifyIfRequired(gwtRequestMetadata); + return gwtRequestMetadata; + } + + private ClassOrInterfaceTypeDetails getGovernor( + final String metadataIdentificationString) { + final JavaType governorTypeName = GwtRequestMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath governorTypePath = GwtRequestMetadata + .getPath(metadataIdentificationString); + final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier( + governorTypeName, governorTypePath); + return typeLocationService.getTypeDetails(physicalTypeId); + } + + /** + * Returns the type on which the given request methods will be invoked + * + * @param invokedFields the autowired fields invoked by layer method calls + * (can include null elements for 'active record' + * calls) + * @return null if active record is being used, otherwise a + * layer component type + */ + private JavaType getInvokedType( + final Collection invokedFields) { + final Collection distinctInvokedTypes = new HashSet(); + for (final FieldMetadata invokedField : invokedFields) { + if (invokedField == null) { + distinctInvokedTypes.add(null); + } + else { + distinctInvokedTypes.add(invokedField.getFieldType()); + } + } + Validate.isTrue(distinctInvokedTypes.size() == 1, + "Expected one invoked type but found: %s", distinctInvokedTypes); + return distinctInvokedTypes.iterator().next(); + } + + public String getProvidesType() { + return GwtRequestMetadata.getMetadataIdentifierType(); + } + + private MethodMetadataBuilder getRequestMethod( + final ClassOrInterfaceTypeDetails request, + final MethodMetadata method, final JavaType returnType) { + final ClassOrInterfaceTypeDetails entity = gwtTypeService + .lookupEntityFromRequest(request); + if (entity == null) { + return null; + } + final List parameterTypes = new ArrayList(); + for (final AnnotatedJavaType parameterType : method.getParameterTypes()) { + parameterTypes.add(new AnnotatedJavaType(gwtTypeService + .getGwtSideLeafType(parameterType.getJavaType(), + entity.getType(), true, false))); + } + return new MethodMetadataBuilder(request.getDeclaredByMetadataId(), + ABSTRACT, method.getMethodName(), returnType, parameterTypes, + method.getParameterNames(), null); + } + + private MethodMetadataBuilder getRequestMethod( + final ClassOrInterfaceTypeDetails request, + final MethodMetadata method, final JavaType entityType, + final JavaType invokedType) { + final ClassOrInterfaceTypeDetails proxy = gwtTypeService + .lookupProxyFromRequest(request); + if (proxy == null) { + return null; + } + final JavaType methodReturnType = getRequestMethodReturnType( + invokedType, method, proxy.getType()); + return getRequestMethod(request, method, methodReturnType); + } + + private MethodMetadata getRequestMethod(final JavaType entity, + final MethodMetadataCustomDataKey methodKey, + final MemberTypeAdditions memberTypeAdditions, + final String declaredByMetadataId) { + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + declaredByMetadataId); // wrong MID, but doesn't matter here + methodBuilder.setMethodName(new JavaSymbolName(memberTypeAdditions + .getMethodName())); + if (memberTypeAdditions.isStatic()) { + // OK to overwrite any other modifiers + methodBuilder.setModifier(STATIC); + } + /* + * TODO make sure the active record instance methods have the correct + * parameters + * + * expected: abstract + * InstanceRequest persist(); actual: abstract Request + * persist(ThingProxy proxy); + */ + for (final MethodParameter methodParameter : memberTypeAdditions + .getMethodParameters()) { + methodBuilder.addParameter(methodParameter.getValue() + .getSymbolName(), methodParameter.getKey()); + } + final JavaType returnType = getReturnType(methodKey, entity); + final JavaType gwtType = gwtTypeService.getGwtSideLeafType(returnType, + entity, true, true); + methodBuilder.setReturnType(gwtType); + return methodBuilder.build(); + } + + private JavaType getRequestMethodReturnType(final JavaType invokedType, + final MethodMetadata method, final JavaType proxyType) { + if (invokedType == null && !method.isStatic()) { + // Calling an active record method that's non-static (i.e. target is + // an entity instance) + final List methodReturnTypeArgs = Arrays.asList( + proxyType, method.getReturnType()); + return new JavaType(INSTANCE_REQUEST.getFullyQualifiedTypeName(), + 0, DataType.TYPE, null, methodReturnTypeArgs); + } + final List methodReturnTypeArgs = Collections + .singletonList(method.getReturnType()); + return new JavaType(REQUEST.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, methodReturnTypeArgs); + } + + private Map getRequestMethodsAndInvokedTypes( + final JavaType entity, final String requestMetadataId) { + final JavaType idType = persistenceMemberLocator + .getIdentifierType(entity); + if (idType == null) { + return null; + } + final Map requestMethods = new LinkedHashMap(); + for (final Entry> methodSignature : getRequestMethodSignatures( + entity, idType).entrySet()) { + final String methodId = methodSignature.getKey().name(); + final MemberTypeAdditions memberTypeAdditions = layerService + .getMemberTypeAdditions(requestMetadataId, methodId, + entity, idType, LAYER_POSITION, + methodSignature.getValue()); + Validate.notNull(memberTypeAdditions, + "No support for %s method for domain type %s", methodId, + entity); + final MethodMetadata requestMethod = getRequestMethod(entity, + methodSignature.getKey(), memberTypeAdditions, + requestMetadataId); + requestMethods.put(requestMethod, + memberTypeAdditions.getInvokedField()); + } + return requestMethods; + } + + private Map> getRequestMethodSignatures( + final JavaType domainType, final JavaType idType) { + final Map> signatures = new LinkedHashMap>(); + final List noArgs = Arrays.asList(); + signatures.put(COUNT_ALL_METHOD, noArgs); + signatures.put(FIND_ALL_METHOD, noArgs); + signatures.put(FIND_ENTRIES_METHOD, Arrays.asList(new MethodParameter( + INT_PRIMITIVE, "firstResult"), new MethodParameter( + INT_PRIMITIVE, "maxResults"))); + signatures.put(FIND_METHOD, + Arrays.asList(new MethodParameter(idType, "id"))); + final List proxyParameterAsList = Arrays + .asList(new MethodParameter(domainType, "proxy")); + signatures.put(PERSIST_METHOD, proxyParameterAsList); + signatures.put(REMOVE_METHOD, proxyParameterAsList); + return signatures; + } + + private JavaType getReturnType(final MethodMetadataCustomDataKey methodKey, + final JavaType entity) { + if (COUNT_ALL_METHOD.equals(methodKey)) { + return LONG_PRIMITIVE; + } + if (FIND_ALL_METHOD.equals(methodKey) + || FIND_ENTRIES_METHOD.equals(methodKey)) { + return JavaType.listOf(entity); + } + if (FIND_METHOD.equals(methodKey)) { + return entity; + } + if (PERSIST_METHOD.equals(methodKey) || REMOVE_METHOD.equals(methodKey)) { + return VOID_PRIMITIVE; + } + throw new IllegalStateException("Unexpected method key " + methodKey); + } + + private AnnotationMetadata getServiceNameAnnotation( + final ClassOrInterfaceTypeDetails request, + final JavaType invokedType, final JavaType entityType, + final String requestMetadataId) { + final List> serviceAttributeValues = new ArrayList>(); + if (invokedType == null) { + // Active record; specify the entity type as the invoked "service" + final StringAttributeValue stringAttributeValue = new StringAttributeValue( + new JavaSymbolName("value"), + entityType.getFullyQualifiedTypeName()); + serviceAttributeValues.add(stringAttributeValue); + } + else { + // Layer component, e.g. repository or service; specify its type as + // the invoked "service" + final StringAttributeValue stringAttributeValue = new StringAttributeValue( + new JavaSymbolName("value"), + invokedType.getFullyQualifiedTypeName()); + serviceAttributeValues.add(stringAttributeValue); + + // Specify the locator that GWT will use to find it + final LogicalPath requestLogicalPath = PhysicalTypeIdentifier + .getPath(request.getDeclaredByMetadataId()); + final JavaType serviceLocator = gwtTypeService + .getServiceLocator(requestLogicalPath.getModule()); + final StringAttributeValue locatorAttributeValue = new StringAttributeValue( + new JavaSymbolName("locator"), + serviceLocator.getFullyQualifiedTypeName()); + serviceAttributeValues.add(locatorAttributeValue); + } + return new AnnotationMetadataBuilder(SERVICE_NAME, + serviceAttributeValues).build(); + } + + /** + * Creates or updates the entity-specific request interface with + * + * @param request + * @param requestMethods the methods to declare in the interface, mapped to + * the injected field type on which they are invoked (required) + * @param entityType + * @param requestMetadataId + * @return the Java source code for the request interface + */ + private String writeRequestInterface( + final ClassOrInterfaceTypeDetails request, + final JavaType invokedType, + final Iterable requestMethods, + final JavaType entityType, final String requestMetadataId) { + final ClassOrInterfaceTypeDetailsBuilder typeDetailsBuilder = new ClassOrInterfaceTypeDetailsBuilder( + request); + + // Service name annotation (@RooGwtRequest was already applied by + // GwtOperationsImpl#createRequestInterface) + typeDetailsBuilder.removeAnnotation(SERVICE_NAME); + typeDetailsBuilder.addAnnotation(getServiceNameAnnotation(request, + invokedType, entityType, requestMetadataId)); + + // Super-interface + typeDetailsBuilder.removeExtendsTypes(OLD_REQUEST_CONTEXT); + if (!typeDetailsBuilder.getExtendsTypes().contains(REQUEST_CONTEXT)) { + typeDetailsBuilder.addExtendsTypes(REQUEST_CONTEXT); + } + + typeDetailsBuilder.clearDeclaredMethods(); + for (final MethodMetadata method : requestMethods) { + typeDetailsBuilder.addMethod(getRequestMethod(request, method, + entityType, invokedType)); + } + + return gwtFileManager.write(typeDetailsBuilder.build(), + GwtUtils.PROXY_REQUEST_WARNING); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadata.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadata.java new file mode 100644 index 000000000..54a2ca280 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadata.java @@ -0,0 +1,48 @@ +package org.springframework.roo.addon.gwt.scaffold; + +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for GWT. + * + * @author Ben Alex + * @author Alan Stewart + * @author Ray Cromwell + * @author Amit Manjhi + * @since 1.1 + */ +public class GwtScaffoldMetadata extends AbstractMetadataItem { + + private static final String PROVIDES_TYPE_STRING = GwtScaffoldMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentifierType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public GwtScaffoldMetadata(final String id) { + super(id); + } +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadataProvider.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadataProvider.java new file mode 100644 index 000000000..e528e5bf1 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadataProvider.java @@ -0,0 +1,14 @@ +package org.springframework.roo.addon.gwt.scaffold; + +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataProvider; + +/** + * Interface for {@link GwtScaffoldMetadataProviderImpl}. + * + * @author James Tyrrell + * @since 1.1.2 + */ +public interface GwtScaffoldMetadataProvider extends MetadataProvider, + MetadataNotificationListener { +} diff --git a/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadataProviderImpl.java b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadataProviderImpl.java new file mode 100644 index 000000000..a19516922 --- /dev/null +++ b/addon-gwt/src/main/java/org/springframework/roo/addon/gwt/scaffold/GwtScaffoldMetadataProviderImpl.java @@ -0,0 +1,471 @@ +package org.springframework.roo.addon.gwt.scaffold; + +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; + +import java.io.File; +import java.io.FileReader; +import java.io.StringWriter; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.Transformer; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.gwt.GwtFileManager; +import org.springframework.roo.addon.gwt.GwtPath; +import org.springframework.roo.addon.gwt.GwtProxyProperty; +import org.springframework.roo.addon.gwt.GwtTemplateDataHolder; +import org.springframework.roo.addon.gwt.GwtTemplateService; +import org.springframework.roo.addon.gwt.GwtType; +import org.springframework.roo.addon.gwt.GwtTypeService; +import org.springframework.roo.addon.gwt.GwtUtils; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +/** + * Monitors Java types and if necessary creates/updates/deletes the GWT files + * maintained for each mirror-compatible object. You can find a list of + * mirror-compatible objects in + * {@link org.springframework.roo.addon.gwt.GwtType}. + *

    + *

    + * For now only @RooJpaEntity instances will be mirror-compatible. + *

    + *

    + * Like all Roo add-ons, this provider aims to expose potentially-useful + * contents of the above files via {@link GwtScaffoldMetadata}. It also attempts + * to avoiding writing to disk unless actually necessary. + *

    + *

    + * A separate type monitors the creation/deletion of the aforementioned files to + * maintain "global indexes". + * + * @author Ben Alex + * @author Alan Stewart + * @author Ray Cromwell + * @author Amit Manjhi + * @since 1.1 + */ +@Component +@Service +public class GwtScaffoldMetadataProviderImpl implements + GwtScaffoldMetadataProvider { + + @Reference protected GwtFileManager gwtFileManager; + @Reference protected GwtTemplateService gwtTemplateService; + @Reference protected GwtTypeService gwtTypeService; + @Reference protected MetadataDependencyRegistry metadataDependencyRegistry; + @Reference protected MetadataService metadataService; + @Reference protected ProjectOperations projectOperations; + @Reference protected TypeLocationService typeLocationService; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + private void buildType(final GwtType type, final String moduleName) { + gwtTypeService.buildType(type, gwtTemplateService + .getStaticTemplateTypeDetails(type, moduleName), moduleName); + } + + private String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return GwtScaffoldMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + @Override + public MetadataItem get(final String metadataIdentificationString) { + // Obtain the governor's information + final ClassOrInterfaceTypeDetails mirroredType = getGovernor(metadataIdentificationString); + if (mirroredType == null + || Modifier.isAbstract(mirroredType.getModifier())) { + return null; + } + + final ClassOrInterfaceTypeDetails proxy = gwtTypeService + .lookupProxyFromEntity(mirroredType); + if (proxy == null || proxy.getDeclaredMethods().isEmpty()) { + return null; + } + + final ClassOrInterfaceTypeDetails request = gwtTypeService + .lookupRequestFromEntity(mirroredType); + if (request == null) { + return null; + } + + if (!GwtUtils.getBooleanAnnotationValue(proxy, + RooJavaType.ROO_GWT_PROXY, "scaffold", false)) { + return null; + } + + final String moduleName = PhysicalTypeIdentifier.getPath( + proxy.getDeclaredByMetadataId()).getModule(); + + final JavaPackage topLevelPackage = projectOperations + .getTopLevelPackage(moduleName); + + cleanUpLegacyProjects(GwtType.LIST_PLACE_RENDERER, topLevelPackage, + "ApplicationListPlaceRenderer"); + + buildType(GwtType.APP_ENTITY_TYPES_PROCESSOR, moduleName); + buildType(GwtType.APP_REQUEST_FACTORY, moduleName); + buildType(GwtType.LIST_PLACE_RENDERER, moduleName); + buildType(GwtType.MASTER_ACTIVITIES, moduleName); + buildType(GwtType.LIST_PLACE_RENDERER, moduleName); + buildType(GwtType.DETAILS_ACTIVITIES, moduleName); + buildType(GwtType.MOBILE_ACTIVITIES, moduleName); + + final GwtScaffoldMetadata gwtScaffoldMetadata = new GwtScaffoldMetadata( + metadataIdentificationString); + + final Map clientSideTypeMap = new LinkedHashMap(); + for (final MethodMetadata proxyMethod : proxy.getDeclaredMethods()) { + if (!proxyMethod.getMethodName().getSymbolName().startsWith("get")) { + continue; + } + final JavaSymbolName propertyName = new JavaSymbolName( + StringUtils.uncapitalize(BeanInfoUtils + .getPropertyNameForJavaBeanMethod(proxyMethod) + .getSymbolName())); + final JavaType propertyType = proxyMethod.getReturnType(); + ClassOrInterfaceTypeDetails ptmd = typeLocationService + .getTypeDetails(propertyType); + if (propertyType.isCommonCollectionType() + && !propertyType.getParameters().isEmpty()) { + ptmd = typeLocationService.getTypeDetails(propertyType + .getParameters().get(0)); + } + + final FieldMetadata field = proxy.getDeclaredField(propertyName); + final List annotations = field != null ? field + .getAnnotations() : Collections + . emptyList(); + + final GwtProxyProperty gwtProxyProperty = new GwtProxyProperty( + topLevelPackage, ptmd, propertyType, + propertyName.getSymbolName(), annotations, proxyMethod + .getMethodName().getSymbolName()); + clientSideTypeMap.put(propertyName, gwtProxyProperty); + } + + final GwtTemplateDataHolder templateDataHolder = gwtTemplateService + .getMirrorTemplateTypeDetails(mirroredType, clientSideTypeMap, + moduleName); + final Map> typesToBeWritten = new LinkedHashMap>(); + final Map xmlToBeWritten = new LinkedHashMap(); + + final Map mirrorTypeMap = GwtUtils.getMirrorTypeMap( + mirroredType.getName(), topLevelPackage); + mirrorTypeMap.put(GwtType.PROXY, proxy.getName()); + mirrorTypeMap.put(GwtType.REQUEST, request.getName()); + + for (final Map.Entry entry : mirrorTypeMap + .entrySet()) { + final GwtType gwtType = entry.getKey(); + final JavaType javaType = entry.getValue(); + + if (!gwtType.isMirrorType() || gwtType.equals(GwtType.PROXY) + || gwtType.equals(GwtType.REQUEST)) { + continue; + } + + cleanUpLegacyProjects(gwtType, topLevelPackage, + javaType.getSimpleTypeName()); + + gwtType.dynamicallyResolveFieldsToWatch(clientSideTypeMap); + gwtType.dynamicallyResolveMethodsToWatch(proxy.getName(), + clientSideTypeMap, topLevelPackage); + + final List extendsTypes = gwtTypeService + .getExtendsTypes(templateDataHolder + .getTemplateTypeDetailsMap().get(gwtType)); + typesToBeWritten.put(gwtType, gwtTypeService + .buildType(gwtType, templateDataHolder + .getTemplateTypeDetailsMap().get(gwtType), + extendsTypes, moduleName)); + + if (gwtType.isCreateUiXml()) { + final GwtPath gwtPath = gwtType.getPath(); + final PathResolver pathResolver = projectOperations + .getPathResolver(); + final String webappPath = pathResolver.getIdentifier( + LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, + moduleName), moduleName); + final String packagePath = pathResolver + .getIdentifier(LogicalPath.getInstance( + Path.SRC_MAIN_JAVA, moduleName), gwtPath + .getPackagePath(topLevelPackage)); + + final String targetDirectory = gwtPath == GwtPath.WEB ? webappPath + : packagePath; + final String destFile = targetDirectory + File.separatorChar + + javaType.getSimpleTypeName() + ".ui.xml"; + final String contents = gwtTemplateService.buildUiXml( + templateDataHolder.getXmlTemplates().get(gwtType), + destFile, + new ArrayList(proxy + .getDeclaredMethods())); + xmlToBeWritten.put(destFile, contents); + } + } + + // Our general strategy is to instantiate GwtScaffoldMetadata, which + // offers a conceptual representation of what should go into the 4 + // key-specific types; after that we do comparisons and write to disk if + // needed + for (final Map.Entry> entry : typesToBeWritten + .entrySet()) { + gwtFileManager.write(typesToBeWritten.get(entry.getKey()), entry + .getKey().isOverwriteConcrete()); + } + for (final ClassOrInterfaceTypeDetails type : templateDataHolder + .getTypeList()) { + gwtFileManager.write(type, false); + } + for (final Map.Entry entry : xmlToBeWritten.entrySet()) { + gwtFileManager.write(entry.getKey(), entry.getValue()); + } + for (final Map.Entry entry : templateDataHolder + .getXmlMap().entrySet()) { + gwtFileManager.write(entry.getKey(), entry.getValue()); + } + + return gwtScaffoldMetadata; + } + + private ClassOrInterfaceTypeDetails getGovernor( + final String metadataIdentificationString) { + final JavaType governorTypeName = GwtScaffoldMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath governorTypePath = GwtScaffoldMetadata + .getPath(metadataIdentificationString); + + final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier( + governorTypeName, governorTypePath); + return typeLocationService.getTypeDetails(physicalTypeId); + } + + @Override + public String getProvidesType() { + return GwtScaffoldMetadata.getMetadataIdentifierType(); + } + + @Override + public void notify(String upstreamDependency, String downstreamDependency) { + if (MetadataIdentificationUtils + .isIdentifyingClass(downstreamDependency)) { + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + upstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(PhysicalTypeIdentifier + .getMetadataIdentiferType())), + "Expected class-level notifications only for PhysicalTypeIdentifier (not '%s')", + upstreamDependency); + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(upstreamDependency); + if (cid == null) { + return; + } + + if (cid.getAnnotation(RooJavaType.ROO_GWT_PROXY) != null) { + final ClassOrInterfaceTypeDetails entityType = gwtTypeService + .lookupEntityFromProxy(cid); + if (entityType != null) { + upstreamDependency = entityType.getDeclaredByMetadataId(); + } + } + else if (cid.getAnnotation(RooJavaType.ROO_GWT_REQUEST) != null) { + final ClassOrInterfaceTypeDetails entityType = gwtTypeService + .lookupEntityFromRequest(cid); + if (entityType != null) { + upstreamDependency = entityType.getDeclaredByMetadataId(); + } + } + else if (cid.getAnnotation(RooJavaType.ROO_GWT_LOCATOR) != null) { + final ClassOrInterfaceTypeDetails entityType = gwtTypeService + .lookupEntityFromLocator(cid); + if (entityType != null) { + upstreamDependency = entityType.getDeclaredByMetadataId(); + } + } + + // A physical Java type has changed, and determine what the + // corresponding local metadata identification string would have + // been + final JavaType typeName = PhysicalTypeIdentifier + .getJavaType(upstreamDependency); + final LogicalPath typePath = PhysicalTypeIdentifier + .getPath(upstreamDependency); + downstreamDependency = createLocalIdentifier(typeName, typePath); + } + + // We only need to proceed if the downstream dependency relationship is + // not already registered + // (if it's already registered, the event will be delivered directly + // later on) + if (metadataDependencyRegistry.getDownstream(upstreamDependency) + .contains(downstreamDependency)) { + return; + } + + // We should now have an instance-specific "downstream dependency" that + // can be processed by this class + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + downstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Unexpected downstream notification for '%s' to this provider (which uses '%s')", + downstreamDependency, getProvidesType()); + + metadataService.evictAndGet(downstreamDependency); + } + + private void cleanUpLegacyProjects(GwtType type, + JavaPackage topLevelPackage, String simpleTypeName) { + if (type.isCreateUiXml() || type == GwtType.MOBILE_LIST_VIEW + || type == GwtType.LIST_PLACE_RENDERER + || type == GwtType.LIST_PLACE_RENDERER) { + String legacySimpleTypeName = simpleTypeName.replace("Desktop", ""); + + String legacyClassName = GwtPath.MANAGED_UI + .packageName(topLevelPackage) + "." + legacySimpleTypeName; + + ClassOrInterfaceTypeDetails legacyView = typeLocationService + .getTypeDetails(new JavaType(legacyClassName)); + + if (legacyView != null + && legacyView.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS) { + String newClassName = type.getPath().packageName( + topLevelPackage) + + "." + simpleTypeName; + + JavaType newClass = new JavaType(newClassName); + + final String focusedModule = projectOperations + .getFocusedModuleName(); + final LogicalPath logicalPath = LogicalPath.getInstance( + SRC_MAIN_JAVA, focusedModule); + + ClassOrInterfaceTypeDetailsBuilder builder = new ClassOrInterfaceTypeDetailsBuilder( + PhysicalTypeIdentifier.createIdentifier(newClass, + logicalPath), legacyView); + builder.setName(newClass); + + ClassOrInterfaceTypeDetails newView = builder.build(); + + gwtFileManager.delete(legacyView); + gwtFileManager.write(newView, false); + + } + + String legacyManagedClassName = GwtPath.MANAGED_UI + .packageName(topLevelPackage) + + "." + + legacySimpleTypeName + + "_Roo_Gwt"; + + ClassOrInterfaceTypeDetails oldManagedDetailsView = typeLocationService + .getTypeDetails(new JavaType(legacyManagedClassName)); + + if (oldManagedDetailsView != null) { + gwtFileManager.delete(oldManagedDetailsView); + } + + if (type.isCreateUiXml()) { + String moduleName = projectOperations.getFocusedModuleName(); + final GwtPath targetPath = type.getPath(); + final GwtPath sourcePath = GwtPath.MANAGED_UI; + final PathResolver pathResolver = projectOperations + .getPathResolver(); + final String sourceDirectory = pathResolver + .getIdentifier(LogicalPath.getInstance( + Path.SRC_MAIN_JAVA, moduleName), sourcePath + .getPackagePath(topLevelPackage)); + final String targetDirectory = pathResolver + .getIdentifier(LogicalPath.getInstance( + Path.SRC_MAIN_JAVA, moduleName), targetPath + .getPackagePath(topLevelPackage)); + + final String sourceFile = sourceDirectory + File.separatorChar + + legacySimpleTypeName + ".ui.xml"; + final String destFile = targetDirectory + File.separatorChar + + simpleTypeName + ".ui.xml"; + + if (gwtFileManager.fileExists(sourceFile)) { + FileReader fileReader; + try { + fileReader = new FileReader(sourceFile); + final DocumentBuilder docBuilder = XmlUtils + .getDocumentBuilder(); + InputSource source = new InputSource(); + + source.setCharacterStream(fileReader); + final Document existingDocument = docBuilder + .parse(source); + existingDocument.setDocumentURI(destFile); + final Transformer transformer = XmlUtils + .createIndentingTransformer(); + final DOMSource domSource = new DOMSource( + existingDocument); + final StreamResult result = new StreamResult( + new StringWriter()); + transformer.transform(domSource, result); + String contents = result.getWriter().toString(); + gwtFileManager.write(destFile, contents); + } + catch (Exception e) { + System.out.println(e.getMessage()); + } + + gwtFileManager.delete(sourceFile); + } + } + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/applicationContext-locators-template.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/applicationContext-locators-template.xml new file mode 100644 index 000000000..026d2466e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/applicationContext-locators-template.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/configuration.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/configuration.xml new file mode 100644 index 000000000..fd8e27c53 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/configuration.xml @@ -0,0 +1,197 @@ + + + + + 2.5.0 + + + + maven.springframework.org.external + SpringSource Maven Repository - External Releases + http://maven.springframework.org/external + + + + + com.google.gwt + gwt-servlet + ${gwt.version} + + + com.google.gwt + gwt-user + ${gwt.version} + provided + + + org.json + json + 20090211 + + + com.google.gwt.inject + gin + 1.5.0 + + + javax.validation + validation-api + 1.0.0.GA + + + javax.validation + validation-api + 1.0.0.GA + sources + + + xalan + xalan + 2.7.1 + + + + + org.codehaus.mojo + gwt-maven-plugin + 2.5.0 + + + com.google.gwt + gwt-dev + ${gwt.version} + + + com.google.gwt + gwt-user + ${gwt.version} + + + + INFO + + /ApplicationScaffold.html + ${project.build.directory}/${project.build.finalName} + + ${project.groupId}.ApplicationScaffold + + true + + + + gwtcompile + prepare-package + + compile + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2 + + + process-classes + + VerifyRequestFactoryInterfaces + java + + -cp + + com.google.web.bindery.requestfactory.apt.ValidationTool + ${project.build.outputDirectory} + ${project.groupId}.client.managed.request.ApplicationRequestFactory + + + + exec + + + + + + + + + 2.5.0 + 1.7.4 + + + + com.google.appengine + appengine-api-1.0-sdk + ${gae.version} + + + + + org.codehaus.mojo + gwt-maven-plugin + 2.5.0 + + + com.google.gwt + gwt-dev + ${gwt.version} + + + com.google.gwt + gwt-user + ${gwt.version} + + + + INFO + + 2.5.0 + /ApplicationScaffold.html + ${project.build.directory}/${project.build.finalName} + + ${project.groupId}.ApplicationScaffold + + com.google.appengine.tools.development.gwt.AppEngineLauncher + "-javaagent:${gae.home}/lib/agent/appengine-agent.jar" -Xmx1024m + true + ${gae.version} + + + + gwtcompile + prepare-package + + compile + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2 + + + process-classes + + VerifyRequestFactoryInterfaces + java + + -cp + + com.google.web.bindery.requestfactory.apt.ValidationTool + ${project.build.outputDirectory} + ${project.groupId}.client.managed.request.ApplicationRequestFactory + + + + exec + + + + + + + \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/ApplicationScaffold-template.gwt.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/ApplicationScaffold-template.gwt.xml new file mode 100644 index 000000000..45094938b --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/ApplicationScaffold-template.gwt.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = 0) { + var mobile = args.substring(startMobile); + var begin = mobile.indexOf("=") + 1; + var end = mobile.indexOf("&"); + if (end == -1) { + end = mobile.length; + } + isMobile = mobile.substring(begin, end); + } + } + + if (isMobile){ + return "mobilesafari"; + } + + return "none"; + ]]> + + + + + + + + + + + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/Scaffold-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/Scaffold-template.java new file mode 100644 index 000000000..2be2c3e50 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/Scaffold-template.java @@ -0,0 +1,18 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ioc.DesktopInjectorWrapper; +import __TOP_LEVEL_PACKAGE__.client.scaffold.ioc.InjectorWrapper; +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.core.client.GWT; + +/** + * Application for browsing entities. + */ +public class Scaffold implements EntryPoint { + final private InjectorWrapper injectorWrapper = GWT.create(DesktopInjectorWrapper.class); + + public void onModuleLoad() { + /* Get and run platform specific app */ + injectorWrapper.getInjector().getScaffoldApp().run(); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldApp-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldApp-template.java new file mode 100644 index 000000000..bc8264e05 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldApp-template.java @@ -0,0 +1,32 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationEntityTypesProcessor; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyListPlace; +import __TOP_LEVEL_PACKAGE__.client.scaffold.gae.*; +import com.google.web.bindery.requestfactory.shared.EntityProxy; + +import java.util.HashSet; +import java.util.Set; + +public class ScaffoldApp { + + static boolean isMobile = false; + + public static boolean isMobile() { + return isMobile; + } + + public void run() { + } + + protected HashSet getTopPlaces() { + Set> types = ApplicationEntityTypesProcessor.getAll(); + HashSet rtn = new HashSet(types.size()); + + for (Class type : types) { + rtn.add(new ProxyListPlace(type)); + } + + return rtn; + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopApp-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopApp-template.java new file mode 100644 index 000000000..bab3df207 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopApp-template.java @@ -0,0 +1,120 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.managed.activity.*; +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationRequestFactory; +import __TOP_LEVEL_PACKAGE__.client.scaffold.gae.GaeHelper; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.*; +import __TOP_LEVEL_PACKAGE__.client.scaffold.request.RequestEvent; +import com.google.gwt.activity.shared.*; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.logging.client.LogConfiguration; +import com.google.gwt.place.shared.*; +import com.google.web.bindery.requestfactory.shared.LoggingRequest; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.HasConstrainedValue; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.google.inject.Inject; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryLogHandler; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; +__GAE_IMPORT__ + +/** + * Application for browsing entities. + */ +public class ScaffoldDesktopApp extends ScaffoldApp { + private static final Logger LOGGER = Logger.getLogger(Scaffold.class.getName()); + private final ScaffoldDesktopShell shell; + private final ApplicationRequestFactory requestFactory; + private final EventBus eventBus; + private final PlaceController placeController; + private final PlaceHistoryFactory placeHistoryFactory; + private final ApplicationMasterActivities applicationMasterActivities; + private final ApplicationDetailsActivities applicationDetailsActivities; + + @Inject + public ScaffoldDesktopApp(ScaffoldDesktopShell shell, ApplicationRequestFactory requestFactory, EventBus eventBus, PlaceController placeController, PlaceHistoryFactory placeHistoryFactory, ApplicationMasterActivities applicationMasterActivities, ApplicationDetailsActivities applicationDetailsActivities, GaeHelper gaeHelper) { + this.shell = shell; + this.requestFactory = requestFactory; + this.eventBus = eventBus; + this.placeController = placeController; + this.placeHistoryFactory = placeHistoryFactory; + this.applicationMasterActivities = applicationMasterActivities; + this.applicationDetailsActivities = applicationDetailsActivities; + } + + public void run() { + /* Add handlers, setup activities */ + init(); + + /* Hide the loading message */ + Element loading = Document.get().getElementById("loading"); + loading.getParentElement().removeChild(loading); + + /* And show the user the shell */ + RootLayoutPanel.get().add(shell); + } + + private void init() { + GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() { + public void onUncaughtException(Throwable e) { + Window.alert("Error: " + e.getMessage()); + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + }); + + if (LogConfiguration.loggingIsEnabled()) { + // Add remote logging handler + RequestFactoryLogHandler.LoggingRequestProvider provider = new RequestFactoryLogHandler.LoggingRequestProvider() { + public LoggingRequest getLoggingRequest() { + return requestFactory.loggingRequest(); + } + }; + Logger.getLogger("").addHandler(new RequestFactoryLogHandler(provider, Level.WARNING, new ArrayList())); + } + + RequestEvent.register(eventBus, new RequestEvent.Handler() { + // Only show loading status if a request isn't serviced in 250ms. + private static final int LOADING_TIMEOUT = 250; + + public void onRequestEvent(RequestEvent requestEvent) { + if (requestEvent.getState() == RequestEvent.State.SENT) { + shell.getMole().showDelayed(LOADING_TIMEOUT); + } else { + shell.getMole().hide(); + } + } + }); + + CachingActivityMapper cached = new CachingActivityMapper(applicationMasterActivities); + ProxyPlaceToListPlace proxyPlaceToListPlace = new ProxyPlaceToListPlace(); + ActivityMapper masterActivityMap = new FilteredActivityMapper(proxyPlaceToListPlace, cached); + final ActivityManager masterActivityManager = new ActivityManager(masterActivityMap, eventBus); + + masterActivityManager.setDisplay(shell.getMasterPanel()); + + ProxyListPlacePicker proxyListPlacePicker = new ProxyListPlacePicker(placeController, proxyPlaceToListPlace); + HasConstrainedValue listPlacePickerView = shell.getPlacesBox(); + listPlacePickerView.setAcceptableValues(getTopPlaces()); + proxyListPlacePicker.register(eventBus, listPlacePickerView); + + final ActivityManager detailsActivityManager = new ActivityManager(applicationDetailsActivities, eventBus); + + detailsActivityManager.setDisplay(shell.getDetailsPanel()); + + /* Browser history integration */ + ScaffoldPlaceHistoryMapper mapper = GWT.create(ScaffoldPlaceHistoryMapper.class); + mapper.setFactory(placeHistoryFactory); + PlaceHistoryHandler placeHistoryHandler = new PlaceHistoryHandler(mapper); + if (getTopPlaces().iterator().hasNext()) { + ProxyListPlace defaultPlace = getTopPlaces().iterator().next(); + placeHistoryHandler.register(placeController, eventBus, defaultPlace); + placeHistoryHandler.handleCurrentHistory(); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopShell-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopShell-template.java new file mode 100644 index 000000000..f0b87b6cd --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopShell-template.java @@ -0,0 +1,75 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.managed.ui.renderer.ApplicationListPlaceRenderer; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyListPlace; +import __TOP_LEVEL_PACKAGE__.client.scaffold.ui.LoginWidget; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.*; + +/** + * The outermost UI of the application. + */ +public class ScaffoldDesktopShell extends Composite { + + interface Binder extends UiBinder { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + @UiField SimplePanel details; + @UiField DivElement error; + @UiField LoginWidget loginWidget; + @UiField SimplePanel master; + @UiField NotificationMole mole; + @UiField(provided = true) + ValuePicker placesBox = new ValuePicker(new ApplicationListPlaceRenderer()); + + public ScaffoldDesktopShell() { + initWidget(BINDER.createAndBindUi(this)); + } + + /** + * @return the panel to hold the details + */ + public SimplePanel getDetailsPanel() { + return details; + } + + /** + * @return the login widget + */ + public LoginWidget getLoginWidget() { + return loginWidget; + } + + /** + * @return the panel to hold the master list + */ + public SimplePanel getMasterPanel() { + return master; + } + + /** + * @return the notification mole for loading feedback + */ + public NotificationMole getMole() { + return mole; + } + + /** + * @return the navigator + */ + public HasConstrainedValue getPlacesBox() { + return placesBox; + } + + /** + * @param string + */ + public void setError(String string) { + error.setInnerText(string); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopShell-template.ui.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopShell-template.ui.xml new file mode 100644 index 000000000..5b78193e2 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldDesktopShell-template.ui.xml @@ -0,0 +1,160 @@ + + + + + + + @def contentWidth 850px; + + .disabled { + color: gray; + } + + .body { + overflow: auto; + } + + .banner { + background-color: #777; + -moz-border-radius-topleft: 10px; + -webkit-border-top-left-radius: 10px; + -moz-border-radius-topright: 10px; + -webkit-border-top-right-radius: 10px; + margin-top: 1.5em; + height: 4em; + } + + .title { + color: white; + padding: 1em; + position: absolute; + color: #def; + } + + .title h2 { + margin: 0; + } + + .error { + position: absolute; + left: 12%; + right: 12%; + text-align: center; + background-color: red; + } + + .login { + position: absolute; + left: 75%; + right: 0%; + text-align: center; + color: #def; + } + + .users { + position: absolute; + right: 0; + } + + .centered { + width: contentWidth; + margin-right: auto; + margin-left: auto; + } + + .content { + position: relative; + border: 1px solid #ddf; + overflow-y: auto; + overflow-x: hidden; + -moz-border-radius-bottomleft: 10px; + -webkit-border-bottom-left-radius: 10px; + -moz-border-radius-bottomright: 10px; + -webkit-border-bottom-right-radius: 10px; + } + + .entities { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 11em; + } + + .entitiesList { + border-right: 1px solid #ddf; + height: 100%; + outline: none; + } + + .entitiesList > div > div { + padding-left: 1em; + padding-top: 5px; + padding-bottom: 5px; + } + + .entityDetails { + margin-left: 11em; + } + + @sprite .gwtLogo { + gwt-image: 'gwtLogo'; + float: right; + } + + @sprite .rooLogo { + gwt-image: 'rooLogo'; + float: right; + } + + .logos { + color: #aaa; + font-size: 0.8em; + width: 160px; + margin-left: auto; + margin-right: auto; + text-align: right; + } + + + + + +

    +
    + +

    Data Browser

    +
    + +
    + + + + +
    + Powered by: + +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileApp-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileApp-template.java new file mode 100644 index 000000000..0b94d6b2d --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileApp-template.java @@ -0,0 +1,246 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.managed.activity.*; +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationRequestFactory; +import __TOP_LEVEL_PACKAGE__.client.managed.ui.renderer.ApplicationListPlaceRenderer; +import __TOP_LEVEL_PACKAGE__.client.scaffold.activity.IsScaffoldMobileActivity; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.*; +import __TOP_LEVEL_PACKAGE__.client.scaffold.gae.GaeHelper; +import __TOP_LEVEL_PACKAGE__.client.style.MobileListResources; +import com.google.gwt.activity.shared.*; +import com.google.gwt.cell.client.AbstractCell; +import com.google.gwt.cell.client.Cell; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.logging.client.LogConfiguration; +import com.google.gwt.place.shared.*; +import com.google.web.bindery.requestfactory.shared.LoggingRequest; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.text.shared.Renderer; +import com.google.gwt.user.cellview.client.CellList; +import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy; +import com.google.gwt.user.client.ui.*; +import com.google.inject.Inject; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryLogHandler; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; +__GAE_IMPORT__ + +/** + * Mobile application for browsing entities. + */ +public class ScaffoldMobileApp extends ScaffoldApp { + + + private static final Logger log = Logger.getLogger(Scaffold.class.getName()); + public static final Place ROOT_PLACE = new Place() {}; + + /** + * The root activity that shows all entities. + */ + private static class DefaultActivity extends AbstractActivity implements IsScaffoldMobileActivity { + private final Widget widget; + + public DefaultActivity(Widget widget) { + this.widget = widget; + } + + @Override + public void start(AcceptsOneWidget panel, EventBus eventBus) { + panel.setWidget(widget); + } + + public Place getBackButtonPlace() { + return null; + } + + public String getBackButtonText() { + return null; + } + + public Place getEditButtonPlace() { + return null; + } + + public String getTitleText() { + return "All Entities"; + } + + public boolean hasEditButton() { + return false; + } + } + + private static MobileListResources res = GWT.create(MobileListResources.class); + + /** + * Get the list resources used for mobile. + */ + public static MobileListResources getMobileListResources() { + if (res == null) { + res = GWT.create(MobileListResources.class); + res.cellListStyle().ensureInjected(); + } + return res; + } + + private IsScaffoldMobileActivity lastActivity; + + private final ScaffoldMobileShell shell; + private final ScaffoldMobileActivities scaffoldMobileActivities; + private final ApplicationRequestFactory requestFactory; + private final EventBus eventBus; + private final PlaceController placeController; + private final PlaceHistoryFactory placeHistoryFactory; + + @Inject + public ScaffoldMobileApp(ScaffoldMobileShell shell, ApplicationRequestFactory requestFactory, EventBus eventBus, PlaceController placeController, ScaffoldMobileActivities scaffoldMobileActivities, PlaceHistoryFactory placeHistoryFactory, GaeHelper gaeHelper) { + this.shell = shell; + this.requestFactory = requestFactory; + this.eventBus = eventBus; + this.placeController = placeController; + this.scaffoldMobileActivities = scaffoldMobileActivities; + this.placeHistoryFactory = placeHistoryFactory; + } + + @Override + public void run() { + isMobile = true; + + /* Add handlers, setup activities */ + init(); + + /* Hide the loading message */ + Element loading = Document.get().getElementById("loading"); + loading.getParentElement().removeChild(loading); + + /* And show the user the shell */ + // TODO (jlabanca): Use RootLayoutPanel when we switch to DockLayoutPanel. + RootPanel.get().add(shell); + } + + private void init() { + GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() { + public void onUncaughtException(Throwable e) { + log.log(Level.SEVERE, e.getMessage(), e); + } + }); + + if (LogConfiguration.loggingIsEnabled()) { + /* Add remote logging handler */ + RequestFactoryLogHandler.LoggingRequestProvider provider = new RequestFactoryLogHandler.LoggingRequestProvider() { + public LoggingRequest getLoggingRequest() { + return requestFactory.loggingRequest(); + } + }; + Logger.getLogger("").addHandler(new RequestFactoryLogHandler(provider, Level.WARNING, new ArrayList())); + } + + /* Left side lets us pick from all the types of entities */ + + final Renderer placePickerRenderer = new ApplicationListPlaceRenderer(); + Cell placePickerCell = new AbstractCell() { + @Override + public void render(Context context, ProxyListPlace value, SafeHtmlBuilder sb) { + sb.appendEscaped(placePickerRenderer.render(value)); + } + }; + CellList placePickerList = new CellList(placePickerCell, getMobileListResources()); + placePickerList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED); + final ValuePicker placePickerView = new ValuePicker(placePickerList); + Activity defaultActivity = new DefaultActivity(placePickerView); + ProxyPlaceToListPlace proxyPlaceToListPlace = new ProxyPlaceToListPlace(); + ProxyListPlacePicker proxyListPlacePicker = new ProxyListPlacePicker(placeController, proxyPlaceToListPlace); + placePickerView.setAcceptableValues(getTopPlaces()); + proxyListPlacePicker.register(eventBus, placePickerView); + + /* + * Wrap the scaffoldMobileActivities so we can intercept activity requests + * and remember the last activity (for back button support). + */ + + scaffoldMobileActivities.setRootActivity(defaultActivity); + ActivityMapper activityMapper = new ActivityMapper() { + public Activity getActivity(Place place) { + // Defer to scaffoldMobileActivities. + Activity nextActivity = scaffoldMobileActivities.getActivity(place); + + // Clear the value of the placePicker so we can select a new top level + // value. + placePickerView.setValue(null, false); + + // Update the title, back and edit buttons. + Button backButton = shell.getBackButton(); + if (nextActivity instanceof IsScaffoldMobileActivity) { + lastActivity = (IsScaffoldMobileActivity) nextActivity; + + // Update the title. + shell.setTitleText(lastActivity.getTitleText()); + + // Update the back button. + String backButtonText = lastActivity.getBackButtonText(); + if (backButtonText == null || backButtonText.length() == 0) { + shell.setBackButtonVisible(false); + } else { + shell.setBackButtonVisible(true); + backButton.setText(backButtonText); + } + + // Update the edit button. + shell.setEditButtonVisible(lastActivity.hasEditButton()); + } else { + lastActivity = null; + shell.setTitleText(""); + shell.setBackButtonVisible(false); + shell.setEditButtonVisible(false); + } + + // Return the activity. + return nextActivity; + } + }; + + /* + * The body is run by an ActivityManager that listens for PlaceChange events + * and finds the corresponding Activity to run + */ + + final ActivityManager activityManager = new ActivityManager(activityMapper, eventBus); + + activityManager.setDisplay(shell.getBody()); + + /* Browser history integration */ + ScaffoldPlaceHistoryMapper mapper = GWT.create(ScaffoldPlaceHistoryMapper.class); + mapper.setFactory(placeHistoryFactory); + PlaceHistoryHandler placeHistoryHandler = new PlaceHistoryHandler(mapper); + placeHistoryHandler.register(placeController, eventBus, ROOT_PLACE); + placeHistoryHandler.handleCurrentHistory(); + + shell.getBackButton().addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + if (lastActivity != null) { + Place backPlace = lastActivity.getBackButtonPlace(); + if (backPlace != null) { + placeController.goTo(backPlace); + } + } + } + }); + shell.getEditButton().addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + if (lastActivity != null) { + Place editPlace = lastActivity.getEditButtonPlace(); + if (editPlace != null) { + placeController.goTo(editPlace); + } + } + } + }); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileShell-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileShell-template.java new file mode 100644 index 000000000..c52e77d36 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileShell-template.java @@ -0,0 +1,88 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ui.LoginWidget; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; + +/** + * Top level UI for the mobile version of the application. + */ +public class ScaffoldMobileShell extends Composite { + + interface Binder extends UiBinder { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + @UiField Button backButton; + @UiField Element backButtonWrapper; + @UiField SimplePanel body; + @UiField Button editButton; + @UiField LoginWidget loginWidget; + @UiField Element title; + + public ScaffoldMobileShell() { + initWidget(BINDER.createAndBindUi(this)); + } + + /** + * @return the back button + */ + public Button getBackButton() { + return backButton; + } + + /** + * @return the body + */ + public SimplePanel getBody() { + return body; + } + + /** + * @return the edit button + */ + public Button getEditButton() { + return editButton; + } + + /** + * @return the login widget + */ + public LoginWidget getLoginWidget() { + return loginWidget; + } + + /** + * Show or hide the back button. + * + * @param visible true to show the button, false to hide + */ + public void setBackButtonVisible(boolean visible) { + setVisible(backButtonWrapper, visible); + } + + /** + * Show or hide the edit button. + * + * @param visible true to show the button, false to hide + */ + public void setEditButtonVisible(boolean visible) { + editButton.setVisible(visible); + } + + /** + * Set the title of the app. + * + * @param text the title to display at the top of the app + */ + public void setTitleText(String text) { + title.setInnerText(text); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileShell-template.ui.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileShell-template.ui.xml new file mode 100644 index 000000000..05a533c2a --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ScaffoldMobileShell-template.ui.xml @@ -0,0 +1,140 @@ + + + + + + + + @sprite .titlebar { + gwt-image: 'titleGradient'; + border-bottom: 1px solid #a0abbf; + height: 44px; + } + + .titlebarLayout { + height: 100%; + width: 100%; + } + + .title { + color: #444; + font-size: 12pt; + font-weight: bold; + text-shadow: #ddd 1px 1px 1px; + } + + .backButtonCell { + width: 50px; + padding-left: 5px; + } + + .editButtonCell { + width: 50px; + padding-right: 5px; + } + + .button { + color: #4d657f; + font-size: 9pt; + font-weight: bold; + border: 1px solid #aebbdd; + padding: 4px 6px; + background: #ecf1fd; + height: 27px; + } + + .backButton { + border-left: none; + padding-left: 2px; + margin-left: 0px; + } + + @sprite .backButtonImage { + gwt-image: 'backButtonImage'; + } + + .login { + height: 32px; + color: #222; + text-align: center; + background: white; + padding: 2px 0px; + border-top: 1px solid #a0abbf; + } + + + + + + + +

    + + + + + +
    + + + + + +
    +
    +
    + Back + +
    +
    + All Entities + + Edit + +
    + + + + + + + + + + + + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/activity/IsScaffoldMobileActivity-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/activity/IsScaffoldMobileActivity-template.java new file mode 100644 index 000000000..4b9c39e83 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/activity/IsScaffoldMobileActivity-template.java @@ -0,0 +1,33 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.activity; + +import com.google.gwt.place.shared.Place; + +/** + * Implemented by mobile activities. + */ +public interface IsScaffoldMobileActivity { + /** + * @return the Place to go when the back button is pressed + */ + Place getBackButtonPlace(); + + /** + * @return the text to display in the back button. + */ + String getBackButtonText(); + + /** + * @return the Place to go when the edit button is pressed + */ + Place getEditButtonPlace(); + + /** + * @return the title of the activity + */ + String getTitleText(); + + /** + * @return true if the activity has an edit button, false if not + */ + boolean hasEditButton(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeAuthRequestTransport-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeAuthRequestTransport-template.java new file mode 100644 index 000000000..93787f162 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeAuthRequestTransport-template.java @@ -0,0 +1,78 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.gae.GaeAuthenticationFailureEvent; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.Response; +import com.google.web.bindery.requestfactory.gwt.client.DefaultRequestTransport; +import com.google.web.bindery.requestfactory.shared.ServerFailure; +import com.google.gwt.user.client.Window; + +/** + * Extends DefaultRequestTransport to handle the authentication failures + * reported by {@link com.google.gwt.sample.gaerequest.server.GaeAuthFilter} + */ +public class GaeAuthRequestTransport extends DefaultRequestTransport { + private final EventBus eventBus; + + public GaeAuthRequestTransport(EventBus eventBus) { + this.eventBus = eventBus; + } + + @Override + protected RequestCallback createRequestCallback( + final TransportReceiver receiver) { + final RequestCallback superCallback = super.createRequestCallback(receiver); + + return new RequestCallback() { + public void onResponseReceived(Request request, Response response) { + /* + * The GaeAuthFailure filter responds with Response.SC_UNAUTHORIZED and + * adds a "login" url header if the user is not logged in. When we + * receive that combo, post an event so that the app can handle things + * as it sees fit. + */ + + int statusCode = response.getStatusCode(); + if (Response.SC_UNAUTHORIZED == statusCode) { + String loginUrl = response.getHeader("login"); + if (loginUrl != null) { + /* + * Hand the receiver a non-fatal callback, so that + * com.google.web.bindery.requestfactory.shared.Receiver will not post a + * runtime exception. + */ + receiver.onTransportFailure(new ServerFailure("Unauthenticated user", null, null, false /* not fatal */)); + eventBus.fireEvent(new GaeAuthenticationFailureEvent(loginUrl)); + return; + } + } + if (statusCode == 0) { + /* + * A response with no status follows the SC_UNAUTHORIZED. + * Report it as non-fatal, so that + * com.google.web.bindery.requestfactory.shared.Receiver will not post a + * runtime exception + */ + receiver.onTransportFailure(new ServerFailure("Status zero response, probably after auth failure", null, null, false /* not fatal */)); + return; + } + superCallback.onResponseReceived(request, response); + } + + public void onError(Request request, Throwable exception) { + superCallback.onError(request, exception); + } + }; + } + + @Override + protected RequestBuilder createRequestBuilder() { + RequestBuilder builder = super.createRequestBuilder(); + // GaeAuthFilter uses this to construct login url + builder.setHeader("requestUrl", Window.Location.getHref()); + return builder; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeAuthenticationFailureEvent-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeAuthenticationFailureEvent-template.java new file mode 100644 index 000000000..896a73684 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeAuthenticationFailureEvent-template.java @@ -0,0 +1,73 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.http.client.Response; + +/** + * An event posted when an authentication failure is detected. + */ +public class GaeAuthenticationFailureEvent extends GwtEvent { + + /** + * Implemented by handlers of this type of event. + */ + public interface Handler extends EventHandler { + /** + * Called when a {@link GaeAuthenticationFailureEvent} is fired. + * + * @param requestEvent a {@link GaeAuthenticationFailureEvent} instance + */ + void onAuthFailure(GaeAuthenticationFailureEvent requestEvent); + } + + private static final Type TYPE = new Type(); + + /** + * Register a {@link GaeAuthenticationFailureEvent.Handler} on an {@link EventBus}. + * + * @param eventBus the {@link EventBus} + * @param handler a {@link GaeAuthenticationFailureEvent.Handler} + * @return a {@link HandlerRegistration} instance + */ + public static HandlerRegistration register(EventBus eventBus, GaeAuthenticationFailureEvent.Handler handler) { + return eventBus.addHandler(TYPE, handler); + } + + /** + * Will only be non-null if this is an event of type {@link State#RECEIVED}, + * and the RPC was successful. + */ + private final String loginUrl; + + /** + * Constructs a new @{link RequestEvent}. + * + * @param state a {@link State} instance + * @param response a {@link Response} instance + */ + public GaeAuthenticationFailureEvent(String loginUrl) { + this.loginUrl = loginUrl; + } + + @Override + public GwtEvent.Type getAssociatedType() { + return TYPE; + } + + /** + * Returns the URL the user can visit to reauthenticate. + * + * @return a {@link Response} instance + */ + public String getLoginUrl() { + return loginUrl; + } + + @Override + protected void dispatch(Handler handler) { + handler.onAuthFailure(this); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeHelper-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeHelper-template.java new file mode 100644 index 000000000..6e14de63f --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeHelper-template.java @@ -0,0 +1,15 @@ +// WARNING: DO NOT EDIT THIS FILE. THIS FILE IS MANAGED BY SPRING ROO. +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.gwt.event.shared.EventBus; +import com.google.inject.Inject; +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationRequestFactory; +import __TOP_LEVEL_PACKAGE__.client.scaffold.ScaffoldDesktopShell; +__GAE_IMPORT__ +public class GaeHelper { + + @Inject + public GaeHelper(ScaffoldDesktopShell shell, ApplicationRequestFactory requestFactory, EventBus eventBus) { + __GAE_HOOKUP__ + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeLoginWidgetDriver-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeLoginWidgetDriver-template.java new file mode 100644 index 000000000..ffe2de271 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/GaeLoginWidgetDriver-template.java @@ -0,0 +1,39 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ui.LoginWidget; +import __TOP_LEVEL_PACKAGE__.shared.gae.GaeUser; +import __TOP_LEVEL_PACKAGE__.shared.gae.GaeUserServiceRequest; +import __TOP_LEVEL_PACKAGE__.shared.gae.MakesGaeRequests; +import com.google.web.bindery.requestfactory.shared.Receiver; +import com.google.gwt.user.client.Window.Location; + +/** + * Makes GAE requests to drive a LoginWidget. + */ +public class GaeLoginWidgetDriver { + private final MakesGaeRequests requests; + + public GaeLoginWidgetDriver(MakesGaeRequests requests) { + this.requests = requests; + } + + public void setWidget(final LoginWidget widget) { + GaeUserServiceRequest request = requests.userServiceRequest(); + request.createLogoutURL(Location.getHref()).to(new Receiver() { + public void onSuccess(String response) { + widget.setLogoutUrl(response); + } + }); + + request.getCurrentUser().to(new Receiver() { + @Override + public void onSuccess(GaeUser response) { + if (response != null) { + widget.setUserName(response.getNickname()); + } + } + }); + + request.fire(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/ReloadOnAuthenticationFailure-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/ReloadOnAuthenticationFailure-template.java new file mode 100644 index 000000000..61bab5627 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/gae/ReloadOnAuthenticationFailure-template.java @@ -0,0 +1,19 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Window.Location; + +/** + * A minimal auth failure handler which takes the user a login page. + */ +public class ReloadOnAuthenticationFailure implements GaeAuthenticationFailureEvent.Handler { + + public HandlerRegistration register(EventBus eventBus) { + return GaeAuthenticationFailureEvent.register(eventBus, this); + } + + public void onAuthFailure(GaeAuthenticationFailureEvent requestEvent) { + Location.replace(requestEvent.getLoginUrl()); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/DesktopInjector-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/DesktopInjector-template.java new file mode 100644 index 000000000..bc3438468 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/DesktopInjector-template.java @@ -0,0 +1,10 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ScaffoldDesktopApp; +import com.google.gwt.inject.client.GinModules; + +@GinModules(value = {ScaffoldModule.class}) +public interface DesktopInjector extends ScaffoldInjector { + + ScaffoldDesktopApp getScaffoldApp(); +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/DesktopInjectorWrapper-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/DesktopInjectorWrapper-template.java new file mode 100644 index 000000000..6b5a6cb2e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/DesktopInjectorWrapper-template.java @@ -0,0 +1,10 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +import com.google.gwt.core.client.GWT; + +public class DesktopInjectorWrapper implements InjectorWrapper { + + public ScaffoldInjector getInjector() { + return GWT.create(DesktopInjector.class); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/InjectorWrapper-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/InjectorWrapper-template.java new file mode 100644 index 000000000..fa6107587 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/InjectorWrapper-template.java @@ -0,0 +1,6 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +public interface InjectorWrapper { + + ScaffoldInjector getInjector(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/MobileInjector-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/MobileInjector-template.java new file mode 100644 index 000000000..a9f1866ce --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/MobileInjector-template.java @@ -0,0 +1,10 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ScaffoldMobileApp; +import com.google.gwt.inject.client.GinModules; + +@GinModules(value = {ScaffoldModule.class}) +public interface MobileInjector extends ScaffoldInjector { + + ScaffoldMobileApp getScaffoldApp(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/MobileInjectorWrapper-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/MobileInjectorWrapper-template.java new file mode 100644 index 000000000..765e481d9 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/MobileInjectorWrapper-template.java @@ -0,0 +1,11 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +import com.google.gwt.core.client.GWT; + +public class MobileInjectorWrapper implements InjectorWrapper { + + @Override + public ScaffoldInjector getInjector() { + return GWT.create(MobileInjector.class); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/ScaffoldInjector-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/ScaffoldInjector-template.java new file mode 100644 index 000000000..a16970686 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/ScaffoldInjector-template.java @@ -0,0 +1,9 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ScaffoldApp; +import com.google.gwt.inject.client.Ginjector; + +public interface ScaffoldInjector extends Ginjector { + + ScaffoldApp getScaffoldApp(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/ScaffoldModule-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/ScaffoldModule-template.java new file mode 100644 index 000000000..25363dec6 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ioc/ScaffoldModule-template.java @@ -0,0 +1,50 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ioc; + +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationRequestFactory; +import __TOP_LEVEL_PACKAGE__.client.scaffold.request.EventSourceRequestTransport; +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.SimpleEventBus; +import com.google.gwt.inject.client.AbstractGinModule; +import com.google.gwt.place.shared.PlaceController; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +__GAE_IMPORT__ + +public class ScaffoldModule extends AbstractGinModule { + + @Override + protected void configure() { + bind(EventBus.class).to(SimpleEventBus.class).in(Singleton.class); + bind(ApplicationRequestFactory.class).toProvider(RequestFactoryProvider.class).in(Singleton.class); + bind(PlaceController.class).toProvider(PlaceControllerProvider.class).in(Singleton.class); + } + + static class PlaceControllerProvider implements Provider { + private final PlaceController placeController; + + @Inject + public PlaceControllerProvider(EventBus eventBus) { + this.placeController = new PlaceController(eventBus); + } + + public PlaceController get() { + return placeController; + } + } + + static class RequestFactoryProvider implements Provider { + private final ApplicationRequestFactory requestFactory; + + @Inject + public RequestFactoryProvider(EventBus eventBus) { + requestFactory = GWT.create(ApplicationRequestFactory.class); + requestFactory.initialize(eventBus, new EventSourceRequestTransport(eventBus__GAE_REQUEST_TRANSPORT__)); + } + + public ApplicationRequestFactory get() { + return requestFactory; + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyEditActivity-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyEditActivity-template.java new file mode 100644 index 000000000..04fc34348 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyEditActivity-template.java @@ -0,0 +1,240 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import javax.validation.ConstraintViolation; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.place.shared.PlaceController; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.web.bindery.requestfactory.shared.*; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationRequestFactory; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.AbstractProxyEditActivity; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyEditView; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyListPlace; + +import java.util.Set; + +/** + * Abstract activity for editing a record. Subclasses must provide access to the + * request that will be fired when Save is clicked. + *

    + * Instances are not reusable. Once an activity is stopped, it cannot be + * restarted. + * + * @param

    the type of Proxy being edited + */ +public abstract class AbstractProxyEditActivity

    implements Activity, ProxyEditView.Delegate { + protected final PlaceController placeController; + protected final ApplicationRequestFactory factory; + protected final EntityProxyId

    proxyId; + + protected RequestFactoryEditorDriver editorDriver; + protected P proxy; + protected AcceptsOneWidget display; + protected EventBus eventBus; + + private boolean waiting; + + public AbstractProxyEditActivity(EntityProxyId

    proxyId, ApplicationRequestFactory factory, PlaceController placeController) { + this.factory = factory; + this.proxyId = proxyId; + this.placeController = placeController; + } + + protected abstract ProxyEditView getView(); + + protected abstract P createProxy(); + + /** + * Called once to create the appropriate request to save changes. + * + * @return the request context to fire when the save button is clicked + */ + protected abstract RequestContext createSaveRequest(P proxy); + + /** + * Get the proxy to be edited. Must be mutable, typically via a call to + * {@link RequestContext#edit(EntityProxy)}, or + * {@link RequestContext#create(Class)}. + */ + protected P getProxy() { + return proxy; + } + + @Override + public void start(AcceptsOneWidget display, EventBus eventBus) { + this.display = display; + this.eventBus = eventBus; + if (proxyId != null) { + createFindRequest().fire(new Receiver

    () { + + @Override + public void onSuccess(P response) { + AbstractProxyEditActivity.this.proxy = response; + bindToView(); + } + }); + } + else { + this.proxy = createProxy(); + bindToView(); + } + } + + protected Request

    createFindRequest() { + return this.factory.find(this.proxyId).with(getView().createEditorDriver().getPaths()); + } + + public void cancelClicked() { + String unsavedChangesWarning = mayStop(); + if ((unsavedChangesWarning == null) || Window.confirm(unsavedChangesWarning)) { + this.editorDriver = null; + exit(false); + } + } + + public String mayStop() { + if (isWaiting() || changed()) { + return "Are you sure you want to abandon your changes?"; + } + + return null; + } + + public void onCancel() { + onStop(); + } + + public void onStop() { + this.editorDriver = null; + } + + public void saveClicked() { + if (!changed()) { + return; + } + + RequestContext request = this.editorDriver.flush(); + if (this.editorDriver.hasErrors()) { + return; + } + + setWaiting(true); + request.fire(new Receiver() { + /* + * Callbacks do nothing if editorDriver is null, we were stopped in + * midflight + */ + @Override + public void onFailure(ServerFailure error) { + if (AbstractProxyEditActivity.this.editorDriver != null) { + setWaiting(false); + super.onFailure(error); + } + } + + @Override + public void onSuccess(Void ignore) { + executePostSaveActions(); + } + + @Override + public void onConstraintViolation(Set> violations) { + if (AbstractProxyEditActivity.this.editorDriver != null) { + setWaiting(false); + AbstractProxyEditActivity.this.editorDriver.setConstraintViolations(violations); + } + } + }); + } + + protected void executePostSaveActions() + { + if (this.editorDriver != null) { + // We want no warnings from mayStop, so: + + // Defeat isChanged check + this.editorDriver = null; + + // Defeat call-in-flight check + setWaiting(false); + + exit(true); + } + } + + public void bindToView() { + this.editorDriver = getView().createEditorDriver(); + executeBeforeBind(); + this.editorDriver.edit(getProxy(), createSaveRequest(getProxy())); + this.editorDriver.flush(); + executeAfterBind(); + this.display.setWidget(getView()); + } + + + /** + * Overridable method to perform actions before editorDriver.edit is called. + * By default it is empty. + */ + protected void executeBeforeBind() { + + } + + /** + * Overridable method to perform actions after editorDriver.edit is called. + * By default it is empty. + */ + protected void executeAfterBind() { + + } + + /** + * Overridable method to perform actions after the view is made visible. + * By default it is empty. + */ + protected void executeDisplaySet() { + + } + + @SuppressWarnings("unchecked") + // id type always matches proxy type + protected EntityProxyId

    getProxyId() { + return (EntityProxyId

    ) getProxy().stableId(); + } + + protected boolean changed() { + return this.editorDriver != null && this.editorDriver.flush().isChanged(); + } + + /** + * Called when the user cancels or has successfully saved. This default + * implementation tells the {@link PlaceController} to show the details of + * the edited record. + * + * @param saved + * true if changes were comitted, false if user canceled + */ + protected void exit(boolean saved) { + this.placeController.goTo(new ProxyListPlace(getProxyId().getProxyClass())); + } + + /** + * @return true if we're waiting for an rpc response. + */ + protected boolean isWaiting() { + return this.waiting; + } + + /** + * While we are waiting for a response, we cannot poke setters on the proxy + * (that is, we cannot call editorDriver.flush). So we set the waiting flag + * to warn ourselves not to, and to disable the view. + */ + protected void setWaiting(boolean wait) { + this.waiting = wait; + getView().setEnabled(!wait); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyListActivity-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyListActivity-template.java new file mode 100644 index 000000000..03bc6c3e7 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyListActivity-template.java @@ -0,0 +1,310 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceChangeEvent; +import com.google.gwt.place.shared.PlaceController; +import com.google.web.bindery.requestfactory.shared.*; +import com.google.gwt.user.cellview.client.AbstractHasData; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.gwt.view.client.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Abstract activity for displaying a list of {@link EntityProxy}. These + * activities are not re-usable. Once they are stopped, they cannot be + * restarted. + *

    + * Subclasses must: + *

    + *

      + *
    • provide a {@link ProxyListView} + *
    • implement method to request a full count + *
    • implement method to find a range of entities + *
    • respond to "show details" commands + *
    + *

    + * Only the properties required by the view will be requested. + * + * @param

    the type of {@link EntityProxy} listed + */ +public abstract class AbstractProxyListActivity

    implements Activity, ProxyListView.Delegate

    { + + /** + * This mapping allows us to update individual rows as records change. + */ + private final Map, Integer> idToRow = new HashMap, Integer>(); + private final Map, P> idToProxy = new HashMap, P>(); + + private final PlaceController placeController; + private final SingleSelectionModel

    selectionModel; + private final Class

    proxyClass; + + private HandlerRegistration rangeChangeHandler; + private ProxyListView

    view; + private AcceptsOneWidget display; + private EntityProxyId

    pendingSelection; + + public AbstractProxyListActivity(PlaceController placeController, ProxyListView

    view, Class

    proxyType) { + this.view = view; + this.placeController = placeController; + this.proxyClass = proxyType; + view.setDelegate(this); + + final HasData

    hasData = view.asHasData(); + rangeChangeHandler = hasData.addRangeChangeHandler(new RangeChangeEvent.Handler() { + public void onRangeChange(RangeChangeEvent event) { + AbstractProxyListActivity.this.onRangeChanged(hasData); + } + }); + + // Inherit the view's key provider + ProvidesKey

    keyProvider = ((AbstractHasData

    ) hasData).getKeyProvider(); + selectionModel = new SingleSelectionModel

    (keyProvider); + hasData.setSelectionModel(selectionModel); + + selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + public void onSelectionChange(SelectionChangeEvent event) { + P selectedObject = selectionModel.getSelectedObject(); + if (selectedObject != null) { + showDetails(selectedObject); + } + } + }); + } + + public void createClicked() { + placeController.goTo(new ProxyPlace(proxyClass)); + } + + public ProxyListView

    getView() { + return view; + } + + public String mayStop() { + return null; + } + + public void onCancel() { + onStop(); + } + + /** + * Called by the table as it needs data. + */ + public void onRangeChanged(HasData

    listView) { + final Range range = listView.getVisibleRange(); + + final Receiver> callback = new Receiver>() { + @Override + public void onSuccess(List

    values) { + if (view == null) { + // This activity is dead + return; + } + idToRow.clear(); + idToProxy.clear(); + for (int i = 0, row = range.getStart(); i < values.size(); i++, row++) { + P proxy = values.get(i); + @SuppressWarnings("unchecked") + // Why is this cast needed? + EntityProxyId

    proxyId = (EntityProxyId

    ) proxy.stableId(); + idToRow.put(proxyId, row); + idToProxy.put(proxyId, proxy); + } + getView().asHasData().setRowData(range.getStart(), values); + finishPendingSelection(); + if (display != null) { + display.setWidget(getView()); + } + } + }; + + fireRangeRequest(range, callback); + } + + public void onStop() { + view.setDelegate(null); + view = null; + rangeChangeHandler.removeHandler(); + rangeChangeHandler = null; + } + + /** + * Select the given record, or clear the selection if called with null or an + * id we don't know. + */ + public void select(EntityProxyId

    proxyId) { + /* + * The selectionModel will not flash if we put it back to the same state it + * is already in, so we can keep this code simple. + */ + + // Clear the selection + P selected = selectionModel.getSelectedObject(); + if (selected != null) { + selectionModel.setSelected(selected, false); + } + + // Select the new proxy, if it's relevant + if (proxyId != null) { + P selectMe = idToProxy.get(proxyId); + if (selectMe != null) { + pendingSelection = null; + selectionModel.setSelected(selectMe, true); + } else { + /* + * It may be that an async request is about to fetch it. + * Make note to select it when it arrives (see + * finishPendingSelection()). + */ + pendingSelection = proxyId; + } + } + } + + public void start(AcceptsOneWidget display, EventBus eventBus) { + view.setDelegate(this); + EntityProxyChange.registerForProxyType(eventBus, proxyClass, + new EntityProxyChange.Handler

    () { + public void onProxyChange(EntityProxyChange

    event) { + update(event.getWriteOperation(), event.getProxyId()); + } + }); + eventBus.addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() { + public void onPlaceChange(PlaceChangeEvent event) { + updateSelection(event.getNewPlace()); + } + }); + this.display = display; + init(); + updateSelection(placeController.getWhere()); + } + + public void update(WriteOperation writeOperation, EntityProxyId

    proxyId) { + switch (writeOperation) { + case UPDATE: + update(proxyId); + break; + case DELETE: + init(); + break; + case PERSIST: + /* + * On create, we presume the new record is at the end of the list, so + * fetch the last page of items. + */ + getLastPage(); + break; + } + } + + protected abstract Request> createRangeRequest(Range range); + + protected abstract void fireCountRequest(Receiver callback); + + /** + * Called when the user chooses a record to view. This default implementation + * sends the {@link PlaceController} to an appropriate {@link ProxyPlace}. + * + * @param record the chosen record + */ + protected void showDetails(P record) { + placeController.goTo(new ProxyPlace(record.stableId(), ProxyPlace.Operation.DETAILS)); + } + + @SuppressWarnings("unchecked") + private EntityProxyId

    cast(ProxyPlace proxyPlace) { + return (EntityProxyId

    ) proxyPlace.getProxyId(); + } + + /** + * Finish selecting a proxy that hadn't yet arrived when + * {@link #select(EntityProxyId)} was called. + */ + private void finishPendingSelection() { + if (pendingSelection != null) { + P selectMe = idToProxy.get(pendingSelection); + pendingSelection = null; + if (selectMe != null) { + selectionModel.setSelected(selectMe, true); + } + } + } + + private void fireRangeRequest(final Range range, final Receiver> callback) { + createRangeRequest(range).with(getView().getPaths()).fire(callback); + } + + private void getLastPage() { + fireCountRequest(new Receiver() { + @Override + public void onSuccess(Long response) { + if (view == null) { + // This activity is dead + return; + } + HasData

    table = getView().asHasData(); + int rows = response.intValue(); + table.setRowCount(rows, true); + if (rows > 0) { + int pageSize = table.getVisibleRange().getLength(); + int remnant = rows % pageSize; + if (remnant == 0) { + table.setVisibleRange(rows - pageSize, pageSize); + } else { + table.setVisibleRange(rows - remnant, pageSize); + } + } + onRangeChanged(table); + } + }); + } + + private void init() { + fireCountRequest(new Receiver() { + @Override + public void onSuccess(Long response) { + if (view == null) { + // This activity is dead + return; + } + getView().asHasData().setRowCount(response.intValue(), true); + onRangeChanged(view.asHasData()); + } + }); + } + + private void update(EntityProxyId

    proxyId) { + final Integer row = idToRow.get(proxyId); + if (row == null) { + return; + } + fireRangeRequest(new Range(row, 1), new Receiver>() { + @Override + public void onSuccess(List

    response) { + getView().asHasData().setRowData(row, + Collections.singletonList(response.get(0))); + } + }); + } + + private void updateSelection(Place newPlace) { + if (newPlace instanceof ProxyPlace) { + ProxyPlace proxyPlace = (ProxyPlace) newPlace; + if (proxyPlace.getOperation() != ProxyPlace.Operation.CREATE + && proxyPlace.getProxyClass().equals(proxyClass)) { + select(cast(proxyPlace)); + return; + } + } + + select(null); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyListView-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyListView-template.java new file mode 100644 index 000000000..c2ad3e7c1 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/AbstractProxyListView-template.java @@ -0,0 +1,47 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.HasData; + +/** + * Abstract implementation of ProxyListView. + * + * @param

    the type of the proxy + */ +public abstract class AbstractProxyListView

    extends Composite implements ProxyListView

    { + private HasData

    display; + private ProxyListView.Delegate

    delegate; + + public HasData

    asHasData() { + return display; + } + + @Override + public AbstractProxyListView

    asWidget() { + return this; + } + + public void setDelegate(final Delegate

    delegate) { + this.delegate = delegate; + } + + protected void init(Widget root, HasData

    display, Button newButton) { + super.initWidget(root); + this.display = display; + + newButton.addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + delegate.createClicked(); + } + }); + } + + protected void initWidget(Widget widget) { + throw new UnsupportedOperationException("AbstractRecordListView must be initialized via init(Widget, HasData

    , Button) "); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/CollectionRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/CollectionRenderer-template.java new file mode 100644 index 000000000..3f9550b74 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/CollectionRenderer-template.java @@ -0,0 +1,39 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +import java.util.Collection; + +/** + * A renderer for Collections that is parameterized by another renderer. + */ +public class CollectionRenderer, T extends Collection> extends AbstractRenderer implements Renderer { + + public static , T extends Collection> CollectionRenderer of(R r) { + return new CollectionRenderer(r); + } + + private R elementRenderer; + + public CollectionRenderer(R elementRenderer) { + this.elementRenderer = elementRenderer; + } + + @Override + public String render(T t) { + StringBuilder toReturn = new StringBuilder(); + boolean first = true; + if (t != null) { + for (E e : t) { + if (!first) { + toReturn.append(','); + } else { + first = false; + } + toReturn.append(elementRenderer.render(e)); + } + } + return toReturn.toString(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/PlaceHistoryFactory-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/PlaceHistoryFactory-template.java new file mode 100644 index 000000000..c3cfeb43c --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/PlaceHistoryFactory-template.java @@ -0,0 +1,24 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.client.managed.request.ApplicationRequestFactory; +import com.google.gwt.place.shared.PlaceTokenizer; +import com.google.inject.Inject; + +public class PlaceHistoryFactory { + private final ProxyListPlace.Tokenizer proxyListPlaceTokenizer; + private final ProxyPlace.Tokenizer proxyPlaceTokenizer; + + @Inject + public PlaceHistoryFactory(ApplicationRequestFactory requestFactory) { + this.proxyListPlaceTokenizer = new ProxyListPlace.Tokenizer(requestFactory); + this.proxyPlaceTokenizer = new ProxyPlace.Tokenizer(requestFactory); + } + + public PlaceTokenizer getProxyListPlaceTokenizer() { + return proxyListPlaceTokenizer; + } + + public PlaceTokenizer getProxyPlaceTokenizer() { + return proxyPlaceTokenizer; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyDetailsView-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyDetailsView-template.java new file mode 100644 index 000000000..72d8ee9b5 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyDetailsView-template.java @@ -0,0 +1,24 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.user.client.TakesValue; +import com.google.gwt.user.client.ui.IsWidget; + +/** + * Implemented by views that show the details of an object. + * + * @param

    the type of object to show + */ +public interface ProxyDetailsView

    extends TakesValue

    , IsWidget { + + /** + * Implemented by the owner of the view. + */ + interface Delegate { + + void deleteClicked(); + + void editClicked(); + } + + boolean confirm(String msg); +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyEditView-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyEditView-template.java new file mode 100644 index 000000000..725345b3e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyEditView-template.java @@ -0,0 +1,32 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.editor.client.HasEditorErrors; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.gwt.user.client.ui.IsWidget; + +/** + * Implemented by views that edit {@link EntityProxy}s. + * + * @param

    the type of the proxy + * @param the type of this ProxyEditView, required to allow {@link #createEditorDriver()} to be correctly typed + */ +public interface ProxyEditView

    > extends IsWidget, HasEditorErrors

    { + + /** + * @return a new {@link RequestFactoryEditorDriver} initialized to run this editor + */ + RequestFactoryEditorDriver createEditorDriver(); + + /** + * Implemented by the owner of the view. + */ + interface Delegate { + + void cancelClicked(); + + void saveClicked(); + } + + void setEnabled(boolean b); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListPlace-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListPlace-template.java new file mode 100644 index 000000000..c73f0abec --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListPlace-template.java @@ -0,0 +1,68 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceTokenizer; +import com.google.gwt.place.shared.Prefix; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.web.bindery.requestfactory.shared.RequestFactory; + +/** + * A place in the app that deals with lists of {@link EntityProxy}. + */ +public class ProxyListPlace extends Place { + + /** + * Tokenizer. + */ + @Prefix("l") + public static class Tokenizer implements PlaceTokenizer { + private final RequestFactory requests; + + public Tokenizer(RequestFactory requests) { + this.requests = requests; + } + + public ProxyListPlace getPlace(String token) { + return new ProxyListPlace(requests.getProxyClass(token)); + } + + public String getToken(ProxyListPlace place) { + return requests.getHistoryToken(place.getProxyClass()); + } + } + + private final Class proxyType; + + public ProxyListPlace(Class proxyType) { + this.proxyType = proxyType; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ProxyListPlace)) { + return false; + } + if (this == obj) { + return true; + } + ProxyListPlace other = (ProxyListPlace) obj; + return proxyType.equals(other.proxyType); + } + + public Class getProxyClass() { + return proxyType; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + proxyType.hashCode(); + return result; + } + + @Override + public String toString() { + return "ProxyListPlace [proxyType=" + proxyType + "]"; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListPlacePicker-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListPlacePicker-template.java new file mode 100644 index 000000000..a16504df2 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListPlacePicker-template.java @@ -0,0 +1,47 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.place.shared.PlaceChangeEvent; +import com.google.gwt.place.shared.PlaceController; +import com.google.gwt.user.client.ui.HasConstrainedValue; + +/** + * Drives a {@link ValueChangeHandler} populated with {@link ProxyListPlace} + * instances, keeping it in sync with the {@link PlaceController}'s notion of + * the current place, and firing place change events as selections are made. + */ +public class ProxyListPlacePicker implements ValueChangeHandler, PlaceChangeEvent.Handler { + private HasConstrainedValue view; + private final PlaceController placeController; + private final ProxyPlaceToListPlace proxyPlaceToListPlace; + + public ProxyListPlacePicker(PlaceController placeController, ProxyPlaceToListPlace proxyPlaceToListPlace) { + this.placeController = placeController; + this.proxyPlaceToListPlace = proxyPlaceToListPlace; + } + + public void onPlaceChange(PlaceChangeEvent event) { + view.setValue(proxyPlaceToListPlace.proxyListPlaceFor(event.getNewPlace()), false); + } + + public void onValueChange(ValueChangeEvent event) { + placeController.goTo(event.getValue()); + } + + public HandlerRegistration register(EventBus eventBus, HasConstrainedValue view) { + this.view = view; + final HandlerRegistration placeRegistration = eventBus.addHandler(PlaceChangeEvent.TYPE, this); + final HandlerRegistration viewRegistration = view.addValueChangeHandler(this); + + return new HandlerRegistration() { + public void removeHandler() { + placeRegistration.removeHandler(); + viewRegistration.removeHandler(); + ProxyListPlacePicker.this.view = null; + } + }; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListView-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListView-template.java new file mode 100644 index 000000000..3d90cfb80 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyListView-template.java @@ -0,0 +1,40 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.view.client.HasData; + +/** + * A view of a list of {@link EntityProxy}s, which declares which properties it + * is able to display. + *

    + * It is expected that such views will typically (eventually) be defined largely + * in ui.xml files which declare the properties of interest, which is why the + * view is a source of a property set rather than a receiver of one. + * + * @param

    the type of the records to display + */ +public interface ProxyListView

    extends IsWidget { + + /** + * Implemented by the owner of a RecordTableView. + * + * @param the type of the records to display + */ + interface Delegate { + + void createClicked(); + } + + HasData

    asHasData(); + + /** + * @return the set of properties this view displays + */ + String[] getPaths(); + + /** + * Sets the delegate. + */ + void setDelegate(Delegate

    delegate); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyPlace-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyPlace-template.java new file mode 100644 index 000000000..77f796398 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyPlace-template.java @@ -0,0 +1,132 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceTokenizer; +import com.google.gwt.place.shared.Prefix; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.web.bindery.requestfactory.shared.EntityProxyId; +import com.google.web.bindery.requestfactory.shared.RequestFactory; + +/** + * A place in the app that deals with a specific {@link RequestFactory} proxy. + */ +public class ProxyPlace extends Place { + + /** + * The things you do with a record, each of which is a different bookmarkable + * location in the scaffold app. + */ + public enum Operation { + CREATE, EDIT, DETAILS + } + + /** + * Tokenizer. + */ + @Prefix("r") + public static class Tokenizer implements PlaceTokenizer { + private static final String SEPARATOR = "!"; + private final RequestFactory requests; + + public Tokenizer(RequestFactory requests) { + this.requests = requests; + } + + public ProxyPlace getPlace(String token) { + String bits[] = token.split(SEPARATOR); + Operation operation = Operation.valueOf(bits[1]); + if (Operation.CREATE == operation) { + return new ProxyPlace(requests.getProxyClass(bits[0])); + } + return new ProxyPlace(requests.getProxyId(bits[0]), operation); + } + + public String getToken(ProxyPlace place) { + if (Operation.CREATE == place.getOperation()) { + return requests.getHistoryToken(place.getProxyClass()) + SEPARATOR + place.getOperation(); + } + return requests.getHistoryToken(place.getProxyId()) + SEPARATOR + place.getOperation(); + } + } + + private final EntityProxyId proxyId; + private final Class proxyClass; + private final Operation operation; + + public ProxyPlace(Class proxyClass) { + this.operation = Operation.CREATE; + this.proxyId = null; + this.proxyClass = proxyClass; + } + + public ProxyPlace(EntityProxyId record) { + this(record, Operation.DETAILS); + } + + public ProxyPlace(EntityProxyId proxyId, Operation operation) { + this.operation = operation; + this.proxyId = proxyId; + this.proxyClass = null; + assert Operation.CREATE != operation; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ProxyPlace)) { + return false; + } + if (this == obj) { + return true; + } + + ProxyPlace other = (ProxyPlace) obj; + if (operation != other.operation) { + return false; + } + if (proxyClass == null) { + if (other.proxyClass != null) { + return false; + } + } else if (!proxyClass.equals(other.proxyClass)) { + return false; + } + if (proxyId == null) { + if (other.proxyId != null) { + return false; + } + } else if (!proxyId.equals(other.proxyId)) { + return false; + } + return true; + } + + public Operation getOperation() { + return operation; + } + + public Class getProxyClass() { + return proxyId != null ? proxyId.getProxyClass() : proxyClass; + } + + /** + * @return the proxyId, or null if the operation is {@link Operation#CREATE} + */ + public EntityProxyId getProxyId() { + return proxyId; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((operation == null) ? 0 : operation.hashCode()); + result = prime * result + ((proxyClass == null) ? 0 : proxyClass.hashCode()); + result = prime * result + ((proxyId == null) ? 0 : proxyId.hashCode()); + return result; + } + + @Override + public String toString() { + return "ProxyPlace [operation=" + operation + ", proxy=" + proxyId + ", proxyClass=" + proxyClass + "]"; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyPlaceToListPlace-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyPlaceToListPlace-template.java new file mode 100644 index 000000000..a532c8581 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ProxyPlaceToListPlace-template.java @@ -0,0 +1,35 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.place; + +import com.google.gwt.activity.shared.FilteredActivityMapper; +import com.google.gwt.place.shared.Place; + +/** + * Converts a {@link #ProxyPlace} to a {@link ProxyListPlace}. + */ +public class ProxyPlaceToListPlace implements FilteredActivityMapper.Filter { + + /** + * Required by {@link FilteredActivityMapper.Filter}, calls + * {@link #proxyListPlaceFor()}. + */ + public Place filter(Place place) { + return proxyListPlaceFor(place); + } + + /** + * @param place a place to process + * @return an appropriate ProxyListPlace, or null if the given place has nothing to do with proxies + */ + public ProxyListPlace proxyListPlaceFor(Place place) { + if (place instanceof ProxyListPlace) { + return (ProxyListPlace) place; + } + + if (!(place instanceof ProxyPlace)) { + return null; + } + + ProxyPlace proxyPlace = (ProxyPlace) place; + return new ProxyListPlace(proxyPlace.getProxyClass()); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ScaffoldPlaceHistoryMapper-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ScaffoldPlaceHistoryMapper-template.java new file mode 100644 index 000000000..1f5f9b48c --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/place/ScaffoldPlaceHistoryMapper-template.java @@ -0,0 +1,31 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.gwt.place.shared.PlaceHistoryMapperWithFactory; + +/** + * This interface is the hub of your application's navigation system. It links + * the {@link com.google.gwt.place.shared.Place Place}s your user navigates to with + * the browser history system — that is, it makes the browser's back and + * forth buttons work for you, and also makes each spot in your app + * bookmarkable. + *

    + *

    + * The simplest way to make new {@link com.google.gwt.place.shared.Place Place} + * types available to your app is to uncomment the {@literal @}WithTokenizers + * annotation below and list their corresponding + * {@link com.google.gwt.place.shared.PlaceTokenizer PlaceTokenizer}s. + *

    + *

    + * This code generated object looks to both the {@literal @}WithTokenizers + * annotation and the factory to infer the types of + * {@link __TOP_LEVEL_PACKAGE__.client.scaffold.place.Place Place}s your app can navigate to. In + * this case it will find the {@link PlaceHistoryFactory#getProxyListPlaceTokenizer()} and + * {@link PlaceHistoryFactory#getProxyPlaceTokenizer()} methods, and so be able to handle + * {@link __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyListPlace ProxyListPlace}s (which show + * all entities of a particular type) and + * {@link __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyPlace ProxyPlace}s (which give access to + * an individual entity). + */ +// @WithTokenizers({MyNewPlace.Tokenizer, MyOtherNewPlace.Tokenizer}) +public interface ScaffoldPlaceHistoryMapper extends PlaceHistoryMapperWithFactory { +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/request/EventSourceRequestTransport-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/request/EventSourceRequestTransport-template.java new file mode 100644 index 000000000..d0fd7a8bd --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/request/EventSourceRequestTransport-template.java @@ -0,0 +1,53 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.request; + +import com.google.gwt.event.shared.EventBus; +import com.google.web.bindery.requestfactory.gwt.client.DefaultRequestTransport; +import com.google.web.bindery.requestfactory.shared.RequestTransport; +import com.google.web.bindery.requestfactory.shared.ServerFailure; + +/** + * Wraps {@link RequestTransport} to post events as requests are sent + * and received. + */ +public class EventSourceRequestTransport implements RequestTransport { + private final EventBus eventBus; + private final RequestTransport wrapped; + + public EventSourceRequestTransport(EventBus eventBus) { + this(eventBus, new DefaultRequestTransport()); + } + + public EventSourceRequestTransport(EventBus eventBus, RequestTransport wrapped) { + this.eventBus = eventBus; + this.wrapped = wrapped; + } + + public void send(String payload, final TransportReceiver receiver) { + TransportReceiver myReceiver = new TransportReceiver() { + + @Override + public void onTransportSuccess(String payload) { + try { + receiver.onTransportSuccess(payload); + } finally { + eventBus.fireEvent(new RequestEvent(RequestEvent.State.RECEIVED)); + } + } + + @Override + public void onTransportFailure(ServerFailure failure) { + try { + receiver.onTransportFailure(failure); + } finally { + eventBus.fireEvent(new RequestEvent(RequestEvent.State.RECEIVED)); + } + } + }; + + try { + wrapped.send(payload, myReceiver); + } finally { + eventBus.fireEvent(new RequestEvent(RequestEvent.State.SENT)); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/request/RequestEvent-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/request/RequestEvent-template.java new file mode 100644 index 000000000..c625d7e07 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/request/RequestEvent-template.java @@ -0,0 +1,75 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.request; + +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.event.shared.HandlerRegistration; + +/** + * An event posted whenever an RPC request is sent or its response is received. + */ +public class RequestEvent extends GwtEvent { + + /** + * Implemented by handlers of this type of event. + */ + public interface Handler extends EventHandler { + + /** + * Called when a {@link RequestEvent} is fired. + * + * @param requestEvent a {@link RequestEvent} instance + */ + void onRequestEvent(RequestEvent requestEvent); + } + + /** + * The request state. + */ + public enum State { + SENT, RECEIVED + } + + private static final Type TYPE = new Type(); + + /** + * Register a {@link RequestEvent.Handler} on an {@link EventBus}. + * + * @param eventBus the {@link EventBus} + * @param handler a {@link RequestEvent.Handler} + * @return a {@link HandlerRegistration} instance + */ + public static HandlerRegistration register(EventBus eventBus, RequestEvent.Handler handler) { + return eventBus.addHandler(TYPE, handler); + } + + private final State state; + + /** + * Constructs a new @{link RequestEvent}. + * + * @param state a {@link State} instance + */ + public RequestEvent(State state) { + this.state = state; + } + + @Override + public GwtEvent.Type getAssociatedType() { + return TYPE; + } + + /** + * Returns the {@link State} associated with this event. + * + * @return a {@link State} instance + */ + public State getState() { + return state; + } + + @Override + protected void dispatch(Handler handler) { + handler.onRequestEvent(this); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalBox-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalBox-template.java new file mode 100644 index 000000000..0a57b3fd8 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalBox-template.java @@ -0,0 +1,16 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.ui.ValueBox; + +import java.math.BigDecimal; + +/** + * A ValueBox that uses {@link BigDecimalParser} and {@link BigDecimalRenderer}. + */ +public class BigDecimalBox extends ValueBox { + + public BigDecimalBox() { + super(Document.get().createTextInputElement(), BigDecimalRenderer.instance(), BigDecimalParser.instance()); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalParser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalParser-template.java new file mode 100644 index 000000000..5e222d548 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalParser-template.java @@ -0,0 +1,38 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.Parser; + +import java.math.BigDecimal; +import java.text.ParseException; + +/** + * Simple parser of BigDecimal that wraps {@link BigDecimal#toString()}. + */ +public class BigDecimalParser implements Parser { + private static BigDecimalParser INSTANCE; + + /** + * @return the instance of the no-op renderer + */ + public static Parser instance() { + if (INSTANCE == null) { + INSTANCE = new BigDecimalParser(); + } + return INSTANCE; + } + + protected BigDecimalParser() { + } + + public BigDecimal parse(CharSequence object) throws ParseException { + if (object == null || "".equals(object.toString())) { + return null; + } + + try { + return new BigDecimal(object.toString()); + } catch (NumberFormatException e) { + throw new ParseException(e.getMessage(), 0); + } + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalRenderer-template.java new file mode 100644 index 000000000..090a52136 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/BigDecimalRenderer-template.java @@ -0,0 +1,34 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +import java.math.BigDecimal; + +/** + * A simple renderer of Float values. + */ +public class BigDecimalRenderer extends AbstractRenderer { + private static BigDecimalRenderer INSTANCE; + + /** + * @return the instance + */ + public static Renderer instance() { + if (INSTANCE == null) { + INSTANCE = new BigDecimalRenderer(); + } + return INSTANCE; + } + + protected BigDecimalRenderer() { + } + + public String render(BigDecimal object) { + if (object == null) { + return ""; + } + + return object.toString(); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteBox-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteBox-template.java new file mode 100644 index 000000000..23a59e218 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteBox-template.java @@ -0,0 +1,14 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.ui.ValueBox; + +/** + * A ValueBox that uses {@link ByteParser} and {@link ByteRenderer}. + */ +public class ByteBox extends ValueBox { + + public ByteBox() { + super(Document.get().createTextInputElement(), ByteRenderer.instance(), ByteParser.instance()); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteParser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteParser-template.java new file mode 100644 index 000000000..b2818921e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteParser-template.java @@ -0,0 +1,37 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.Parser; + +import java.text.ParseException; + +/** + * Simple parser of Byte that wraps {@link Byte#valueOf(String)}. + */ +public class ByteParser implements Parser { + private static ByteParser INSTANCE; + + /** + * @return the instance of the no-op renderer + */ + public static Parser instance() { + if (INSTANCE == null) { + INSTANCE = new ByteParser(); + } + return INSTANCE; + } + + protected ByteParser() { + } + + public Byte parse(CharSequence object) throws ParseException { + if (object == null || "".equals(object.toString())) { + return null; + } + + try { + return Byte.valueOf(object.toString()); + } catch (NumberFormatException e) { + throw new ParseException(e.getMessage(), 0); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteRenderer-template.java new file mode 100644 index 000000000..dab5e3793 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ByteRenderer-template.java @@ -0,0 +1,32 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +/** + * A simple renderer of Byte values. + */ +public class ByteRenderer extends AbstractRenderer { + private static ByteRenderer INSTANCE; + + /** + * @return the instance + */ + public static Renderer instance() { + if (INSTANCE == null) { + INSTANCE = new ByteRenderer(); + } + return INSTANCE; + } + + protected ByteRenderer() { + } + + public String render(Byte object) { + if (object == null) { + return ""; + } + + return object.toString(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharBox-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharBox-template.java new file mode 100644 index 000000000..fdbe97533 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharBox-template.java @@ -0,0 +1,14 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.ui.ValueBox; + +/** + * A ValueBox that uses {@link CharParser} and {@link CharRenderer}. + */ +public class CharBox extends ValueBox { + + public CharBox() { + super(Document.get().createTextInputElement(), CharRenderer.instance(), CharParser.instance()); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharParser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharParser-template.java new file mode 100644 index 000000000..70c522c4c --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharParser-template.java @@ -0,0 +1,36 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.Parser; + +import java.text.ParseException; + +/** + * Simple parser of Character. + */ +public class CharParser implements Parser { + private static CharParser INSTANCE; + + /** + * @return the instance of the no-op renderer + */ + public static Parser instance() { + if (INSTANCE == null) { + INSTANCE = new CharParser(); + } + return INSTANCE; + } + + protected CharParser() { + } + + public Character parse(CharSequence object) throws ParseException { + if (object == null || object.length() == 0 || "".equals(object.toString())) { + return null; + } + try { + return object.charAt(0); + } catch (IndexOutOfBoundsException e) { + throw new ParseException(e.getMessage(), 0); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharRenderer-template.java new file mode 100644 index 000000000..a4f6c1a5d --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CharRenderer-template.java @@ -0,0 +1,32 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +/** + * A simple renderer of Character values. + */ +public class CharRenderer extends AbstractRenderer { + private static CharRenderer INSTANCE; + + /** + * @return the instance + */ + public static Renderer instance() { + if (INSTANCE == null) { + INSTANCE = new CharRenderer(); + } + return INSTANCE; + } + + protected CharRenderer() { + } + + public String render(Character object) { + if (object == null) { + return ""; + } + + return object.toString(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CollectionRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CollectionRenderer-template.java new file mode 100644 index 000000000..66e7fbbbb --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/CollectionRenderer-template.java @@ -0,0 +1,39 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +import java.util.Collection; + +/** + * A renderer for Collections that is parameterized by another renderer. + */ +public class CollectionRenderer, T extends Collection> extends AbstractRenderer implements Renderer { + + public static , T extends Collection> CollectionRenderer of(R r) { + return new CollectionRenderer(r); + } + + private R elementRenderer; + + public CollectionRenderer(R elementRenderer) { + this.elementRenderer = elementRenderer; + } + + @Override + public String render(T t) { + StringBuilder toReturn = new StringBuilder(); + boolean first = true; + if (t != null) { + for (E e : t) { + if (!first) { + toReturn.append(','); + } else { + first = false; + } + toReturn.append(elementRenderer.render(e)); + } + } + return toReturn.toString(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatBox-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatBox-template.java new file mode 100644 index 000000000..86bea11b2 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatBox-template.java @@ -0,0 +1,14 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.ui.ValueBox; + +/** + * A ValueBox that uses {@link FloatParser} and {@link FloatRenderer}. + */ +public class FloatBox extends ValueBox { + + public FloatBox() { + super(Document.get().createTextInputElement(), FloatRenderer.instance(), FloatParser.instance()); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatParser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatParser-template.java new file mode 100644 index 000000000..0f15edfde --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatParser-template.java @@ -0,0 +1,37 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.Parser; + +import java.text.ParseException; + +/** + * Simple parser of Float that wraps {@link Float#valueOf(String)}. + */ +public class FloatParser implements Parser { + private static FloatParser INSTANCE; + + /** + * @return the instance of the no-op renderer + */ + public static Parser instance() { + if (INSTANCE == null) { + INSTANCE = new FloatParser(); + } + return INSTANCE; + } + + protected FloatParser() { + } + + public Float parse(CharSequence object) throws ParseException { + if (object == null || "".equals(object.toString())) { + return null; + } + + try { + return Float.valueOf(object.toString()); + } catch (NumberFormatException e) { + throw new ParseException(e.getMessage(), 0); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatRenderer-template.java new file mode 100644 index 000000000..168987fdd --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/FloatRenderer-template.java @@ -0,0 +1,33 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +/** + * A simple renderer of Float values. + */ +public class FloatRenderer extends AbstractRenderer { + private static FloatRenderer INSTANCE; + + /** + * @return the instance + */ + public static Renderer instance() { + if (INSTANCE == null) { + INSTANCE = new FloatRenderer(); + } + return INSTANCE; + } + + protected FloatRenderer() { + } + + public String render(Float object) { + if (object == null) { + return ""; + } + + return NumberFormat.getDecimalFormat().format(object); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/LoginWidget-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/LoginWidget-template.java new file mode 100644 index 000000000..385ce7e3c --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/LoginWidget-template.java @@ -0,0 +1,47 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Widget; + +/** + * A simple widget which displays info about the user and a logout link. + */ +public class LoginWidget extends Composite { + + interface Binder extends UiBinder { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + @UiField SpanElement name; + @UiField Anchor logoutLink; + + public LoginWidget() { + initWidget(BINDER.createAndBindUi(this)); + } + + public void setUserName(String userName) { + name.setInnerText(userName); + } + + public void setLogoutUrl(String url) { + logoutLink.setHref(url); + } + + /** + * Squelch clicks of the logout link if no href has been set. + */ + @UiHandler("logoutLink") + void handleClick(ClickEvent e) { + if ("".equals(logoutLink.getHref())) { + e.stopPropagation(); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/LoginWidget-template.ui.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/LoginWidget-template.ui.xml new file mode 100644 index 000000000..addb34f5a --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/LoginWidget-template.ui.xml @@ -0,0 +1,18 @@ + + + .link { + /* make it look like text */ + color: inherit; + text-decoration: inherit; + } + + + +

    + Not logged in + | + Sign out +
    + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/MobileProxyListView-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/MobileProxyListView-template.java new file mode 100644 index 000000000..30960dcfa --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/MobileProxyListView-template.java @@ -0,0 +1,52 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import __TOP_LEVEL_PACKAGE__.client.scaffold.ScaffoldMobileApp; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.AbstractProxyListView; +import __TOP_LEVEL_PACKAGE__.client.scaffold.place.ProxyListView; +import com.google.gwt.cell.client.AbstractCell; +import com.google.gwt.core.client.GWT; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.text.shared.SafeHtmlRenderer; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.cellview.client.CellList; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Widget; + +/** + * An implementation of {@link ProxyListView} used in mobile applications + * + * @param

    the type of the proxy + */ +public abstract class MobileProxyListView

    extends AbstractProxyListView

    { + + interface Binder extends UiBinder> { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + @UiField(provided = true) CellList

    list; + @UiField Button newButton; + + /** + * Constructor. + * + * @param buttonText the text to display on the create button + * @param renderer the {@link SafeHtmlRenderer} used to render the proxy + */ + public MobileProxyListView(String buttonText, final SafeHtmlRenderer

    renderer) { + // Create the CellList to display the proxies. + AbstractCell

    cell = new AbstractCell

    () { + @Override + public void render(Context context, P value, SafeHtmlBuilder sb) { + renderer.render(value, sb); + } + }; + this.list = new CellList

    (cell, ScaffoldMobileApp.getMobileListResources()); + init(BINDER.createAndBindUi(this), list, newButton); + + // Initialize the widget. + this.newButton.setText(buttonText); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/MobileProxyListView-template.ui.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/MobileProxyListView-template.ui.xml new file mode 100644 index 000000000..da2b991e2 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/MobileProxyListView-template.ui.xml @@ -0,0 +1,36 @@ + + + + + + @sprite .createButton { + gwt-image: 'createButton'; + border: 0; + margin: 9px 0px 9px 9px; + width: 12em; + font-size: 12pt; + cursor: pointer; + text-align: left; + padding-left: 22px; + color: #6a779a; + } + + .list { + border-top: 1px solid #ddd; + } + + + + + + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortBox-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortBox-template.java new file mode 100644 index 000000000..f5fe605dd --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortBox-template.java @@ -0,0 +1,14 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.ui.ValueBox; + +/** + * A ValueBox that uses {@link ShortParser} and {@link ShortRenderer}. + */ +public class ShortBox extends ValueBox { + + public ShortBox() { + super(Document.get().createTextInputElement(), ShortRenderer.instance(), ShortParser.instance()); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortParser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortParser-template.java new file mode 100644 index 000000000..733229ff2 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortParser-template.java @@ -0,0 +1,37 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.Parser; + +import java.text.ParseException; + +/** + * Simple parser of Short that wraps {@link Short#valueOf(String)}. + */ +public class ShortParser implements Parser { + private static ShortParser INSTANCE; + + /** + * @return the instance of the no-op renderer + */ + public static Parser instance() { + if (INSTANCE == null) { + INSTANCE = new ShortParser(); + } + return INSTANCE; + } + + protected ShortParser() { + } + + public Short parse(CharSequence object) throws ParseException { + if (object == null || "".equals(object.toString())) { + return null; + } + + try { + return Short.valueOf(object.toString()); + } catch (NumberFormatException e) { + throw new ParseException(e.getMessage(), 0); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortRenderer-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortRenderer-template.java new file mode 100644 index 000000000..b486bb067 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/scaffold/ui/ShortRenderer-template.java @@ -0,0 +1,32 @@ +package __TOP_LEVEL_PACKAGE__.client.scaffold.ui; + +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; + +/** + * A simple renderer of Short values. + */ +public class ShortRenderer extends AbstractRenderer { + private static ShortRenderer INSTANCE; + + /** + * @return the instance + */ + public static Renderer instance() { + if (INSTANCE == null) { + INSTANCE = new ShortRenderer(); + } + return INSTANCE; + } + + protected ShortRenderer() { + } + + public String render(Short object) { + if (object == null) { + return ""; + } + + return object.toString(); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/MobileListResources-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/MobileListResources-template.java new file mode 100644 index 000000000..60fbab82e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/MobileListResources-template.java @@ -0,0 +1,27 @@ +package __TOP_LEVEL_PACKAGE__.client.style; + +import com.google.gwt.resources.client.CssResource.NotStrict; +import com.google.gwt.user.cellview.client.CellList; + +/** + * The styles and resources used by the mobile Scaffold. + */ +public interface MobileListResources extends CellList.Resources { + + interface MobileStyle extends CellList.Style { + + /** + * Applied to the date property in a cell. + */ + String dateProp(); + + /** + * Applied to the secondary property in a cell. + */ + String secondaryProp(); + } + + @NotStrict + @Source("mobile.css") + MobileStyle cellListStyle(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/backButton-template.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/backButton-template.png new file mode 100644 index 000000000..3013828d2 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/backButton-template.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/createButton-template.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/createButton-template.png new file mode 100644 index 000000000..7da8e84fd Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/createButton-template.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/createButton.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/createButton.png new file mode 100644 index 000000000..7da8e84fd Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/createButton.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/groupIcon.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/groupIcon.png new file mode 100644 index 000000000..af8e401b0 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/groupIcon.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/gwtLogo.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/gwtLogo.png new file mode 100644 index 000000000..f08094e24 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/gwtLogo.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/openGradient-template.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/openGradient-template.png new file mode 100644 index 000000000..b381d52ef Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/openGradient-template.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/openGradient.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/openGradient.png new file mode 100644 index 000000000..b381d52ef Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/openGradient.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/rooLogo.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/rooLogo.png new file mode 100644 index 000000000..e58fbd9af Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/rooLogo.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/selectionGradient-template.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/selectionGradient-template.png new file mode 100644 index 000000000..108ea3fc0 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/selectionGradient-template.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/selectionGradient.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/selectionGradient.png new file mode 100644 index 000000000..108ea3fc0 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/selectionGradient.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/titleGradient-template.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/titleGradient-template.png new file mode 100644 index 000000000..275912133 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/titleGradient-template.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/userIcon.png b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/userIcon.png new file mode 100644 index 000000000..f879804e3 Binary files /dev/null and b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/images/userIcon.png differ diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/mobile-template.css b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/mobile-template.css new file mode 100644 index 000000000..723ba1575 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/client/style/mobile-template.css @@ -0,0 +1,54 @@ +.cellListWidget { +} + +.cellListEvenItem { + cursor: pointer; + border-bottom: 1px solid #ddd; + padding: 10px; + font-weight: bold; + font-size: 12pt; +} + +.cellListOddItem { + cursor: pointer; + border-bottom: 1px solid #ddd; + padding: 10px; + font-weight: bold; + font-size: 12pt; +} + +.cellListKeyboardSelectedItem { +} + +.cellListSelectedItem { +} + +.secondaryProp { + padding-top: 2px; + color: #888; + font-size: 10pt; + width: 70%; + overflow: hidden; + min-height: 10pt; +} + +.dateProp { + padding-top: 2px; + text-align: right; + color: #888; + font-size: 10pt; + overflow: hidden; + position: absolute; + top: 0px; + right: 10px; +} + +.gwt-TextBox { + border: 1px solid black; + padding: 1px; +} + +.gwt-DateBox { + border: 1px solid black; + padding: 1px; +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/CustomRequestFactoryServlet-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/CustomRequestFactoryServlet-template.java new file mode 100644 index 000000000..8d07f0782 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/CustomRequestFactoryServlet-template.java @@ -0,0 +1,17 @@ +package __TOP_LEVEL_PACKAGE__.server; + +import com.google.web.bindery.requestfactory.server.DefaultExceptionHandler; +import com.google.web.bindery.requestfactory.server.ExceptionHandler; +import com.google.web.bindery.requestfactory.server.RequestFactoryServlet; +import com.google.web.bindery.requestfactory.server.ServiceLayerDecorator; + +public class CustomRequestFactoryServlet extends RequestFactoryServlet { + + public CustomRequestFactoryServlet() { + this(new DefaultExceptionHandler(), new CustomServiceLayerDecorator()); + } + + public CustomRequestFactoryServlet(ExceptionHandler exceptionHandler, ServiceLayerDecorator... serviceDecorators) { + super(exceptionHandler, serviceDecorators); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/CustomServiceLayerDecorator-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/CustomServiceLayerDecorator-template.java new file mode 100644 index 000000000..d2d29b6b8 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/CustomServiceLayerDecorator-template.java @@ -0,0 +1,15 @@ +package __TOP_LEVEL_PACKAGE__.server; + +import com.google.web.bindery.requestfactory.server.ServiceLayerDecorator; +import com.google.web.bindery.requestfactory.shared.Locator; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +public class CustomServiceLayerDecorator extends ServiceLayerDecorator { + + @Override + public > T createLocator(Class clazz) { + ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(CustomRequestFactoryServlet.getThreadLocalServletContext()); + return context.getBean(clazz); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/CustomXmlWebApplicationContext-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/CustomXmlWebApplicationContext-template.java new file mode 100644 index 000000000..9927203e8 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/CustomXmlWebApplicationContext-template.java @@ -0,0 +1,19 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.web.context.support.XmlWebApplicationContext; + +import com.google.appengine.api.utils.SystemProperty; + +public class CustomXmlWebApplicationContext extends XmlWebApplicationContext +{ + protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) + { + super.initBeanDefinitionReader(beanDefinitionReader); + if (SystemProperty.environment.value() == SystemProperty.Environment.Value.Production) + { + beanDefinitionReader.setValidating(false); + } + beanDefinitionReader.setValidating(false); + } +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/GaeAuthFilter-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/GaeAuthFilter-template.java new file mode 100644 index 000000000..7d628b1e3 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/GaeAuthFilter-template.java @@ -0,0 +1,39 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A servlet filter that handles basic GAE user authentication. + */ +public class GaeAuthFilter implements Filter { + + public void destroy() { + } + + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + UserService userService = UserServiceFactory.getUserService(); + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + if (!userService.isUserLoggedIn()) { + String requestUrl = request.getHeader("requestUrl"); + if (requestUrl == null) { + requestUrl = request.getRequestURI(); + } + response.setHeader("login", userService.createLoginURL(requestUrl)); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + filterChain.doFilter(request, response); + } + + public void init(FilterConfig config) { + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/UserServiceLocator-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/UserServiceLocator-template.java new file mode 100644 index 000000000..28c685348 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/UserServiceLocator-template.java @@ -0,0 +1,29 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.web.bindery.requestfactory.shared.ServiceLocator; + +/** + * Gives a RequestFactory system access to the Google AppEngine UserService. + */ +public class UserServiceLocator implements ServiceLocator { + + public UserServiceWrapper getInstance(Class clazz) { + final UserService service = UserServiceFactory.getUserService(); + return new UserServiceWrapper() { + public String createLoginURL(String destinationURL) { + return service.createLoginURL(destinationURL); + } + + public String createLogoutURL(String destinationURL) { + return service.createLogoutURL(destinationURL); + } + + public User getCurrentUser() { + return service.getCurrentUser(); + } + }; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/UserServiceWrapper-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/UserServiceWrapper-template.java new file mode 100644 index 000000000..6b673620a --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/UserServiceWrapper-template.java @@ -0,0 +1,18 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.appengine.api.users.User; + +/** + * Service object that reduces the visible api of + * {@link com.google.appengine.api.users.UserService}. Needed to work around a + * limitation of RequestFactory, which cannot yet handle overloaded service + * methods. + */ +public interface UserServiceWrapper { + + String createLoginURL(String destinationURL); + + String createLogoutURL(String destinationURL); + + User getCurrentUser(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthentication-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthentication-template.java new file mode 100644 index 000000000..ad9c1867e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthentication-template.java @@ -0,0 +1,68 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +public class GoogleAuthentication extends PreAuthenticatedAuthenticationToken +{ + private static final long serialVersionUID = 1L; + + private GoogleUser googleUser; + private Object aCredentials; + private boolean isAuthenticated; + + public GoogleAuthentication(GoogleUser googleUser, Object aCredentials) + { + super(googleUser, aCredentials); + this.googleUser = googleUser; + this.aCredentials = aCredentials; + this.isAuthenticated = googleUser != null; + } + + public GoogleAuthentication(GoogleUser googleUser, Object aCredentials, Collection authorities) + { + super(googleUser, aCredentials, authorities); + this.googleUser = googleUser; + this.aCredentials = aCredentials; + this.isAuthenticated = googleUser != null; + } + + @Override + public String getName() + { + return googleUser == null ? "" : googleUser.getEmail(); + } + + @Override + public Collection getAuthorities() + { + return googleUser == null ? new ArrayList() : googleUser.getAuthorities(); + } + + @Override + public Object getCredentials() + { + return aCredentials; + } + + @Override + public Object getPrincipal() + { + return googleUser == null ? "" : googleUser.getEmail(); + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException + { + this.isAuthenticated = isAuthenticated; + } + + @Override + public boolean isAuthenticated() + { + return isAuthenticated; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationEntryPoint-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationEntryPoint-template.java new file mode 100644 index 000000000..f6c7e697c --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationEntryPoint-template.java @@ -0,0 +1,22 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +public class GoogleAuthenticationEntryPoint implements AuthenticationEntryPoint +{ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException, ServletException + { + response.sendRedirect("/security/login.jsp"); + + } + +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationFilter-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationFilter-template.java new file mode 100644 index 000000000..e91ea34b4 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationFilter-template.java @@ -0,0 +1,76 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; + +@Component +public class GoogleAuthenticationFilter extends GenericFilterBean +{ + private AuthenticationManager authenticationManager; + private AuthenticationDetailsSource ads = new WebAuthenticationDetailsSource(); + private AuthenticationFailureHandler authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler(); + + private UserService userService = UserServiceFactory.getUserService(); + + @Autowired + public void setAuthenticationManager(AuthenticationManager authenticationManager) + { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) + { + User user = userService.getCurrentUser(); + + if (user != null) + { + String email = user.getEmail(); + + PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(email, null); + token.setDetails(ads.buildDetails((HttpServletRequest) request)); + + try + { + authentication = authenticationManager.authenticate(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + catch (AuthenticationException ex) + { + authenticationFailureHandler.onAuthenticationFailure((HttpServletRequest) request, (HttpServletResponse) response, ex); + } + } + } + + chain.doFilter(request, response); + } + +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationProvider-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationProvider-template.java new file mode 100644 index 000000000..07ab1c4fa --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleAuthenticationProvider-template.java @@ -0,0 +1,33 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +public class GoogleAuthenticationProvider implements AuthenticationProvider +{ + private GoogleUserService googleUserService; + + @Autowired + public void setGoogleUserService(GoogleUserService googleUserService) + { + this.googleUserService = googleUserService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException + { + GoogleUser googleUser = googleUserService.findCurrentUser(); + + return new GoogleAuthentication(googleUser, authentication.getDetails()); + } + + @Override + public boolean supports(Class authentication) + { + return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleUser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleUser-template.java new file mode 100644 index 000000000..6ca6c5ffb --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleUser-template.java @@ -0,0 +1,17 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; + +import com.tvt.fieldmanager.shared.gae.GaeUser; + +public interface GoogleUser +{ + Collection getAuthorities(); + + String getNickname(); + + String getEmail(); + +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleUserService-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleUserService-template.java new file mode 100644 index 000000000..9b6e6a7bf --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/gae/security/GoogleUserService-template.java @@ -0,0 +1,6 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +public interface GoogleUserService +{ + GoogleUser findCurrentUser(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/locator/GwtServiceLocator-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/locator/GwtServiceLocator-template.java new file mode 100644 index 000000000..37d866e38 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/server/locator/GwtServiceLocator-template.java @@ -0,0 +1,19 @@ +package __TOP_LEVEL_PACKAGE__.server.locator; + +import javax.servlet.http.HttpServletRequest; + +import com.google.web.bindery.requestfactory.server.RequestFactoryServlet; +import com.google.web.bindery.requestfactory.shared.ServiceLocator; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +public class GwtServiceLocator implements ServiceLocator { + + HttpServletRequest request = RequestFactoryServlet.getThreadLocalRequest(); + ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext()); + + @Override + public Object getInstance(Class clazz) { + return context.getBean(clazz); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/GaeUser-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/GaeUser-template.java new file mode 100644 index 000000000..0da7a62ef --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/GaeUser-template.java @@ -0,0 +1,15 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import com.google.web.bindery.requestfactory.shared.ProxyForName; +import com.google.web.bindery.requestfactory.shared.ValueProxy; + +/** + * Client visible proxy of Google AppEngine User class. + */ +@ProxyForName("com.google.appengine.api.users.User") +public interface GaeUser extends ValueProxy { + + String getNickname(); + + String getEmail(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/GaeUserServiceRequest-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/GaeUserServiceRequest-template.java new file mode 100644 index 000000000..00c2ed79d --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/GaeUserServiceRequest-template.java @@ -0,0 +1,20 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +import __TOP_LEVEL_PACKAGE__.server.gae.UserServiceLocator; +import __TOP_LEVEL_PACKAGE__.server.gae.UserServiceWrapper; +import com.google.web.bindery.requestfactory.shared.Request; +import com.google.web.bindery.requestfactory.shared.RequestContext; +import com.google.web.bindery.requestfactory.shared.Service; + +/** + * Makes requests of the Google AppEngine UserService. + */ +@Service(value = UserServiceWrapper.class, locator = UserServiceLocator.class) +public interface GaeUserServiceRequest extends RequestContext { + + Request createLoginURL(String destinationURL); + + Request createLogoutURL(String destinationURL); + + Request getCurrentUser(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/MakesGaeRequests-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/MakesGaeRequests-template.java new file mode 100644 index 000000000..0cf712d7b --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/gae/MakesGaeRequests-template.java @@ -0,0 +1,13 @@ +package __TOP_LEVEL_PACKAGE__.__SEGMENT_PACKAGE__; + +/** + * Implemented by {@link com.google.web.bindery.requestfactory.shared.RequestFactory}s + * that vend AppEngine requests. + */ +public interface MakesGaeRequests { + + /** + * Return a request selector. + */ + GaeUserServiceRequest userServiceRequest(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/scaffold/ScaffoldRequestFactory-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/scaffold/ScaffoldRequestFactory-template.java new file mode 100644 index 000000000..c6343dc76 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/module/shared/scaffold/ScaffoldRequestFactory-template.java @@ -0,0 +1,17 @@ +package __TOP_LEVEL_PACKAGE__.shared.scaffold; + +import com.google.web.bindery.requestfactory.shared.LoggingRequest; +import com.google.web.bindery.requestfactory.shared.RequestFactory; + +/** + * The base request factory interface for this app. Add + * new custom request types here without fear of them + * being managed away by Roo. + */ +public interface ScaffoldRequestFactory extends RequestFactory { + + /** + * Return a GWT logging request. + */ + LoggingRequest loggingRequest(); +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ActivitiesMapper.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ActivitiesMapper.xtm new file mode 100644 index 000000000..5627f7f43 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ActivitiesMapper.xtm @@ -0,0 +1,58 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.place.shared.PlaceController; +import com.google.web.bindery.requestfactory.shared.EntityProxyId; +import com.google.web.bindery.requestfactory.shared.RequestContext; + +import {{=placePackage}}.ProxyPlace; +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Maps {@link ProxyPlace} instances to the {@link Activity} to run. + */ +public class {{=className}} { + private final {{=requestFactory}} factory; + private final PlaceController placeController; + + public {{=className}}({{=requestFactory}} factory, PlaceController placeController) { + this.factory = factory; + this.placeController = placeController; + } + + public Activity getActivity(ProxyPlace place) { + switch (place.getOperation()) { + case DETAILS: + return new {{=detailsActivity}}((EntityProxyId<{{=proxy}}>)place.getProxyId(), factory, + placeController, ScaffoldApp.isMobile() ? {{=mobileDetailsView}}.instance() : {{=desktopDetailsView}}.instance()); + + case EDIT: + return makeEditActivity(place); + + case CREATE: + return makeCreateActivity(); + } + + throw new IllegalArgumentException("Unknown operation " + place.getOperation()); + } + + @SuppressWarnings("unchecked") + private EntityProxyId<{{=proxy}}> coerceId(ProxyPlace place) { + return (EntityProxyId<{{=proxy}}>) place.getProxyId(); + } + + private Activity makeCreateActivity() { + {{=desktopEditView}}.instance().setCreating(true); + Activity activity = new {{=editActivity}}(null, factory, ScaffoldApp.isMobile() ? {{=mobileEditView}}.instance() : {{=desktopEditView}}.instance(), placeController); + + return new {{=editActivityWrapper}}(factory, ScaffoldApp.isMobile() ? {{=mobileEditView}}.instance() : {{=desktopEditView}}.instance(), activity, null); + } + + private Activity makeEditActivity(ProxyPlace place) { + {{=desktopEditView}}.instance().setCreating(false); + EntityProxyId<{{=proxy}}> proxyId = coerceId(place); + Activity activity = new {{=editActivity}}(proxyId, factory, ScaffoldApp.isMobile() ? {{=mobileEditView}}.instance() : {{=desktopEditView}}.instance(), placeController); + + return new {{=editActivityWrapper}}(factory, ScaffoldApp.isMobile() ? {{=mobileEditView}}.instance() : {{=desktopEditView}}.instance(), activity, proxyId); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationDetailsActivities.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationDetailsActivities.xtm new file mode 100644 index 000000000..3a0f71c48 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationDetailsActivities.xtm @@ -0,0 +1,37 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.activity.shared.ActivityMapper; +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceController; +import com.google.inject.Inject; + +import {{=placePackage}}.ProxyPlace; +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Instantiates detail activities. + */ +public class ApplicationDetailsActivities implements ActivityMapper { + private final {{=requestFactory}} requests; + private final PlaceController placeController; + + @Inject + public ApplicationDetailsActivities({{=requestFactory}} requestFactory, PlaceController placeController) { + this.requests = requestFactory; + this.placeController = placeController; + } + + public Activity getActivity(Place place) { + if (!(place instanceof ProxyPlace)) { + return null; + } + + final ProxyPlace proxyPlace = (ProxyPlace) place; + + return new ApplicationEntityTypesProcessor() {{{#entities}} + @Override +{{=entity}}{{/entities}} + }.process(proxyPlace.getProxyClass()); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationEntityTypesProcessor.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationEntityTypesProcessor.xtm new file mode 100644 index 000000000..8e66d2754 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationEntityTypesProcessor.xtm @@ -0,0 +1,116 @@ +package {{=packageName}}; + +import com.google.web.bindery.requestfactory.shared.EntityProxy; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +{{#imports}}import {{=import}}; +{{/imports}} + + +/** + * A helper class for dealing with proxy types. Subclass it and override the + * various handle methods for type specific handling of proxy objects or + * classes, then call {@link #process(Class)} or {@link #process(Object)}. + * Optionally use {#setResult} to set the return value of the {@link #process} + * call. + + *

    + * Use {@link #getAll} for a set of all proxy types. + * + * @param the type to filter to + */ +public abstract class {{=className}} { + + /** + * Return a set of all proxy types available to this application. + */ + public static Set> getAll() { + Set> rtn = new HashSet>(); +{{#proxys}} + rtn.add({{=proxy}}.class);{{/proxys}} + + return Collections.unmodifiableSet(rtn); + } + + private static void process({{=className}} processor, Class clazz) {{{#entities1}} +{{=entity}}{{/entities1}} + processor.handleNonProxy(null); + } + + private static void process({{=className}} processor, Object proxy) {{{#entities2}} +{{=entity}}{{/entities2}} + processor.handleNonProxy(proxy); + } + + private final T defaultValue; + private T result; + + /** + * Create an instance with a null default value. + */ + public {{=className}}() { + defaultValue = null; + } + + /** + * Create an instance with the given default value. + * + * @param the value that will be returned by {@link #process} if {@link #setResult} is not called. + */ + public {{=className}}(T defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Called if {@link #process} is called with a non-proxy object. This default + * implementation does nothing. + */ + public void handleNonProxy(Object object) { + } + +{{#entities3}}{{=entity}} +{{/entities3}} + /** + * Call the handle method of the appropriate type, with a null argument. Note + * that this will not work as expected on the class objects returned by the + * {@link #getClass()} method of a proxy object, due to limitations of GWT's + * metadata. It will only work with against class objects in the set returned + * by {@link #getAll()}, or returned by + * {@link com.google.web.bindery.requestfactory.shared.RequestFactory#getClass(Proxy)} + * or + * {@link com.google.web.bindery.requestfactory.shared.RequestFactory#getClass(String)}. + * + * @param clazz the proxy type to resolve + * @return the value provided via {@link #setResult}, or the default value + */ + public T process(Class clazz) { + setResult(defaultValue); + {{=className}}.process(this, clazz); + return result; + } + + /** + * Process a proxy object + * + * @param proxy the proxy to process + * @return the value provided via {@link #setResult}, or the default value + */ + public T process(Object proxy) { + setResult(defaultValue); + {{=className}}.process(this, proxy); + return result; + } + + /** + * Set the value to return from a call to {@link #process(Class)} or + * {@link #process(Object)}. + * + * @param result the value to return + */ + protected void setResult(T result) { + this.result = result; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationListPlaceRenderer.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationListPlaceRenderer.xtm new file mode 100644 index 000000000..d0766ae06 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationListPlaceRenderer.xtm @@ -0,0 +1,22 @@ +package {{=packageName}}; + +import com.google.gwt.text.shared.AbstractRenderer; + +import {{=placePackage}}.ProxyListPlace; +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Renders {@link ProxyListPlace}s for display to users. + */ +public class ApplicationListPlaceRenderer extends AbstractRenderer { + + public String render(ProxyListPlace object) { + return new ApplicationEntityTypesProcessor() { +{{#entities}} + @Override + public void handle{{=entitySimpleName}}({{=entityFullPath}} isNull) { + setResult("{{=entitySimpleName}}s"); + }{{/entities}} + }.process(object.getProxyClass()); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationMasterActivities.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationMasterActivities.xtm new file mode 100644 index 000000000..f0d5381e6 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationMasterActivities.xtm @@ -0,0 +1,42 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.activity.shared.ActivityMapper; +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceController; +import com.google.inject.Inject; + +import {{=placePackage}}.ProxyListPlace; +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Instantiates master activities. + */ +public final class ApplicationMasterActivities implements ActivityMapper { + private final {{=requestFactory}} requests; + private final PlaceController placeController; + + @Inject + public ApplicationMasterActivities({{=requestFactory}} requests, PlaceController placeController) { + this.requests = requests; + this.placeController = placeController; + } + + public Activity getActivity(Place place) { + if (!(place instanceof ProxyListPlace)) { + return null; + } + + ProxyListPlace listPlace = (ProxyListPlace) place; + + return new ApplicationEntityTypesProcessor() {{{#entities}} + @Override + public void handle{{=entitySimpleName}}({{=entityFullPath}} isNull) { + setResult(new {{=entitySimpleName}}ListActivity(requests, + ScaffoldApp.isMobile() ? {{=entitySimpleName}}MobileListView.instance() : {{=entitySimpleName}}DesktopListView.instance(), + placeController)); + } + {{/entities}} + }.process(listPlace.getProxyClass()); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationRequestFactory.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationRequestFactory.xtm new file mode 100644 index 000000000..330dba058 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ApplicationRequestFactory.xtm @@ -0,0 +1,13 @@ +package {{=packageName}}; + +import {{=sharedScaffoldPackage}}.ScaffoldRequestFactory; +{{#gae}}import {{=sharedGaePackage}}.MakesGaeRequests; +{{/gae}} + +{{#imports}}import {{=import}}; +{{/imports}} + +public interface ApplicationRequestFactory extends ScaffoldRequestFactory{{#gae}}, MakesGaeRequests{{/gae}} { +{{#entities}} +{{=entity}}{{/entities}} +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/CollectionEditor.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/CollectionEditor.xtm new file mode 100644 index 000000000..845be4a78 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/CollectionEditor.xtm @@ -0,0 +1,229 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.editor.client.EditorDelegate; +import com.google.gwt.editor.client.LeafValueEditor; +import com.google.gwt.editor.client.ValueAwareEditor; +import com.google.gwt.editor.client.adapters.EditorSource; +import com.google.gwt.editor.client.adapters.ListEditor; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.shared.EventBus; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.text.shared.Renderer; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.client.ui.Widget; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import {{=scaffoldUiPackage}}.CollectionRenderer; +{{#imports}}import {{=import}}; +{{/imports}} + +public class {{=boundCollectionType}}{{=collectionType}}Editor extends Composite implements ValueAwareEditor<{{=collectionType}}<{{=boundCollectionType}}>>, LeafValueEditor<{{=collectionType}}<{{=boundCollectionType}}>> { + + @UiField + FlowPanel container; + + private Renderer<{{=boundCollectionType}}> renderer = new Renderer<{{=boundCollectionType}}>() { + @Override + public String render({{=boundCollectionType}} o) { + return o == null ? "" : String.valueOf(o); + } + + @Override + public void render({{=boundCollectionType}} o, Appendable appendable) throws IOException { + appendable.append(render(o)); + } + }; + + @UiField(provided = true) + @Ignore + ValueListBox<{{=boundCollectionType}}> picker = new ValueListBox<{{=boundCollectionType}}>(renderer); + + @UiField + Button add; + + @UiField + HTMLPanel editorPanel; + + @UiField + Button clickToEdit; + + @UiField + HTMLPanel viewPanel; + + @UiField + Label viewLabel; + + @UiField + Style style; + + boolean editing = false; + + private {{=collectionType}}<{{=boundCollectionType}}> values; + + private List<{{=boundCollectionType}}> displayedList; + + public CollectionEditor() { + initWidget(GWT.create(Binder.class).createAndBindUi(this)); + Driver driver = GWT.create(Driver.class); + ListEditor<{{=boundCollectionType}}, NameLabel> editor = ListEditor.of(new NameLabelSource()); + driver.initialize(editor); + driver.display(new ArrayList<{{=boundCollectionType}}>()); + displayedList = editor.getList(); + editing = false; + } + + @UiHandler("add") + public void addClicked(ClickEvent e) { + if (picker.getValue() == null || displayedList.contains(picker.getValue())) { + return; + } + displayedList.add(picker.getValue()); + viewLabel.setText(makeFlatList(displayedList)); + } + + @UiHandler("clickToEdit") + public void clickToEditClicked(ClickEvent e) { + toggleEditorMode(); + } + + + interface Binder extends UiBinder { + } + + interface Driver extends RequestFactoryEditorDriver, ListEditor<{{=boundCollectionType}}, NameLabel>> { + } + + public void onLoad() { + makeEditable(false); + } + + public void setAcceptableValues(Collection<{{=boundCollectionType}}> values) { + picker.setAcceptableValues(values); + } + + @Override + public void setDelegate(EditorDelegate<{{=collectionType}}<{{=boundCollectionType}}>> editorDelegate) { + } + + @Override + public void onPropertyChange(java.lang.String... strings) { + } + + @Override + public void flush() { + } + + @Override + public void setValue({{=collectionType}}<{{=boundCollectionType}}> values) { + + this.values = values; + makeEditable(editing = false); + if (displayedList != null) { + displayedList.clear(); + if (values != null) { + for ({{=boundCollectionType}} e : values) { + displayedList.add(e); + } + } + } + viewLabel.setText(makeFlatList(values)); + } + + private void makeEditable(boolean editable) { + if (editable) { + editorPanel.setStylePrimaryName(style.editorPanelVisible()); + viewPanel.setStylePrimaryName(style.viewPanelHidden()); + clickToEdit.setText("Done"); + } else { + editorPanel.setStylePrimaryName(style.editorPanelHidden()); + viewPanel.setStylePrimaryName(style.viewPanelVisible()); + clickToEdit.setText("Edit"); + } + } + + private java.lang.String makeFlatList(Collection values) { + return CollectionRenderer.of(renderer).render(values); + } + + private void toggleEditorMode() { + editing = !editing; + makeEditable(editing); + } + + @Override + public {{=collectionType}}<{{=boundCollectionType}}> getValue() { + if (values == null && displayedList.size() == 0) { + return null; + } + return new {{=collectionTypeImpl}}<{{=boundCollectionType}}>(displayedList); + } + + interface Style extends CssResource { + + java.lang.String editorPanelHidden(); + + java.lang.String editorPanelVisible(); + + java.lang.String viewPanelHidden(); + + java.lang.String viewPanelVisible(); + } + + class NameLabel extends Composite implements LeafValueEditor<{{=boundCollectionType}}> { + + @Ignore + final Label displayNameEditor = new Label(); + + public NameLabel() { + this(null); + } + + public NameLabel(EventBus eventBus) { + initWidget(displayNameEditor); + } + + @Override + public void setValue({{=boundCollectionType}} o) { + displayNameEditor.setText(String.valueOf(o)); + } + + @Override + public {{=boundCollectionType}} getValue() { + return {{=boundCollectionType}}.valueOf(displayNameEditor.getText()); + } + } + + private class NameLabelSource extends EditorSource { + + @Override + public NameLabel create(int index) { + NameLabel label = new NameLabel(); + container.insert(label, index); + return label; + } + + @Override + public void dispose(NameLabel subEditor) { + subEditor.removeFromParent(); + } + + @Override + public void setIndex(NameLabel editor, int index) { + container.insert(editor, index); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/CollectionEditorUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/CollectionEditorUiXml.xtm new file mode 100644 index 000000000..717271657 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/CollectionEditorUiXml.xtm @@ -0,0 +1,34 @@ + + + + .editorPanelVisible { + border: thin solid black; + margin: 2px; + overflow: hidden; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + } + + .viewPanelVisible { + } + + .editorPanelHidden, .viewPanelHidden { + display: none + } + + + Edit + + + + + Entry to Add: + + Add +
    + +
    +
    +
    \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopDetailsView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopDetailsView.xtm new file mode 100644 index 000000000..761cf809e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopDetailsView.xtm @@ -0,0 +1,86 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.HasClickHandlers; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Widget; + +import {{=placePackage}}.ProxyListView; +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Details view for {{=proxy}}. + */ +public class {{=className}} extends Composite implements {{=detailsView}} { + interface Binder extends UiBinder {} + + private static final Binder BINDER = GWT.create(Binder.class); + + private static {{=className}} instance; + + {{=proxy}} proxy; + {{#fields}} + @UiField SpanElement {{=field}};{{/fields}} + + @UiField SpanElement displayRenderer; + @UiField HasClickHandlers edit; + @UiField HasClickHandlers delete; + + private Delegate delegate; + + public static {{=className}} instance() { + if (instance == null) { + instance = new {{=className}}(); + } + + return instance; + } + + public {{=className}}() { + initWidget(BINDER.createAndBindUi(this)); + } + + public Widget asWidget() { + return this; + } + + public boolean confirm(String msg) { + return Window.confirm(msg); + } + + public {{=proxy}} getValue() { + return proxy; + } + + @UiHandler("delete") + public void onDeleteClicked(ClickEvent e) { + delegate.deleteClicked(); + } + + @UiHandler("edit") + public void onEditClicked(ClickEvent e) { + delegate.editClicked(); + } + + public void setDelegate(Delegate delegate) { + this.delegate = delegate; + } + + public void setValue({{=proxy}} proxy) { + this.proxy = proxy; + {{#managedProperties}} + {{=prop}}.setInnerText(proxy.{{=propGetter}}() == null ? "" : {{=propFormatter}}(proxy.{{=propGetter}}())); + {{/managedProperties}} + + displayRenderer.setInnerText({{=proxyRenderer}}.instance().render(proxy)); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopDetailsViewUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopDetailsViewUiXml.xtm new file mode 100644 index 000000000..2015c0b39 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopDetailsViewUiXml.xtm @@ -0,0 +1,46 @@ + + + + + .fields { + margin-top: 0.5em; + margin-left: 1em; + } + .label { + min-height: 25px; + font-weight: bold; + } + .button { + margin-right: 1em; + } + .bar { + margin-left: 1em; + } + .header { + margin-left: 1em; + color: #4B4A4A; + text-shadow: #ddf 1px 1px 0; + margin-bottom: 0; + } + .underline { + border-bottom: 2px solid #6F7277; + } + + +

    +

    +
    + + + {{#properties}} + + {{/properties}} +
    {{=propReadable}}:
    + +
    + Edit + Delete +
    + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopEditView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopEditView.xtm new file mode 100644 index 000000000..49bcb4078 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopEditView.xtm @@ -0,0 +1,122 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.editor.client.EditorError; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.shared.EventBus; +import com.google.web.bindery.requestfactory.shared.RequestFactory; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.DoubleBox; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.IntegerBox; +import com.google.gwt.user.client.ui.LongBox; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.datepicker.client.DateBox; +import {{=scaffoldUiPackage}}.*; + +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.Collection; +import java.util.List; + +/** + * Edit view for {{=proxy}}. + */ +public class {{=className}} extends Composite implements {{=editActivityWrapper}}.View<{{=className}}> { + + interface Binder extends UiBinder { + } + + interface Driver extends + RequestFactoryEditorDriver<{{=proxy}}, {{=className}}> { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + private static {{=className}} instance; + + {{#editViewProps}} + {{=prop}};{{/editViewProps}} + + @UiField Button cancel; + @UiField Button save; + @UiField DivElement errors; + + @UiField Element editTitle; + @UiField Element createTitle; + + private Delegate delegate; + + public static {{=className}} instance() { + if (instance == null) { + instance = new {{=className}}(); + } + + //Jim + return instance; + } + + public {{=className}}() { + initWidget(BINDER.createAndBindUi(this)); + } + + @Override + public RequestFactoryEditorDriver<{{=proxy}}, {{=className}}> createEditorDriver() { + RequestFactoryEditorDriver<{{=proxy}}, {{=className}}> driver = GWT.create(Driver.class); + driver.initialize(this); + return driver; + } + +{{#setEnumValuePickers}}{{=setValuePicker}}{{/setEnumValuePickers}} +{{#setProxyValuePickers}}{{=setValuePicker}}{{/setProxyValuePickers}} + public void setCreating(boolean creating) { + if (creating) { + editTitle.getStyle().setDisplay(Display.NONE); + createTitle.getStyle().clearDisplay(); + } else { + editTitle.getStyle().clearDisplay(); + createTitle.getStyle().setDisplay(Display.NONE); + } + } + + public void setDelegate(Delegate delegate) { + this.delegate = delegate; + } + + public void setEnabled(boolean enabled) { + save.setEnabled(enabled); + } + + public void showErrors(List errors) { + SafeHtmlBuilder b = new SafeHtmlBuilder(); + for (EditorError error : errors) { + b.appendEscaped(error.getPath()).appendEscaped(": "); + b.appendEscaped(error.getMessage()).appendHtmlConstant("
    "); + } + this.errors.setInnerHTML(b.toSafeHtml().asString()); + } + + @UiHandler("cancel") + void onCancel(ClickEvent event) { + //Jimbo + delegate.cancelClicked(); + } + + @UiHandler("save") + void onSave(ClickEvent event) { + delegate.saveClicked(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopEditViewUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopEditViewUiXml.xtm new file mode 100644 index 000000000..a4cafb1e8 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopEditViewUiXml.xtm @@ -0,0 +1,59 @@ + + + + + .errors { + padding-left: 0.5em; + background-color: red; + } + .fields { + margin-top: 0.5em; + margin-left: 1em; + } + .label { + min-height: 25px; + font-weight: bold; + } + .button { + margin-right: 1em; + } + .bar { + margin-left: 1em; + } + .header { + margin-left: 1em; + color: #4B4A4A; + text-shadow: #ddf 1px 1px 0; + margin-bottom: 0; + } + .underline { + border-bottom: 2px solid #6F7277; + } + + + +
    +

    + Edit {{name}} +

    +

    New {{=name}}

    +
    +
    + + {{#editableProperties}} + + + + + {{/editableProperties}} +
    {{=propReadable}}:
    <{{=propBinder}} ui:field='{{=prop}}'/>
    +
    + Save + Cancel +
    +
    +
    diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopListView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopListView.xtm new file mode 100644 index 000000000..196dbef5e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopListView.xtm @@ -0,0 +1,69 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.text.client.DateTimeFormatRenderer; +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.Renderer; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HTMLPanel; + +import {{=placePackage}}.AbstractProxyListView; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.HashSet; +import java.util.Set; + +/** + * {@link AbstractProxyListView} specialized to {@link {{=name}}Key}} values. + */ +public class {{=className}} extends AbstractProxyListView<{{=proxy}}> { + interface Binder extends UiBinder { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + private static {{=className}} instance; + + @UiField CellTable<{{=proxy}}> table; + @UiField Button newButton; + + private Set paths = new HashSet(); + + public static {{=className}} instance() { + if (instance == null) { + instance = new {{=className}}(); + } + + return instance; + } + + public {{=className}}() { + init(BINDER.createAndBindUi(this), table, newButton); + table.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED); + init(); + } + + public void init() { + {{#properties}} + paths.add("{{=prop}}"); + table.addColumn(new TextColumn<{{=proxy}}>() { + Renderer<{{=propType}}> renderer = {{=propRenderer}}; + + @Override + public String getValue({{=proxy}} object) { + return renderer.render(object.{{=propGetter}}()); + } + }, "{{=propReadable}}");{{/properties}} + } + public String[] getPaths() { + return paths.toArray(new String[paths.size()]); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopListViewUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopListViewUiXml.xtm new file mode 100644 index 000000000..7a0ac6c5e --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DesktopListViewUiXml.xtm @@ -0,0 +1,57 @@ + + + + + + .controls { + position: absolute; + left:0; + right:0; + top:3px; + height: 2em; + margin-left: 15px; + } + @sprite .createButton { + gwt-image: 'createButton'; + border: 0; + margin-top: 5px; + width: 12em; + font-size: 1em; + cursor: pointer; + text-align: left; + padding-left: 22px; + } + .controls table { + position:absolute; + right:0; + top:0; + } + .controls button { + display:inline; + } + .listView { + position: relative; + } + .listView > table { + table-layout:fixed; + } + .listView > table td { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + cursor: pointer; + } + + + + +
    + +
    + Create {{=name}} + +
    +
    +
    diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DetailsActivity.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DetailsActivity.xtm new file mode 100644 index 000000000..947868715 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DetailsActivity.xtm @@ -0,0 +1,113 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.AbstractActivity; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceController; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.web.bindery.requestfactory.shared.EntityProxyId; +import com.google.web.bindery.requestfactory.shared.Receiver; +import com.google.web.bindery.requestfactory.shared.Request; +import com.google.gwt.user.client.ui.AcceptsOneWidget; + +import {{=placePackage}}.ProxyPlace; +import {{=placePackage}}.ProxyListPlace; +import {{=placePackage}}.ProxyPlace.Operation; +{{#imports}}import {{=import}}; +{{/imports}} +import java.util.Set; + +/** + * An {@link com.google.gwt.activity.shared.Activity Activity} that requests and + * displays detailed information on a given {{=proxy}}. + */ +public class {{=className}} extends AbstractActivity implements {{=detailsView}}.Delegate, {{=isScaffoldMobileActivity}} { + + private final ApplicationRequestFactory requests; + private final PlaceController placeController; + private final {{=detailsView}} view; + private final EntityProxyId<{{=proxy}}> proxyId; + private AcceptsOneWidget display; + + public {{=className}}(EntityProxyId<{{=proxy}}> proxyId, ApplicationRequestFactory requests, PlaceController placeController, {{=detailsView}} view) { + this.placeController = placeController; + this.proxyId = proxyId; + this.requests = requests; + view.setDelegate(this); + this.view = view; + } + + public void deleteClicked() { + if (!view.confirm("Really delete this entry? You cannot undo this change.")) { + return; + } + + requests.{{=nameUncapitalized}}Request().{{=removeMethodSignature}}(view.getValue()).fire(new Receiver() { + public void onSuccess(Void ignore) { + if (display == null) { + // This activity is dead + return; + } + + // Go back to the previous place. + placeController.goTo(getBackButtonPlace()); + } + }); + } + + public void editClicked() { + placeController.goTo(getEditButtonPlace()); + } + + public Place getBackButtonPlace() { + return new ProxyListPlace({{=proxy}}.class); + } + + public String getBackButtonText() { + return "Back"; + } + + public Place getEditButtonPlace() { + return new ProxyPlace(view.getValue().stableId(), Operation.EDIT); + } + + public String getTitleText() { + return "View {{=name}}"; + } + + public boolean hasEditButton() { + return true; + } + + public void onCancel() { + onStop(); + } + + public void onStop() { + display = null; + } + + public void start(AcceptsOneWidget displayIn, EventBus eventBus) { + this.display = displayIn; + Receiver callback = new Receiver() { + public void onSuccess(EntityProxy proxy) { + if (proxy == null) { + // Deleted entity, bad bookmark, that kind of thing + placeController.goTo(getBackButtonPlace()); + return; + } + if (display == null) { + return; + } + view.setValue(({{=proxy}}) proxy); + display.setWidget(view); + } + }; + + find(callback); + } + + private void find(Receiver callback) { + requests.find(proxyId).with({{=proxyFields}}).fire(callback); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DetailsView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DetailsView.xtm new file mode 100644 index 000000000..91cdb6deb --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/DetailsView.xtm @@ -0,0 +1,17 @@ +package {{=packageName}}; + +import {{=placePackage}}.ProxyDetailsView; +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Details view for {{=proxy}}. + */ +public interface {{=className}} extends ProxyDetailsView<{{=proxy}}> +{ + void setDelegate(Delegate delegate); + + interface Delegate extends ProxyDetailsView.Delegate + { + + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditActivity.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditActivity.xtm new file mode 100755 index 000000000..95eedb9f6 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditActivity.xtm @@ -0,0 +1,66 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceController; +import com.google.web.bindery.requestfactory.shared.EntityProxyId; +import com.google.web.bindery.requestfactory.shared.Receiver; +import com.google.web.bindery.requestfactory.shared.Request; +import com.google.web.bindery.requestfactory.shared.RequestContext; +import com.google.gwt.user.client.ui.AcceptsOneWidget; + +import {{=placePackage}}.AbstractProxyEditActivity; + +import {{=placePackage}}.ProxyListPlace; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.ArrayList; +import java.util.List; + +/** + * Wraps {{=proxy}} edit and create activities to manage extra portions of their + * views, like value pickers. + */ +public class {{=className}} extends AbstractProxyEditActivity<{{=proxy}}> implements {{=editView}}.Delegate +{ + private final {{=editView}} view; + private final {{=request}} request; + + public {{=className}}(EntityProxyId<{{=proxy}}> proxyId, ApplicationRequestFactory factory, {{=editView}} view, PlaceController placeController) + { + super(proxyId, factory, placeController); + this.view = view; + this.request = factory.{{=nameUncapitalized}}Request(); + } + + @Override + protected {{=editView}} getView() + { + return view; + } + + @Override + public void start(AcceptsOneWidget display, EventBus eventBus) + { + this.view.setDelegate(this); + super.start(display, eventBus); + } + + @Override + protected {{=proxy}} createProxy() + { + return request.create({{=proxy}}.class); + } + + @Override + protected RequestContext createSaveRequest({{=proxy}} proxy) + { + request.{{=persistMethodSignature}}(proxy); + return request; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditActivityWrapper.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditActivityWrapper.xtm new file mode 100644 index 000000000..5f2060fd8 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditActivityWrapper.xtm @@ -0,0 +1,113 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.place.shared.Place; +import com.google.web.bindery.requestfactory.shared.EntityProxyId; +import com.google.web.bindery.requestfactory.shared.Receiver; +import com.google.gwt.user.client.ui.AcceptsOneWidget; + +import {{=placePackage}}.ProxyPlace; +import {{=placePackage}}.ProxyListPlace; +import {{=placePackage}}.ProxyEditView; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.ArrayList; +import java.util.List; + +/** + * Wraps {{=proxy}} edit and create activities to manage extra portions of their + * views, like value pickers. + */ +public class {{=className}} implements Activity, {{=isScaffoldMobileActivity}} { + + /** + * The view used by this activity. + * + * @param the type of the ProxyEditView + */ + public interface View> extends {{=editView}} + { + {{#setEnumValuePickers}} + void {{=setValuePickerName}}(Collection<{{=valueType}}> values); + {{/setEnumValuePickers}} + {{#setProxyValuePickers}} + void {{=setValuePickerName}}(Collection<{{=valueType}}> values); + {{/setProxyValuePickers}} + } + + private final EntityProxyId<{{=proxy}}> proxyId; + private final ApplicationRequestFactory requests; + private final View view; + private final Activity wrapped; + + public {{=className}}(ApplicationRequestFactory requests, + View view, Activity activity, EntityProxyId<{{=proxy}}> proxyId) { + this.requests = requests; + this.view = view; + this.wrapped = activity; + this.proxyId = proxyId; + } + + public Place getBackButtonPlace() { + return (proxyId == null) ? new ProxyListPlace({{=proxy}}.class) : + new ProxyPlace(proxyId, ProxyPlace.Operation.DETAILS); + } + + public String getBackButtonText() { + return "Cancel"; + } + + public Place getEditButtonPlace() { + return null; + } + + public String getTitleText() { + return (proxyId == null) ? "New {{=name}}" : "Edit {{=name}}"; + } + + public boolean hasEditButton() { + return false; + } + + @Override + public String mayStop() { + return wrapped.mayStop(); + } + + @Override + public void onCancel() { + wrapped.onCancel(); + } + + @Override + public void onStop() { + wrapped.onStop(); + } + + @Override + public void start(AcceptsOneWidget display, EventBus eventBus) + {{{#setEnumValuePickers}} + view.{{=setValuePickerName}}(Arrays.asList({{=valueType}}.values())); + {{/setEnumValuePickers}} + + {{#setProxyValuePickers}} + view.{{=setValuePickerName}}(Collections.<{{=valueType}}> emptyList()); + requests.{{=requestInterface}}().{{=findMethod}}.with( + {{=rendererType}}.instance().getPaths()).fire( + new Receiver>() { + public void onSuccess(List<{{=valueType}}> response) { + List<{{=valueType}}> values = new ArrayList<{{=valueType}}>(); + values.add(null); + values.addAll(response); + view.{{=setValuePickerName}}(values); + } + }); + {{/setProxyValuePickers}} + wrapped.start(display, eventBus); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditRenderer.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditRenderer.xtm new file mode 100644 index 000000000..fedbfa38d --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditRenderer.xtm @@ -0,0 +1,32 @@ +package {{=packageName}}; + +import com.google.web.bindery.requestfactory.gwt.ui.client.ProxyRenderer; + +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Renders {@link {{=proxy}} for display to the user. Requires the + * {{=displayFields}} properties to have been fetched. + */ +public class {{=className}} extends ProxyRenderer<{{=proxy}}> { + private static {{=className}} INSTANCE; + + public static {{=className}} instance() { + if (INSTANCE == null) { + INSTANCE = new {{=className}}(); + } + + return INSTANCE; + } + + protected {{=className}}() { + super(new String[] {"{{=primaryProp}}"}); + } + + public String render({{=proxy}} object) { + if (object == null) { + return ""; + } + return object.{{=primaryPropGetter}}() + " (" + object.{{=primaryPropGetter}}() + ")"; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditView.xtm new file mode 100644 index 000000000..9e93f824d --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/EditView.xtm @@ -0,0 +1,20 @@ +package {{=packageName}}; + +import {{=placePackage}}.ProxyEditView; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.Collection; +import java.util.List; + +/** + * Edit view for {{=proxy}}. + */ +public interface {{=className}}> extends ProxyEditView<{{=proxy}}, V> { + + void setDelegate(Delegate delegate); + + interface Delegate extends ProxyEditView.Delegate { + + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListActivity.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListActivity.xtm new file mode 100644 index 000000000..578590c3b --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListActivity.xtm @@ -0,0 +1,56 @@ +package {{=packageName}}; + +import com.google.gwt.place.shared.Place; +import com.google.gwt.place.shared.PlaceController; +import com.google.web.bindery.requestfactory.shared.Receiver; +import com.google.web.bindery.requestfactory.shared.Request; +import com.google.gwt.view.client.Range; + +import {{=placePackage}}.AbstractProxyListActivity; +import {{=placePackage}}.ProxyListView; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.List; + +/** + * Activity that requests and displays all {{=proxy}}. + */ +public class {{=className}} extends AbstractProxyListActivity<{{=proxy}}> implements {{=isScaffoldMobileActivity}} { + + private final {{=requestFactory}} requests; + + public {{=className}}({{=requestFactory}} requests, + ProxyListView<{{=proxy}}> view, PlaceController placeController) { + super(placeController, view, {{=proxy}}.class); + this.requests = requests; + } + + public Place getBackButtonPlace() { + return ScaffoldMobileApp.ROOT_PLACE; + } + + public String getBackButtonText() { + return "Entities"; + } + + public Place getEditButtonPlace() { + return null; + } + + public String getTitleText() { + return "{{=pluralName}}"; + } + + public boolean hasEditButton() { + return false; + } + + protected Request> createRangeRequest(Range range) { + return requests.{{=nameUncapitalized}}Request().find{{=name}}Entries(range.getStart(), range.getLength()); + } + + protected void fireCountRequest(Receiver callback) { + requests.{{=nameUncapitalized}}Request().{{=countEntitiesMethod}}().fire(callback); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListEditor.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListEditor.xtm new file mode 100644 index 000000000..1b4e11a61 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListEditor.xtm @@ -0,0 +1,253 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.editor.client.Editor; +import com.google.gwt.editor.client.EditorDelegate; +import com.google.gwt.editor.client.LeafValueEditor; +import com.google.gwt.editor.client.ValueAwareEditor; +import com.google.gwt.editor.client.adapters.EditorSource; +import com.google.gwt.editor.client.adapters.ListEditor; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.client.ui.Widget; + +import {{=scaffoldUiPackage}}.CollectionRenderer; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Displays an editor for a List of {{=proxy}} objects. + */ +public class {{=className}} extends Composite implements ValueAwareEditor>, LeafValueEditor> { + + interface Binder extends UiBinder { + + } + + interface Driver extends RequestFactoryEditorDriver, // + ListEditor<{{=proxy}}, NameLabel>> { + + } + + class NameLabel extends Composite implements LeafValueEditor<{{=proxy}}> { + + final Label {{=primaryProp}}Editor = new Label(); + private {{=proxy}} proxy = null; + + public NameLabel() { + this(null); + } + + public NameLabel(final EventBus eventBus) { + initWidget({{=primaryProp}}Editor); + } + + public void setValue({{=proxy}} proxy) { + this.proxy = proxy; + {{=primaryProp}}Editor.setText({{=proxyRenderer}}.instance().render(proxy)); + } + + @Override + public {{=proxy}} getValue() { + return proxy; + } + } + + interface Style extends CssResource { + + String editorPanelHidden(); + + String editorPanelVisible(); + + String viewPanelHidden(); + + String viewPanelVisible(); + } + + /** + * This is used by a ListEditor. + */ + private class NameLabelSource extends EditorSource { + + @Override + public NameLabel create(int index) { + NameLabel label = new NameLabel(); + container.insert(label, index); + return label; + } + + @Override + public void dispose(NameLabel subEditor) { + subEditor.removeFromParent(); + } + + @Override + public void setIndex(NameLabel editor, int index) { + container.insert(editor, index); + } + } + + @UiField + FlowPanel container; + + @UiField(provided = true) + @Editor.Ignore + ValueListBox<{{=proxy}}> picker + = new ValueListBox<{{=proxy}}>( + {{=proxyRenderer}}.instance(), + new com.google.web.bindery.requestfactory.gwt.ui.client.EntityProxyKeyProvider<{{=proxy}}>()); + + ; + + @UiField + Button add; + + @UiField + HTMLPanel editorPanel; + + @UiField + Button clickToEdit; + + @UiField + HTMLPanel viewPanel; + + @UiField + Label viewLabel; + + @UiField + Style style; + + boolean editing = false; + + private List<{{=proxy}}> values; + + private final List<{{=proxy}}> displayedList; + + public {{=className}}() { + + // Create the UI + initWidget(GWT.create(Binder.class).createAndBindUi(this)); + + // Create the driver which manages the data-bound widgets + Driver driver = GWT.create(Driver.class); + + // Use a ListEditor that uses our NameLabelSource + ListEditor<{{=proxy}}, NameLabel> listEditor = ListEditor.of(new NameLabelSource()); + + // Configure the driver + driver.initialize(listEditor); + + /* + * Notice the backing list is essentially anonymous. + */ + driver.display(new ArrayList<{{=proxy}}>()); + + // Modifying this list triggers widget creation and destruction + displayedList = listEditor.getList(); + + editing = false; + } + + @UiHandler("add") + public void addClicked(ClickEvent e) { + if (picker.getValue() == null){ + return; + } + for ({{=proxy}} proxy : displayedList) { + if (proxy.{{=primaryPropGetter}}().equals(picker.getValue().{{=primaryPropGetter}}())) { + return; + } + } + displayedList.add(picker.getValue()); + viewLabel.setText(makeFlatList(displayedList)); + } + + @UiHandler("clickToEdit") + public void clickToEditClicked(ClickEvent e) { + toggleEditorMode(); + } + + @Override + public void flush() { + } + + @Override + public List<{{=proxy}}> getValue() { + if (values == null && displayedList.size() == 0) { + return null; + } + return new ArrayList<{{=proxy}}>(displayedList); + } + + public void onLoad() { + makeEditable(false); + } + + @Override + public void onPropertyChange(String... strings) { + } + + public void setAcceptableValues(Collection<{{=proxy}}> proxies) { + picker.setAcceptableValues(proxies); + } + + @Override + public void setDelegate( + EditorDelegate> editorDelegate) { + } + + @Override + public void setValue(List<{{=proxy}}> values) { + this.values = values; + makeEditable(editing = false); + if (displayedList != null) { + displayedList.clear(); + if (values != null) { + for ({{=proxy}} e : values) { + displayedList.add(e); + } + } + } + viewLabel.setText(makeFlatList(values)); + } + + private void makeEditable(boolean editable) { + if (editable) { + editorPanel.setStylePrimaryName(style.editorPanelVisible()); + viewPanel.setStylePrimaryName(style.viewPanelHidden()); + clickToEdit.setText("Done"); + } else { + editorPanel.setStylePrimaryName(style.editorPanelHidden()); + viewPanel.setStylePrimaryName(style.viewPanelVisible()); + clickToEdit.setText("Edit"); + } + } + + private String makeFlatList(Collection<{{=proxy}}> values) { + return CollectionRenderer.of({{=proxyRenderer}}.instance()) + .render(values); + } + + private void toggleEditorMode() { + editing = !editing; + makeEditable(editing); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListEditorUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListEditorUiXml.xtm new file mode 100644 index 000000000..f4d20a786 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ListEditorUiXml.xtm @@ -0,0 +1,32 @@ + + + + .editorPanelVisible { + border: thin solid black; + margin: 2px; + overflow: hidden; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + } + + .viewPanelVisible { + } + + .editorPanelHidden, .viewPanelHidden { + display: none + } + + + Edit + + + + + Entry to Add: + Add
    + +
    +
    +
    \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileDetailsView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileDetailsView.xtm new file mode 100644 index 000000000..5f3fa96f5 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileDetailsView.xtm @@ -0,0 +1,72 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.HasClickHandlers; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Widget; + +{{#imports}}import {{=import}}; +{{/imports}} +/** + * Details view for {{=name}} proxys. + */ +public class {{=className}} extends Composite implements {{=detailsView}} { + interface Binder extends UiBinder {} + + private static final Binder BINDER = GWT.create(Binder.class); + + private static {{=className}} instance; + + public static {{=className}} instance() { + if (instance == null) { + instance = new {{=className}}(); + } + return instance; + } + + {{=proxy}} proxy; + {{#fields}} + @UiField Element {{=field}};{{/fields}} + @UiField HasClickHandlers delete; + + private Delegate delegate; + + public {{=className}}() { + initWidget(BINDER.createAndBindUi(this)); + } + + public Widget asWidget() { + return this; + } + + public boolean confirm(String msg) { + return Window.confirm(msg); + } + + public {{=proxy}} getValue() { + return proxy; + } + + @UiHandler("delete") + public void onDeleteClicked(ClickEvent e) { + delegate.deleteClicked(); + } + + public void setDelegate(Delegate delegate) { + this.delegate = delegate; + } + + public void setValue({{=proxy}} proxy) { + this.proxy = proxy;{{#managedProperties}} + {{=prop}}.setInnerText(proxy.{{=propGetter}}() == null ? "" : {{=propFormatter}}(proxy.{{=propGetter}}()));{{/managedProperties}} + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileDetailsViewUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileDetailsViewUiXml.xtm new file mode 100644 index 000000000..fe63f492f --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileDetailsViewUiXml.xtm @@ -0,0 +1,23 @@ + + + + .outer { + padding: 10px; + } + .label { + font-weight: bold; + } + .value { + padding: 4px 0px 15px 15px; + } + + + + {{#properties}} +
    {{=propReadable}}
    + {{/properties}} + + Delete +
    +
    diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileEditView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileEditView.xtm new file mode 100644 index 000000000..6c24e72e5 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileEditView.xtm @@ -0,0 +1,107 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.editor.client.EditorError; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.shared.EventBus; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.web.bindery.requestfactory.shared.RequestFactory; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.DoubleBox; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.IntegerBox; +import com.google.gwt.user.client.ui.LongBox; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.datepicker.client.DateBox; +import {{=scaffoldUiPackage}}.*; + +import {{=placePackage}}.ProxyEditView; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.Collection; +import java.util.List; + +/** + * Edit view for {{=name}} proxys. + */ +public class {{=className}} extends Composite implements {{=editActivityWrapper}}.View<{{=className}}> { + + interface Binder extends UiBinder { + } + + interface Driver extends + RequestFactoryEditorDriver<{{=proxy}}, {{=className}}> { + } + + private static final Binder BINDER = GWT.create(Binder.class); + + private static {{=className}} instance; + + public static {{=className}} instance() { + if (instance == null) { + instance = new {{=className}}(); + } + return instance; + } + + {{#editViewProps}} + {{=prop}};{{/editViewProps}} + @UiField Button save; + @UiField DivElement errors; + + private Delegate delegate; + + public {{=className}}() { + initWidget(BINDER.createAndBindUi(this)); + } + + @Override + public RequestFactoryEditorDriver<{{=proxy}}, {{=className}}> createEditorDriver() { + RequestFactoryEditorDriver<{{=proxy}}, {{=className}}> driver = GWT.create(Driver.class); + driver.initialize(this); + return driver; + } + + {{#setEnumValuePickers}} + {{=setValuePicker}}{{/setEnumValuePickers}} + + {{#setProxyValuePickers}} + {{=setValuePicker}}{{/setProxyValuePickers}} + + public void setCreating(boolean creating) { + } + + public void setDelegate(Delegate delegate) { + this.delegate = delegate; + } + + public void setEnabled(boolean enabled) { + save.setEnabled(enabled); + } + + public void showErrors(List errors) { + SafeHtmlBuilder b = new SafeHtmlBuilder(); + for (EditorError error : errors) { + b.appendEscaped(error.getPath()).appendEscaped(": "); + b.appendEscaped(error.getMessage()).appendHtmlConstant("
    "); + } + this.errors.setInnerHTML(b.toSafeHtml().asString()); + } + + @UiHandler("save") + void onSave(ClickEvent event) { + delegate.saveClicked(); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileEditViewUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileEditViewUiXml.xtm new file mode 100644 index 000000000..453c1e7bd --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileEditViewUiXml.xtm @@ -0,0 +1,31 @@ + + + + .outer { + padding: 10px; + } + .error { + padding-left: 0.5em; + background-color: red; + } + .label { + font-weight: bold; + } + .value { + padding-bottom: 10px; + } + + + +
    + {{#editableProperties}} +
    {{=propReadable}}
    <{{=propBinder}} ui:field='{{=prop}}'/>
    {{/editableProperties}} + + Save +
    +
    diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileListView.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileListView.xtm new file mode 100644 index 000000000..9301404d0 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/MobileListView.xtm @@ -0,0 +1,86 @@ +package {{=packageName}}; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.text.client.DateTimeFormatRenderer; +import com.google.gwt.text.shared.AbstractRenderer; +import com.google.gwt.text.shared.AbstractSafeHtmlRenderer; +import com.google.gwt.text.shared.Renderer; +import com.google.gwt.text.shared.SafeHtmlRenderer; + +{{#imports}}import {{=import}}; +{{/imports}} +import java.util.HashSet; +import java.util.Set; + +/** + * {@link MobileProxyListView} specialized to {@link {{=name}}Key} values. + */ +public class {{=className}} extends MobileProxyListView<{{=proxy}}> { + + /** + * The renderer used to render cells. + */ + private static class CellRenderer extends AbstractSafeHtmlRenderer<{{proxy}}> { + private final String dateStyle = ScaffoldMobileApp.getMobileListResources().cellListStyle().dateProp(); + private final String secondaryStyle = ScaffoldMobileApp.getMobileListResources().cellListStyle().secondaryProp(); + private final Renderer<{{=proxy}}> primaryRenderer = {{=proxyRenderer}}.instance(); + {{#mobileProperties}} + private final Renderer<{{=propType}}> {{=propRendererName}} = {{=propRenderer}}; + {{/mobileProperties}} + + @Override + public SafeHtml render({{=proxy}} value) { + if (value == null) { + return SafeHtmlUtils.EMPTY_SAFE_HTML; + } + + // Primary property. + SafeHtmlBuilder sb = new SafeHtmlBuilder(); + {{=primaryPropBuilder}} + + // Secondary property. + sb.appendHtmlConstant("
    "); + sb.appendHtmlConstant("
    "); + {{=secondaryPropBuilder}} + sb.appendHtmlConstant("
    "); + + // Date property. + sb.appendHtmlConstant("
    "); + {{=datePropBuilder}} + sb.appendHtmlConstant("
    "); + sb.appendHtmlConstant("
    "); + + return sb.toSafeHtml(); + } + } + + private static {{=className}} instance; + + private final Set paths = new HashSet(); + + public static {{=className}} instance() { + if (instance == null) { + instance = new {{=className}}(); + } + + return instance; + } + + public {{=className}}() { + super("New {{=name}}", new CellRenderer()); + init(); + } + + public void init() { + {{#mobileProperties}} + paths.add("{{=prop}}");{{/mobileProperties}} + } + + public String[] getPaths() { + return paths.toArray(new String[paths.size()]); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ScaffoldMobileActivities.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ScaffoldMobileActivities.xtm new file mode 100644 index 000000000..8eb3e5463 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/ScaffoldMobileActivities.xtm @@ -0,0 +1,33 @@ +package {{=packageName}}; + +import com.google.gwt.activity.shared.Activity; +import com.google.gwt.activity.shared.ActivityMapper; +import com.google.gwt.place.shared.Place; +import com.google.inject.Inject; + +/** + * Instantiates activities for the mobile app. + */ +public final class ScaffoldMobileActivities implements ActivityMapper { + private final ApplicationMasterActivities listActivityBuilder; + private final ApplicationDetailsActivities detailsActivityBuilder; + private Activity rootActivity; + + @Inject + public ScaffoldMobileActivities(ApplicationMasterActivities listActivitiesBuilder, ApplicationDetailsActivities detailsActivityBuilder) { + this.listActivityBuilder = listActivitiesBuilder; + this.detailsActivityBuilder = detailsActivityBuilder; + } + + public Activity getActivity(Place place) { + Activity rtn = listActivityBuilder.getActivity(place); + if (rtn == null) { + rtn = detailsActivityBuilder.getActivity(place); + } + return rtn == null ? rootActivity : rtn; + } + + public void setRootActivity(Activity activity) { + this.rootActivity = activity; + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SetEditor.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SetEditor.xtm new file mode 100644 index 000000000..97e2f99b0 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SetEditor.xtm @@ -0,0 +1,278 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.editor.client.Editor; +import com.google.gwt.editor.client.EditorDelegate; +import com.google.gwt.editor.client.LeafValueEditor; +import com.google.gwt.editor.client.ValueAwareEditor; +import com.google.gwt.editor.client.adapters.EditorSource; +import com.google.gwt.editor.client.adapters.ListEditor; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.client.ui.Widget; + +import {{=scaffoldUiPackage}}.CollectionRenderer; +{{#imports}}import {{=import}}; +{{/imports}} + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Displays an editor for a Set of {{=proxy}} objects. + */ +public class {{=className}} extends Composite implements ValueAwareEditor>, LeafValueEditor> { + + interface Binder extends UiBinder { + + } + + interface Driver extends RequestFactoryEditorDriver, // + ListEditor<{{=proxy}}, NameLabel>> { + + } + + class NameLabel extends Composite implements LeafValueEditor<{{=proxy}}> { + + final Label {{=primaryProp}}Editor = new Label(); + private {{=proxy}} proxy = null; + + public NameLabel() { + this(null); + } + + public NameLabel(final EventBus eventBus) { + initWidget({{=primaryProp}}Editor); + } + + public void setValue({{=proxy}} proxy) { + this.proxy = proxy; + {{=primaryProp}}Editor.setText({{=proxyRenderer}}.instance().render(proxy)); + } + + @Override + public {{=proxy}} getValue() { + return proxy; + } + } + + interface Style extends CssResource { + + String editorPanelHidden(); + + String editorPanelVisible(); + + String viewPanelHidden(); + + String viewPanelVisible(); + } + + /** + * This is used by a ListEditor. + */ + private class NameLabelSource extends EditorSource { + + @Override + public NameLabel create(int index) { + NameLabel label = new NameLabel(); + addToTable(label.getValue(), index); + return label; + } + + @Override + public void dispose(NameLabel subEditor) { + subEditor.removeFromParent(); + } + + @Override + public void setIndex(NameLabel editor, int index) { + addToTable(editor.getValue(), index); + } + } + + @UiField + FlexTable table; + + @UiField(provided = true) + @Editor.Ignore + ValueListBox<{{=proxy}}> picker + = new ValueListBox<{{=proxy}}>( + {{=proxyRenderer}}.instance(), + new com.google.web.bindery.requestfactory.gwt.ui.client.EntityProxyKeyProvider<{{=proxy}}>()); + + ; + + @UiField + Button add; + + @UiField + HTMLPanel editorPanel; + + @UiField + Button clickToEdit; + + @UiField + HTMLPanel viewPanel; + + @UiField + Label viewLabel; + + @UiField + Style style; + + boolean editing = false; + + private Set<{{=proxy}}> values; + + private final List<{{=proxy}}> displayedList; + + public {{=className}}() { + + // Create the UI + initWidget(GWT.create(Binder.class).createAndBindUi(this)); + + // Create the driver which manages the data-bound widgets + Driver driver = GWT.create(Driver.class); + + // Use a ListEditor that uses our NameLabelSource + ListEditor<{{=proxy}}, NameLabel> listEditor = ListEditor.of(new NameLabelSource()); + + // Configure the driver + driver.initialize(listEditor); + + /* + * Notice the backing list is essentially anonymous. + */ + driver.display(new ArrayList<{{=proxy}}>()); + + // Modifying this list triggers widget creation and destruction + displayedList = listEditor.getList(); + + editing = false; + } + + @UiHandler("add") + public void addClicked(ClickEvent e) { + if (picker.getValue() == null){ + return; + } + for ({{=proxy}} proxy : displayedList) { + if (proxy.{{=primaryPropGetter}}().equals(picker.getValue().{{=primaryPropGetter}}())) { + return; + } + } + displayedList.add(picker.getValue()); + viewLabel.setText(makeFlatList(displayedList)); + addToTable(picker.getValue()); + } + + @UiHandler("clickToEdit") + public void clickToEditClicked(ClickEvent e) { + toggleEditorMode(); + } + + @Override + public void flush() { + } + + @Override + public Set<{{=proxy}}> getValue() { + if (values == null && displayedList.size() == 0) { + return null; + } + return new HashSet<{{=proxy}}>(displayedList); + } + + public void onLoad() { + makeEditable(false); + } + + @Override + public void onPropertyChange(String... strings) { + } + + public void setAcceptableValues(Collection<{{=proxy}}> proxies) { + picker.setAcceptableValues(proxies); + } + + @Override + public void setDelegate( + EditorDelegate> editorDelegate) { + } + + @Override + public void setValue(Set<{{=proxy}}> values) { + this.values = values; + makeEditable(editing = false); + if (displayedList != null) { + displayedList.clear(); + table.clear(); + if (values != null) { + for ({{=proxy}} e : values) { + displayedList.add(e); + addToTable(e); + } + } + } + viewLabel.setText(makeFlatList(values)); + } + + private void addToTable({{=proxy}} value) { + addToTable(value, displayedList.size() - 1); + } + + private void addToTable({{=proxy}} value, int index){ + final int finalIndex = index; + if (value != null) { + table.setText(finalIndex, 0, {{=proxyRenderer}}.instance().render(value)); + Button removeEntryButton = new Button("x"); + removeEntryButton.addClickHandler(new ClickHandler() { + public void onClick(final ClickEvent event){ + displayedList.remove(finalIndex); + table.removeRow(finalIndex); + viewLabel.setText(makeFlatList(displayedList)); + } + }); + table.setWidget(finalIndex, 1, removeEntryButton); + } + } + + private void makeEditable(boolean editable) { + if (editable) { + editorPanel.setStylePrimaryName(style.editorPanelVisible()); + viewPanel.setStylePrimaryName(style.viewPanelHidden()); + clickToEdit.setText("Done"); + } else { + editorPanel.setStylePrimaryName(style.editorPanelHidden()); + viewPanel.setStylePrimaryName(style.viewPanelVisible()); + clickToEdit.setText("Edit"); + } + } + + private String makeFlatList(Collection<{{=proxy}}> values) { + return CollectionRenderer.of({{=proxyRenderer}}.instance()) + .render(values); + } + + private void toggleEditorMode() { + editing = !editing; + makeEditable(editing); + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SetEditorUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SetEditorUiXml.xtm new file mode 100644 index 000000000..95c8fc077 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SetEditorUiXml.xtm @@ -0,0 +1,31 @@ + + + .editorPanelVisible { + border: thin solid black; + margin: 2px; + overflow: hidden; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + } + + .viewPanelVisible { + } + + .editorPanelHidden, .viewPanelHidden { + display: none + } + + + Edit + + + + + Entry to Add: + Add
    + +
    +
    +
    \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SimpleCollectionEditor.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SimpleCollectionEditor.xtm new file mode 100644 index 000000000..74467c71f --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SimpleCollectionEditor.xtm @@ -0,0 +1,261 @@ +package {{=packageName}}; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.editor.client.EditorDelegate; +import com.google.gwt.editor.client.LeafValueEditor; +import com.google.gwt.editor.client.ValueAwareEditor; +import com.google.gwt.editor.client.adapters.EditorSource; +import com.google.gwt.editor.client.adapters.ListEditor; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.shared.EventBus; +import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.text.shared.Renderer; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.client.ui.Widget; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import {{=scaffoldUiPackage}}.CollectionRenderer; +{{#imports}}import {{=import}}; +{{/imports}} + +public class {{=boundCollectionType}}{{=collectionType}}Editor extends Composite implements ValueAwareEditor<{{=collectionType}}<{{=boundCollectionType}}>>, LeafValueEditor<{{=collectionType}}<{{=boundCollectionType}}>> { + + @UiField + FlowPanel container; + + private Renderer<{{=boundCollectionType}}> renderer = new Renderer<{{=boundCollectionType}}>() { + @Override + public String render({{=boundCollectionType}} o) { + return o == null ? "" : String.valueOf(o); + } + + @Override + public void render({{=boundCollectionType}} o, Appendable appendable) throws IOException { + appendable.append(render(o)); + } + }; + + @UiField + @Ignore + {{=inputType}} value; + + @UiField + Button add; + + @UiField + HTMLPanel editorPanel; + + @UiField + Button clickToEdit; + + @UiField + HTMLPanel viewPanel; + + @UiField + Label viewLabel; + + @UiField + Style style; + + boolean editing = false; + + private {{=collectionType}}<{{=boundCollectionType}}> values; + + private List<{{=boundCollectionType}}> displayedList; + + public CollectionEditor() { + initWidget(GWT.create(Binder.class).createAndBindUi(this)); + Driver driver = GWT.create(Driver.class); + ListEditor<{{=boundCollectionType}}, NameLabel> editor = ListEditor.of(new NameLabelSource()); + driver.initialize(editor); + driver.display(new ArrayList<{{=boundCollectionType}}>()); + displayedList = editor.getList(); + editing = false; + } + + @UiHandler("add") + public void addClicked(ClickEvent e) { + if (picker.getValue() == null || displayedList.contains(picker.getValue())) { + return; + } + displayedList.add(picker.getValue()); + value.setText(""); + } + + @UiHandler("clickToEdit") + public void clickToEditClicked(ClickEvent e) { + toggleEditorMode(); + if (values != null) { + displayedList.addAll(values); + } + } + + + interface Binder extends UiBinder { + } + + interface Driver extends RequestFactoryEditorDriver, ListEditor<{{=boundCollectionType}}, NameLabel>> { + } + + public void onLoad() { + makeEditable(false); + } + + public void setAcceptableValues(Collection<{{=boundCollectionType}}> values) { + + } + + @Override + public void setDelegate(EditorDelegate<{{=collectionType}}<{{=boundCollectionType}}>> editorDelegate) { + } + + @Override + public void onPropertyChange(java.lang.String... strings) { + } + + @Override + public void flush() { + } + + @Override + public void setValue({{=collectionType}}<{{=boundCollectionType}}> values) { + + this.values = values; + makeEditable(editing = false); + if (displayedList != null) { + displayedList.clear(); + if (values != null) { + for ({{=boundCollectionType}} e : values) { + displayedList.add(e); + } + } + } + viewLabel.setText(makeFlatList(values)); + } + + private void makeEditable(boolean editable) { + if (editable) { + editorPanel.setStylePrimaryName(style.editorPanelVisible()); + viewPanel.setStylePrimaryName(style.viewPanelHidden()); + clickToEdit.setText("Done"); + } else { + editorPanel.setStylePrimaryName(style.editorPanelHidden()); + viewPanel.setStylePrimaryName(style.viewPanelVisible()); + clickToEdit.setText("Edit"); + } + } + + private java.lang.String makeFlatList(Collection values) { + return CollectionRenderer.of(renderer).render(values); + } + + private void toggleEditorMode() { + editing = !editing; + makeEditable(editing); + viewLabel.setText(makeFlatList(displayedList)); + } + + @Override + public {{=collectionType}}<{{=boundCollectionType}}> getValue() { + if (values == null && displayedList.size() == 0) { + return null; + } + return new {{=collectionTypeImpl}}<{{=boundCollectionType}}>(displayedList); + } + + interface Style extends CssResource { + + java.lang.String editorPanelHidden(); + + java.lang.String editorPanelVisible(); + + java.lang.String viewPanelHidden(); + + java.lang.String viewPanelVisible(); + } + + class NameLabel extends Composite implements LeafValueEditor<{{=boundCollectionType}}> { + + @Ignore + final Label displayNameEditor = new Label(); + + final Button removeButton = new Button("X"); + + int index; + + public NameLabel(int index) { + this(null); + } + + public NameLabel(EventBus eventBus, int index) { + HorizontalPanel panel = new HorizontalPanel(); + panel.setSpacing(5); + panel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); + panel.add(displayNameEditor); + panel.add(removeButton); + this.index = index; + + removeButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) + { + displayedList.remove(NameLabel.this.index); + for (int i = 0; i < container.getWidgetCount(); i++) { + NameLabel label = (NameLabel) container.getWidget(i); + label.setIndex(i); + } + } + }); + initWidget(panel); + } + + void setIndex(int index) { + this.index = index; + } + + @Override + public void setValue({{=boundCollectionType}} o) { + displayNameEditor.setText(String.valueOf(o)); + } + + @Override + public {{=boundCollectionType}} getValue() { + return {{=boundCollectionType}}.valueOf(displayNameEditor.getText()); + } + } + + private class NameLabelSource extends EditorSource { + + @Override + public NameLabel create(int index) { + NameLabel label = new NameLabel(); + container.insert(label, index); + return label; + } + + @Override + public void dispose(NameLabel subEditor) { + subEditor.removeFromParent(); + } + + @Override + public void setIndex(NameLabel editor, int index) { + container.insert(editor, index); + } + } +} diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SimpleCollectionEditorUiXml.xtm b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SimpleCollectionEditorUiXml.xtm new file mode 100644 index 000000000..b39dd0723 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/SimpleCollectionEditorUiXml.xtm @@ -0,0 +1,34 @@ + + + + .editorPanelVisible { + border: thin solid black; + margin: 2px; + overflow: hidden; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + } + + .viewPanelVisible { + } + + .editorPanelHidden, .viewPanelHidden { + display: none + } + + + Edit + + + + + Entry to Add: + + Add +
    + +
    +
    +
    \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/gwt-module.dtd b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/gwt-module.dtd new file mode 100644 index 000000000..3407bf6ea --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/gwt-module.dtd @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/xhtml.ent b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/xhtml.ent new file mode 100644 index 000000000..156975313 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/scaffold/templates/xhtml.ent @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/setup/App-template.gwt.xml b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/setup/App-template.gwt.xml new file mode 100644 index 000000000..c402823d0 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/setup/App-template.gwt.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/setup/client/AppEntryPoint-template.java b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/setup/client/AppEntryPoint-template.java new file mode 100644 index 000000000..683146e68 --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/setup/client/AppEntryPoint-template.java @@ -0,0 +1,9 @@ +package __TOP_LEVEL_PACKAGE__.client; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.core.client.GWT; + +public class AppEntryPoint implements EntryPoint { + + public void onModuleLoad() {} +} \ No newline at end of file diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/webapp/ApplicationScaffold.html b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/webapp/ApplicationScaffold.html new file mode 100644 index 000000000..b187b544c --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/webapp/ApplicationScaffold.html @@ -0,0 +1,15 @@ + + + + + + Data Browser + + + + + + loading… + + + diff --git a/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/webapp/index.html b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/webapp/index.html new file mode 100644 index 000000000..4dc6cfb7a --- /dev/null +++ b/addon-gwt/src/main/resources/org/springframework/roo/addon/gwt/webapp/index.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/GwtPathTest.java b/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/GwtPathTest.java new file mode 100644 index 000000000..9ea1a9333 --- /dev/null +++ b/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/GwtPathTest.java @@ -0,0 +1,47 @@ +package org.springframework.roo.addon.gwt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.HashSet; + +import org.junit.Test; + +/** + * Unit test of the {@link GwtPath} enum. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class GwtPathTest { + + @Test + public void testPackageNameForWebPath() { + assertEquals("", GwtPath.WEB.packageName(null)); + } + + @Test + public void testSegmentNamesAreNonNull() { + for (final GwtPath gwtPath : GwtPath.values()) { + assertNotNull("Null segment name for " + gwtPath, + gwtPath.getSegmentName()); + } + } + + @Test + public void testSegmentNamesAreUnique() { + final Collection segmentNames = new HashSet(); + for (final GwtPath gwtPath : GwtPath.values()) { + final String segmentName = gwtPath.getSegmentName(); + assertTrue("Duplicate segment name '" + segmentName + "'", + segmentNames.add(segmentName)); + } + } + + @Test + public void testSegmentPackageForWebPath() { + assertEquals("", GwtPath.WEB.segmentPackage()); + } +} diff --git a/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/GwtProxyPropertyTest.java b/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/GwtProxyPropertyTest.java new file mode 100644 index 000000000..54bedb727 --- /dev/null +++ b/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/GwtProxyPropertyTest.java @@ -0,0 +1,45 @@ +package org.springframework.roo.addon.gwt; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link GwtProxyProperty} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class GwtProxyPropertyTest { + + private static final String GETTER = "getBar"; + private static final String NAME = "foo"; + + @Test + public void testSetIsCollectionOfProxy() { + // Set up + final JavaPackage mockTopLevelPackage = mock(JavaPackage.class); + final ClassOrInterfaceTypeDetails mockCoitd = mock(ClassOrInterfaceTypeDetails.class); + final JavaType genericType = new JavaType( + "com.foo.roo2881.client.proxy.Foo1Proxy"); + final JavaType proxyType = new JavaType("java.util.Set", 0, + DataType.TYPE, null, Arrays.asList(genericType)); + final List annotations = Collections.emptyList(); + final GwtProxyProperty proxyProperty = new GwtProxyProperty( + mockTopLevelPackage, mockCoitd, proxyType, NAME, annotations, + GETTER); + + // Invoke and check + assertTrue(proxyProperty.isCollectionOfProxy()); + } +} diff --git a/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataTest.java b/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataTest.java new file mode 100644 index 000000000..dc538e8a1 --- /dev/null +++ b/addon-gwt/src/test/java/org/springframework/roo/addon/gwt/request/GwtRequestMetadataTest.java @@ -0,0 +1,100 @@ +package org.springframework.roo.addon.gwt.request; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Unit test for {@link GwtRequestMetadata} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class GwtRequestMetadataTest { + + private static final String CONTENTS = "contents"; + private static final String MID_1 = "MID:x#y"; + private static final String MID_2 = "MID:x#z"; + + @Test + public void testInstanceDoesNotEqualInstanceOfOtherClass() { + // Set up + final GwtRequestMetadata instance = new GwtRequestMetadata(MID_1, + CONTENTS); + + // Invoke and check + assertFalse(instance.equals(new Object())); + } + + @Test + public void testInstanceDoesNotEqualNull() { + // Set up + final GwtRequestMetadata instance = new GwtRequestMetadata(MID_1, + CONTENTS); + + // Invoke and check + assertFalse(instance.equals(null)); + } + + @Test + public void testInstanceEqualsItself() { + // Set up + final GwtRequestMetadata instance = new GwtRequestMetadata(MID_1, + CONTENTS); + + // Invoke and check + assertEquals(instance, instance); + } + + @Test + public void testInstancesWithDifferentContentsAreNotEqual() { + // Set up + final GwtRequestMetadata instance1 = new GwtRequestMetadata(MID_1, + CONTENTS); + final GwtRequestMetadata instance2 = new GwtRequestMetadata(MID_1, + CONTENTS + "x"); + + // Invoke and check + assertFalse(instance1.equals(instance2)); + assertFalse(instance2.equals(instance1)); + } + + @Test + public void testInstancesWithDifferentContentsHaveDifferentHashCodes() { + // Set up + final GwtRequestMetadata instance1 = new GwtRequestMetadata(MID_1, + CONTENTS); + final GwtRequestMetadata instance2 = new GwtRequestMetadata(MID_1, + CONTENTS + "x"); + + // Invoke and check + assertTrue(instance1.hashCode() != instance2.hashCode()); + } + + @Test + public void testInstancesWithSameContentsAreEqual() { + // Set up + final GwtRequestMetadata instance1 = new GwtRequestMetadata(MID_1, + CONTENTS); + final GwtRequestMetadata instance2 = new GwtRequestMetadata(MID_2, + CONTENTS); + + // Invoke and check + assertEquals(instance1, instance2); + assertEquals(instance2, instance1); + } + + @Test + public void testInstancesWithSameContentsHaveSameHashCodes() { + // Set up + final GwtRequestMetadata instance1 = new GwtRequestMetadata(MID_1, + CONTENTS); + final GwtRequestMetadata instance2 = new GwtRequestMetadata(MID_2, + CONTENTS); + + // Invoke and check + assertTrue(instance1.hashCode() == instance2.hashCode()); + } +} diff --git a/addon-javabean/pom.xml b/addon-javabean/pom.xml new file mode 100644 index 000000000..c555e33d3 --- /dev/null +++ b/addon-javabean/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.javabean + bundle + Spring Roo - Addon - JavaBean Method Manager + Support for creation and management of accessors and mutators following the JavaBean standard. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanAnnotationValues.java b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanAnnotationValues.java new file mode 100644 index 000000000..1ed83c079 --- /dev/null +++ b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanAnnotationValues.java @@ -0,0 +1,38 @@ +package org.springframework.roo.addon.javabean; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooJavaBean} annotation. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JavaBeanAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private boolean gettersByDefault = true; + @AutoPopulate private boolean settersByDefault = true; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public JavaBeanAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_JAVA_BEAN); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public boolean isGettersByDefault() { + return gettersByDefault; + } + + public boolean isSettersByDefault() { + return settersByDefault; + } +} diff --git a/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanMetadata.java b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanMetadata.java new file mode 100644 index 000000000..b41c96e39 --- /dev/null +++ b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanMetadata.java @@ -0,0 +1,628 @@ +package org.springframework.roo.addon.javabean; + +import static org.springframework.roo.model.GoogleJavaType.GAE_DATASTORE_KEY; +import static org.springframework.roo.model.GoogleJavaType.GAE_DATASTORE_KEY_FACTORY; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; +import static org.springframework.roo.model.JdkJavaType.HASH_SET; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.SET; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.TRANSIENT; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.DeclaredFieldAnnotationDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooJavaBean}. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +public class JavaBeanMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = JavaBeanMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private JavaBeanAnnotationValues annotationValues; + + private Map declaredFields; + + private List interfaceMethods; + + /** + * Constructor + * + * @param identifier + * the ID of the metadata to create (must be a valid ID) + * @param aspectName + * the name of the ITD to be created (required) + * @param governorPhysicalTypeMetadata + * the governor (required) + * @param annotationValues + * the values of the {@link RooJavaBean} annotation (required) + * @param declaredFields + * the fields declared in the governor (required, can be empty) + */ + public JavaBeanMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final JavaBeanAnnotationValues annotationValues, + final Map declaredFields, + List interfaceMethods) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(declaredFields, "Declared fields required"); + + if (!isValid()) { + return; + } + if (declaredFields.isEmpty()) { + return; // N.B. the MD is still valid, just empty + } + + this.annotationValues = annotationValues; + this.declaredFields = declaredFields; + this.interfaceMethods = interfaceMethods; + + // Add getters and setters + for (final Entry entry : declaredFields + .entrySet()) { + final FieldMetadata field = entry.getKey(); + final MethodMetadataBuilder accessorMethod = getDeclaredGetter(field); + final MethodMetadataBuilder mutatorMethod = getDeclaredSetter(field); + + // Check to see if GAE is interested + if (entry.getValue() != null) { + JavaSymbolName hiddenIdFieldName; + if (field.getFieldType().isCommonCollectionType()) { + hiddenIdFieldName = governorTypeDetails + .getUniqueFieldName(field.getFieldName() + .getSymbolName() + "Keys"); + builder.getImportRegistrationResolver().addImport( + GAE_DATASTORE_KEY_FACTORY); + builder.addField(getMultipleEntityIdField(hiddenIdFieldName)); + } else { + hiddenIdFieldName = governorTypeDetails + .getUniqueFieldName(field.getFieldName() + .getSymbolName() + "Id"); + builder.addField(getSingularEntityIdField(hiddenIdFieldName)); + } + + processGaeAnnotations(field); + + accessorMethod.setBodyBuilder(getGaeAccessorBody(field, + hiddenIdFieldName)); + mutatorMethod.setBodyBuilder(getGaeMutatorBody(field, + hiddenIdFieldName)); + } + + builder.addMethod(accessorMethod); + builder.addMethod(mutatorMethod); + } + + // Implements interface methods if exists + if (interfaceMethods != null) { + for (MethodMetadata interfaceMethod : interfaceMethods) { + MethodMetadataBuilder methodBuilder = getInterfaceMethod(interfaceMethod); + // ROO-3584: JavaBean implementing Interface defining getters and setters + // ROO-3585: If interface method already exists on type is not necessary + // to add on ITD. Method builder will be NULL. + if(methodBuilder != null && + !checkIfInterfaceMethodWasImplemented(methodBuilder)){ + builder.addMethod(methodBuilder); + } + } + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Obtains the specific accessor method that is either contained within the + * normal Java compilation unit or will be introduced by this add-on via an + * ITD. + * + * @param field + * that already exists on the type either directly or via + * introduction (required; must be declared by this type to be + * located) + * @return the method corresponding to an accessor, or null if not found + */ + private MethodMetadataBuilder getDeclaredGetter(final FieldMetadata field) { + Validate.notNull(field, "Field required"); + + // Compute the mutator method name + final JavaSymbolName methodName = BeanInfoUtils + .getAccessorMethodName(field); + + // See if the type itself declared the accessor + if (governorHasMethod(methodName)) { + return null; + } + + // Decide whether we need to produce the accessor method (see ROO-619 + // for reason we allow a getter for a final field) + if (annotationValues.isGettersByDefault() + && !Modifier.isTransient(field.getModifier()) + && !Modifier.isStatic(field.getModifier())) { + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return this." + + field.getFieldName().getSymbolName() + ";"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + methodName, field.getFieldType(), bodyBuilder); + } + + return null; + } + + /** + * Obtains the specific mutator method that is either contained within the + * normal Java compilation unit or will be introduced by this add-on via an + * ITD. + * + * @param field + * that already exists on the type either directly or via + * introduction (required; must be declared by this type to be + * located) + * @return the method corresponding to a mutator, or null if not found + */ + private MethodMetadataBuilder getDeclaredSetter(final FieldMetadata field) { + Validate.notNull(field, "Field required"); + + // Compute the mutator method name + final JavaSymbolName methodName = BeanInfoUtils + .getMutatorMethodName(field); + + // Compute the mutator method parameters + final JavaType parameterType = field.getFieldType(); + + // See if the type itself declared the mutator + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + // Compute the mutator method parameter names + final List parameterNames = Arrays.asList(field + .getFieldName()); + + // Decide whether we need to produce the mutator method (disallowed for + // final fields as per ROO-36) + if (annotationValues.isSettersByDefault() + && !Modifier.isTransient(field.getModifier()) + && !Modifier.isStatic(field.getModifier()) + && !Modifier.isFinal(field.getModifier())) { + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("this." + + field.getFieldName().getSymbolName() + " = " + + field.getFieldName().getSymbolName() + ";"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + methodName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + return null; + } + + /** + * Obtains a valid MethodMetadataBuilder with necessary configuration + * + * @param method + * @return MethodMetadataBuilder + */ + private MethodMetadataBuilder getInterfaceMethod(final MethodMetadata method) { + + // Compute the method name + final JavaSymbolName methodName = method.getMethodName(); + // See if the type itself declared the accessor + if (governorHasMethod(methodName)) { + return null; + } + // Getting return type + JavaType returnType = method.getReturnType(); + // Generating body + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + // If return type is not primitive, return null + if (returnType.isPrimitive()) { + JavaType baseType = returnType.getBaseType(); + if (baseType.equals(JavaType.BOOLEAN_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return false;"); + } else if (baseType.equals(JavaType.BYTE_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return 0;"); + } else if (baseType.equals(JavaType.SHORT_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return 0;"); + } else if (baseType.equals(JavaType.INT_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return 0;"); + } else if (baseType.equals(JavaType.LONG_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return 0;"); + } else if (baseType.equals(JavaType.FLOAT_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return 0;"); + } else if (baseType.equals(JavaType.DOUBLE_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return 0.00;"); + } else if (baseType.equals(JavaType.CHAR_PRIMITIVE)) { + bodyBuilder.appendFormalLine("return '\0';"); + } + } else { + bodyBuilder.appendFormalLine("return null;"); + } + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + returnType, bodyBuilder); + + } + + private InvocableMemberBodyBuilder getEntityCollectionAccessorBody( + final FieldMetadata field, final JavaSymbolName entityIdsFieldName) { + final String entityCollectionName = field.getFieldName() + .getSymbolName(); + final String entityIdsName = entityIdsFieldName.getSymbolName(); + final String localEnitiesName = "local" + + StringUtils.capitalize(entityCollectionName); + + final JavaType collectionElementType = field.getFieldType() + .getParameters().get(0); + final String simpleCollectionElementTypeName = collectionElementType + .getSimpleTypeName(); + + JavaType collectionType = field.getFieldType(); + builder.getImportRegistrationResolver().addImport(collectionType); + + final String collectionName = field + .getFieldType() + .getNameIncludingTypeParameters() + .replace( + field.getFieldType().getPackage() + .getFullyQualifiedPackageName() + + ".", ""); + String instantiableCollection = collectionName; + + // GAE only supports java.util.List and java.util.Set collections and we + // need a concrete implementation of either. + if (collectionType.getFullyQualifiedTypeName().equals( + LIST.getFullyQualifiedTypeName())) { + collectionType = new JavaType( + ARRAY_LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, collectionType.getParameters()); + instantiableCollection = collectionType + .getNameIncludingTypeParameters().replace( + collectionType.getPackage() + .getFullyQualifiedPackageName() + ".", ""); + } else if (collectionType.getFullyQualifiedTypeName().equals( + SET.getFullyQualifiedTypeName())) { + collectionType = new JavaType(HASH_SET.getFullyQualifiedTypeName(), + 0, DataType.TYPE, null, collectionType.getParameters()); + instantiableCollection = collectionType + .getNameIncludingTypeParameters().replace( + collectionType.getPackage() + .getFullyQualifiedPackageName() + ".", ""); + } + + builder.getImportRegistrationResolver().addImport(collectionType); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(collectionName + " " + localEnitiesName + + " = new " + instantiableCollection + "();"); + bodyBuilder.appendFormalLine("for (Key key : " + entityIdsName + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(simpleCollectionElementTypeName + + " entity = " + simpleCollectionElementTypeName + ".find" + + simpleCollectionElementTypeName + "(key.getId());"); + bodyBuilder.appendFormalLine("if (entity != null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(localEnitiesName + ".add(entity);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("this." + entityCollectionName + " = " + + localEnitiesName + ";"); + bodyBuilder.appendFormalLine("return " + localEnitiesName + ";"); + + return bodyBuilder; + } + + private InvocableMemberBodyBuilder getEntityCollectionMutatorBody( + final FieldMetadata field, final JavaSymbolName entityIdsFieldName) { + final String entityCollectionName = field.getFieldName() + .getSymbolName(); + final String entityIdsName = entityIdsFieldName.getSymbolName(); + final JavaType collectionElementType = field.getFieldType() + .getParameters().get(0); + final String localEnitiesName = "local" + + StringUtils.capitalize(entityCollectionName); + + JavaType collectionType = field.getFieldType(); + builder.getImportRegistrationResolver().addImport(collectionType); + + final String collectionName = field + .getFieldType() + .getNameIncludingTypeParameters() + .replace( + field.getFieldType().getPackage() + .getFullyQualifiedPackageName() + + ".", ""); + String instantiableCollection = collectionName; + if (collectionType.getFullyQualifiedTypeName().equals( + LIST.getFullyQualifiedTypeName())) { + collectionType = new JavaType( + ARRAY_LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, collectionType.getParameters()); + instantiableCollection = collectionType + .getNameIncludingTypeParameters().replace( + collectionType.getPackage() + .getFullyQualifiedPackageName() + ".", ""); + } else if (collectionType.getFullyQualifiedTypeName().equals( + SET.getFullyQualifiedTypeName())) { + collectionType = new JavaType(HASH_SET.getFullyQualifiedTypeName(), + 0, DataType.TYPE, null, collectionType.getParameters()); + instantiableCollection = collectionType + .getNameIncludingTypeParameters().replace( + collectionType.getPackage() + .getFullyQualifiedPackageName() + ".", ""); + } + + builder.getImportRegistrationResolver().addImports(collectionType, + LIST, ARRAY_LIST); + + final String identifierMethodName = getIdentifierMethodName(field) + .getSymbolName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(collectionName + " " + localEnitiesName + + " = new " + instantiableCollection + "();"); + bodyBuilder + .appendFormalLine("List longIds = new ArrayList();"); + bodyBuilder.appendFormalLine("for (Key key : " + entityIdsName + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("if (!longIds.contains(key.getId())) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("longIds.add(key.getId());"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("for (" + + collectionElementType.getSimpleTypeName() + " entity : " + + entityCollectionName + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("if (!longIds.contains(entity." + + identifierMethodName + "())) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("longIds.add(entity." + + identifierMethodName + "());"); + bodyBuilder.appendFormalLine(entityIdsName + + ".add(KeyFactory.createKey(" + + collectionElementType.getSimpleTypeName() + + ".class.getName(), entity." + identifierMethodName + "()));"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(localEnitiesName + ".add(entity);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("this." + entityCollectionName + " = " + + localEnitiesName + ";"); + + return bodyBuilder; + } + + private InvocableMemberBodyBuilder getGaeAccessorBody( + final FieldMetadata field, final JavaSymbolName hiddenIdFieldName) { + return field.getFieldType().isCommonCollectionType() ? getEntityCollectionAccessorBody( + field, hiddenIdFieldName) : getSingularEntityAccessor(field, + hiddenIdFieldName); + } + + private InvocableMemberBodyBuilder getGaeMutatorBody( + final FieldMetadata field, final JavaSymbolName hiddenIdFieldName) { + return field.getFieldType().isCommonCollectionType() ? getEntityCollectionMutatorBody( + field, hiddenIdFieldName) : getSingularEntityMutator(field, + hiddenIdFieldName); + } + + private InvocableMemberBodyBuilder getInterfaceMethodBody( + JavaType returnType) { + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("// Interface Implementation"); + bodyBuilder.appendFormalLine("return 0;"); + return bodyBuilder; + } + + private JavaSymbolName getIdentifierMethodName(final FieldMetadata field) { + final JavaSymbolName identifierAccessorMethodName = declaredFields + .get(field); + return identifierAccessorMethodName != null ? identifierAccessorMethodName + : new JavaSymbolName("getId"); + } + + private FieldMetadataBuilder getMultipleEntityIdField( + final JavaSymbolName fieldName) { + builder.getImportRegistrationResolver().addImport(HASH_SET); + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, fieldName, + new JavaType(SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Collections.singletonList(GAE_DATASTORE_KEY)), + "new HashSet()"); + } + + private InvocableMemberBodyBuilder getSingularEntityAccessor( + final FieldMetadata field, final JavaSymbolName hiddenIdFieldName) { + final String entityName = field.getFieldName().getSymbolName(); + final String entityIdName = hiddenIdFieldName.getSymbolName(); + final String simpleFieldTypeName = field.getFieldType() + .getSimpleTypeName(); + + final String identifierMethodName = getIdentifierMethodName(field) + .getSymbolName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("if (this." + entityIdName + " != null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("if (this." + entityName + + " == null || this." + entityName + "." + identifierMethodName + + "() != this." + entityIdName + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("this." + entityName + " = " + + simpleFieldTypeName + ".find" + simpleFieldTypeName + + "(this." + entityIdName + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("this." + entityName + " = null;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return this." + entityName + ";"); + + return bodyBuilder; + } + + private FieldMetadataBuilder getSingularEntityIdField( + final JavaSymbolName fieldName) { + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, fieldName, + LONG_OBJECT, null); + } + + private InvocableMemberBodyBuilder getSingularEntityMutator( + final FieldMetadata field, final JavaSymbolName hiddenIdFieldName) { + final String entityName = field.getFieldName().getSymbolName(); + final String entityIdName = hiddenIdFieldName.getSymbolName(); + final String identifierMethodName = getIdentifierMethodName(field) + .getSymbolName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (" + entityName + " != null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("if (" + entityName + "." + + identifierMethodName + " () == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(entityName + ".persist();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("this." + entityIdName + " = " + + entityName + "." + identifierMethodName + "();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("this." + entityIdName + " = null;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("this." + entityName + " = " + entityName + + ";"); + + return bodyBuilder; + } + + private void processGaeAnnotations(final FieldMetadata field) { + for (final AnnotationMetadata annotation : field.getAnnotations()) { + if (annotation.getAnnotationType().equals(ONE_TO_ONE) + || annotation.getAnnotationType().equals(MANY_TO_ONE) + || annotation.getAnnotationType().equals(ONE_TO_MANY) + || annotation.getAnnotationType().equals(MANY_TO_MANY)) { + builder.addFieldAnnotation(new DeclaredFieldAnnotationDetails( + field, new AnnotationMetadataBuilder(annotation + .getAnnotationType()).build(), true)); + builder.addFieldAnnotation(new DeclaredFieldAnnotationDetails( + field, new AnnotationMetadataBuilder(TRANSIENT).build())); + break; + } + } + } + + /** + * To check if current method was implemented on _JavaBean.aj. + * If method was implemented, is not necessary to add again. + * + * @param methodBuilder + * @return + */ + private boolean checkIfInterfaceMethodWasImplemented( + MethodMetadataBuilder methodBuilder) { + // Obtain current declared methods + List declaredMethods = builder.getDeclaredMethods(); + + for(MethodMetadataBuilder method : declaredMethods){ + // If current method equals to interface method, return false + if(method.getMethodName().equals(methodBuilder.getMethodName())){ + return true; + } + } + return false; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanMetadataProvider.java b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanMetadataProvider.java new file mode 100644 index 000000000..c5080c0dd --- /dev/null +++ b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/JavaBeanMetadataProvider.java @@ -0,0 +1,243 @@ +package org.springframework.roo.addon.javabean; + +import static org.springframework.roo.model.JpaJavaType.TRANSIENT; +import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Provides {@link JavaBeanMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class JavaBeanMetadataProvider extends AbstractItdMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JavaBeanMetadataProvider.class); + + private final Set producedMids = new LinkedHashSet(); + + private ProjectOperations projectOperations; + private Boolean wasGaeEnabled; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_JAVA_BEAN); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return JavaBeanMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_JAVA_BEAN); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = JavaBeanMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JavaBeanMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + private JavaSymbolName getIdentifierAccessorMethodName( + final FieldMetadata field, final String metadataIdentificationString) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final LogicalPath path = PhysicalTypeIdentifier.getPath(field + .getDeclaredByMetadataId()); + final String moduleNme = path.getModule(); + if (projectOperations.isProjectAvailable(moduleNme) + || !projectOperations.isFeatureInstalled(FeatureNames.GAE)) { + return null; + } + // We are not interested if the field is annotated with + // @javax.persistence.Transient + for (final AnnotationMetadata annotationMetadata : field + .getAnnotations()) { + if (annotationMetadata.getAnnotationType().equals(TRANSIENT)) { + return null; + } + } + JavaType fieldType = field.getFieldType(); + // If the field is a common collection type we need to get the element + // type + if (fieldType.isCommonCollectionType()) { + if (fieldType.getParameters().isEmpty()) { + return null; + } + fieldType = fieldType.getParameters().get(0); + } + + final MethodMetadata identifierAccessor = persistenceMemberLocator + .getIdentifierAccessor(fieldType); + if (identifierAccessor != null) { + getMetadataDependencyRegistry().registerDependency( + identifierAccessor.getDeclaredByMetadataId(), + metadataIdentificationString); + return identifierAccessor.getMethodName(); + } + + return null; + } + + public String getItdUniquenessFilenameSuffix() { + return "JavaBean"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final JavaBeanAnnotationValues annotationValues = new JavaBeanAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound()) { + return null; + } + + ClassOrInterfaceTypeDetails currentClassDetails = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + + final Map declaredFields = new LinkedHashMap(); + for (final FieldMetadata field : currentClassDetails + .getDeclaredFields()) { + declaredFields.put( + field, + getIdentifierAccessorMethodName(field, + metadataIdentificationString)); + } + + // In order to handle switching between GAE and JPA produced MIDs need + // to be remembered so they can be regenerated on JPA <-> GAE switch + producedMids.add(metadataIdentificationString); + + // Getting implements + List interfaces = currentClassDetails.getImplementsTypes(); + List interfaceMethods = null; + if (!interfaces.isEmpty()) { + for (JavaType currentInterface : interfaces) { + ClassOrInterfaceTypeDetails currentInterfaceDetails = getTypeLocationService() + .getTypeDetails(currentInterface); + if(currentInterfaceDetails != null){ + interfaceMethods = currentInterfaceDetails.getDeclaredMethods(); + } + } + } + + return new JavaBeanMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues, declaredFields, + interfaceMethods); + } + + public String getProvidesType() { + return JavaBeanMetadata.getMetadataIdentiferType(); + } + + // We need to notified when ProjectMetadata changes in order to handle JPA + // <-> GAE persistence changes + @Override + protected void notifyForGenericListener(final String upstreamDependency) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + + Validate.notNull(projectOperations, "ProjectOperations is required"); + + // If the upstream dependency is null or invalid do not continue + if (StringUtils.isBlank(upstreamDependency) + || !MetadataIdentificationUtils.isValid(upstreamDependency)) { + return; + } + // If the upstream dependency isn't ProjectMetadata do not continue + if (!ProjectMetadata.isValid(upstreamDependency)) { + return; + } + // If the project isn't valid do not continue + if (projectOperations.isProjectAvailable(ProjectMetadata + .getModuleName(upstreamDependency))) { + final boolean isGaeEnabled = projectOperations + .isFeatureInstalled(FeatureNames.GAE); + // We need to determine if the persistence state has changed, we do + // this by comparing the last known state to the current state + final boolean hasGaeStateChanged = wasGaeEnabled == null + || isGaeEnabled != wasGaeEnabled; + if (hasGaeStateChanged) { + wasGaeEnabled = isGaeEnabled; + for (final String producedMid : producedMids) { + metadataService.evictAndGet(producedMid); + } + } + } + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on JavaBeanMetadataProvider."); + return null; + } + } +} diff --git a/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/RooJavaBean.java b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/RooJavaBean.java new file mode 100644 index 000000000..5f57635ad --- /dev/null +++ b/addon-javabean/src/main/java/org/springframework/roo/addon/javabean/RooJavaBean.java @@ -0,0 +1,30 @@ +package org.springframework.roo.addon.javabean; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Creates JavaBean accessors and mutators for fields declared against this + * type. + * + * @author Ben Alex + * @since 1.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJavaBean { + + /** + * @return whether to generate getters for each non-transient field declared + * in this class (defaults to true) + */ + boolean gettersByDefault() default true; + + /** + * @return whether to generate setters for each non-transient field declared + * in this class (defaults to true) + */ + boolean settersByDefault() default true; +} diff --git a/addon-javabean/src/test/java/org/springframework/roo/addon/javabean/JavaBeanAnnotationValuesTest.java b/addon-javabean/src/test/java/org/springframework/roo/addon/javabean/JavaBeanAnnotationValuesTest.java new file mode 100644 index 000000000..bf8df590c --- /dev/null +++ b/addon-javabean/src/test/java/org/springframework/roo/addon/javabean/JavaBeanAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.javabean; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link JavaBeanAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JavaBeanAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooJavaBean.class; + } + + @Override + protected Class getValuesClass() { + return JavaBeanAnnotationValues.class; + } +} diff --git a/addon-jdbc/pom.xml b/addon-jdbc/pom.xml new file mode 100644 index 000000000..93d697979 --- /dev/null +++ b/addon-jdbc/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.jdbc + bundle + Spring Roo - Addon - JDBC Driver Acquisition + Support for configuration of persistence settings in target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + \ No newline at end of file diff --git a/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/JdbcDriverManager.java b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/JdbcDriverManager.java new file mode 100644 index 000000000..b6b0aa5ad --- /dev/null +++ b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/JdbcDriverManager.java @@ -0,0 +1,29 @@ +package org.springframework.roo.addon.jdbc; + +import java.sql.Driver; + +/** + * Locates a JDBC driver and returns an instantiated version thereof. + *

    + * An implementation may inform the user where such a driver can be obtained if + * it is not presently available. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface JdbcDriverManager { + + /** + * Attempts to locate and instantiate the specified JDBC driver. + *

    + * The JDBC driver must provide a public no-argument constructor. + * + * @param driverClassName to load (required) + * @param displayAddOns display available add-ons if possible (required) + * @return the driver, or null if the driver could not be located + * @throws RuntimeException if the driver was located but could not be + * instantiated + */ + Driver loadDriver(String driverClassName, boolean displayAddOns) + throws RuntimeException; +} diff --git a/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/JdbcDriverProvider.java b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/JdbcDriverProvider.java new file mode 100644 index 000000000..59ccfe647 --- /dev/null +++ b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/JdbcDriverProvider.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.jdbc.polling; + +import java.sql.Driver; + +import org.springframework.roo.addon.jdbc.JdbcDriverManager; +import org.springframework.roo.addon.jdbc.polling.internal.PollingJdbcDriverManager; + +/** + * Represents an implementation capable of participating in a + * {@link PollingJdbcDriverManager}. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface JdbcDriverProvider { + + /** + * See {@link JdbcDriverManager#loadDriver(String, boolean)} for + * description. + */ + Driver loadDriver(String driverClassName) throws RuntimeException; +} diff --git a/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/internal/CommonJdbcDriverProvider.java b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/internal/CommonJdbcDriverProvider.java new file mode 100644 index 000000000..01c99dc55 --- /dev/null +++ b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/internal/CommonJdbcDriverProvider.java @@ -0,0 +1,64 @@ +package org.springframework.roo.addon.jdbc.polling.internal; + +import java.sql.Driver; +import java.sql.DriverManager; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jdbc.polling.JdbcDriverProvider; +import org.springframework.roo.support.osgi.BundleFindingUtils; + +/** + * Basic implementation of {@link JdbcDriverProvider} that provides common JDBC + * drivers. To be returned by this provider, it is necessary the JDBC driver has + * been declared as an optional import within the JDBC add-on's OSGi bundle + * manifest. + * + * @author Alan Stewart + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class CommonJdbcDriverProvider implements JdbcDriverProvider { + + private BundleContext bundleContext; + + protected void activate(final ComponentContext context) { + bundleContext = context.getBundleContext(); + } + + protected void deactivate(final ComponentContext context) { + bundleContext = null; + } + + public Driver loadDriver(final String driverClassName) + throws RuntimeException { + // Try a search + final Class clazz = BundleFindingUtils.findFirstBundleWithType( + bundleContext, driverClassName); + + if (clazz == null) { + // Let's give up given it doesn't seem to be loadable + return null; + } + + if (!Driver.class.isAssignableFrom(clazz)) { + // That's weird, it doesn't seem to be a driver + return null; + } + + // Time to create it and register etc + try { + final Driver result = (Driver) clazz.newInstance(); + DriverManager.registerDriver(result); + return result; + } + catch (final Exception e) { + throw new IllegalStateException("Unable to load JDBC driver '" + + driverClassName + "'", e); + } + } +} diff --git a/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/internal/PollingJdbcDriverManager.java b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/internal/PollingJdbcDriverManager.java new file mode 100644 index 000000000..1b995fdfc --- /dev/null +++ b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/polling/internal/PollingJdbcDriverManager.java @@ -0,0 +1,100 @@ +package org.springframework.roo.addon.jdbc.polling.internal; + +import java.sql.Driver; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.jdbc.JdbcDriverManager; +import org.springframework.roo.addon.jdbc.polling.JdbcDriverProvider; +import org.springframework.roo.support.api.AddOnSearch; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Polls all OSGi-located {@link JdbcDriverProvider} instances and returns the + * first JDBC driver provided by an instance. + *

    + * Failing the location of a suitable {@link JdbcDriverProvider}, automatically + * suggests an add-on which may be able to provide the driver as a console + * message. + * + * @author Alan Stewart + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +@Reference(name = "jdbcDriverProvider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = JdbcDriverProvider.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class PollingJdbcDriverManager implements JdbcDriverManager { + + private static final Logger LOGGER = HandlerUtils + .getLogger(PollingJdbcDriverManager.class); + + @Reference private AddOnSearch addOnSearch; + private final Set providers = new HashSet(); + + protected void bindJdbcDriverProvider(final JdbcDriverProvider listener) { + synchronized (providers) { + providers.add(listener); + } + } + + public Driver loadDriver(final String driverClassName, + final boolean displayAddOns) throws RuntimeException { + Validate.notBlank(driverClassName, "Driver class name required"); + synchronized (providers) { + for (final JdbcDriverProvider provider : providers) { + final Driver driver = provider.loadDriver(driverClassName); + if (driver != null) { + return driver; + } + } + + if (!displayAddOns) { + // Caller requested add-on information not be displayed (might + // be in a TAB assist section etc) + return null; + } + + // No implementation could provide it + + // Compute a suitable search term for a JDBC driver + final String searchTerms = "#jdbcdriver,driverclass:" + + driverClassName; + + // Do a silent (console message free) lookup of matches + final Integer matches = addOnSearch.searchAddOns(false, + searchTerms, false, 1, 99, false, false, false, null); + + // Render to screen if required + if (matches == null) { + LOGGER.info("Spring Roo automatic add-on discovery service currently unavailable"); + } + else if (matches == 0) { + LOGGER.info("addon search --requiresDescription \"" + + searchTerms + "\" found no matches"); + } + else if (matches > 0) { + LOGGER.info("Located add-on" + (matches == 1 ? "" : "s") + + " that may offer this JDBC driver"); + addOnSearch.searchAddOns(true, searchTerms, false, 1, 99, + false, false, false, null); + } + + return null; + } + } + + protected void unbindJdbcDriverProvider(final JdbcDriverProvider listener) { + synchronized (providers) { + providers.remove(listener); + } + } +} diff --git a/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/util/DerbyUtils.java b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/util/DerbyUtils.java new file mode 100644 index 000000000..e188e2662 --- /dev/null +++ b/addon-jdbc/src/main/java/org/springframework/roo/addon/jdbc/util/DerbyUtils.java @@ -0,0 +1,28 @@ +package org.springframework.roo.addon.jdbc.util; + +import java.io.OutputStream; + +/** + * Utility class to direct output of derby logging to an {@link OutputStream}, + * thus suppressing the creation of the derby.log file. + *

    + * To take effect in Roo, either add the following to the roo-dev and + * roo-dev.bat files
    + * -Dderby.stream.error.field=org.springframework.roo.addon.jdbc.util.DerbyUtils.DEV_NULL + *
    + * or entering, + * export ROO_OPTS="-Dderby.stream.error.field=org.springframework.roo.addon.jdbc.util.DerbyUtils.DEV_NULL" + *
    + * on the command line before starting the Roo shell. + * + * @author Alan Stewart + * @since 1.1 + */ +public abstract class DerbyUtils { + + public static final OutputStream DEV_NULL = new OutputStream() { + @Override + public void write(final int b) { + } + }; +} diff --git a/addon-jms/pom.xml b/addon-jms/pom.xml new file mode 100644 index 000000000..03417b4cd --- /dev/null +++ b/addon-jms/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.jms + bundle + Spring Roo - Addon - JMS + Support for configuring Java Messaging System settings in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsCommands.java b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsCommands.java new file mode 100644 index 000000000..a921ab242 --- /dev/null +++ b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsCommands.java @@ -0,0 +1,76 @@ +package org.springframework.roo.addon.jms; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Commands for the 'install jms' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class JmsCommands implements CommandMarker { + + @Reference private JmsOperations jmsOperations; + @Reference private StaticFieldConverter staticFieldConverter; + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(JmsProvider.class); + staticFieldConverter.add(JmsDestinationType.class); + } + + @CliCommand(value = "jms listener class", help = "Create an asynchronous JMS consumer") + public void addJmsListener( + @CliOption(key = "class", mandatory = true, help = "The name of the class to create") final JavaType typeName, + @CliOption(key = { "destinationName" }, mandatory = false, unspecifiedDefaultValue = "myDestination", specifiedDefaultValue = "myDestination", help = "The name of the destination") final String name, + @CliOption(key = { "destinationType" }, mandatory = false, unspecifiedDefaultValue = "QUEUE", specifiedDefaultValue = "QUEUE", help = "The type of the destination") final JmsDestinationType type) { + + jmsOperations.addJmsListener(typeName, name, type); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(JmsProvider.class); + staticFieldConverter.remove(JmsDestinationType.class); + } + + @CliCommand(value = "field jms template", help = "Insert a JmsOperations field into an existing type") + public void injectJmsProducer( + @CliOption(key = { "", "fieldName" }, mandatory = false, specifiedDefaultValue = "jmsOperations", unspecifiedDefaultValue = "jmsOperations", help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "async", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates if the injected method should be executed asynchronously") final boolean async) { + + jmsOperations.injectJmsTemplate(typeName, fieldName, async); + } + + @CliCommand(value = "jms setup", help = "Install a JMS provider into your project") + public void installJms( + @CliOption(key = { "provider" }, mandatory = true, help = "The persistence provider to support") final JmsProvider jmsProvider, + @CliOption(key = { "destinationName" }, mandatory = false, unspecifiedDefaultValue = "myDestination", specifiedDefaultValue = "myDestination", help = "The name of the destination") final String name, + @CliOption(key = { "destinationType" }, mandatory = false, unspecifiedDefaultValue = "QUEUE", specifiedDefaultValue = "QUEUE", help = "The type of the destination") final JmsDestinationType type) { + + jmsOperations.installJms(jmsProvider, name, type); + } + + @CliAvailabilityIndicator({ "field jms template", "jms listener class" }) + public boolean isInsertJmsAvailable() { + return jmsOperations.isManageJmsAvailable(); + } + + @CliAvailabilityIndicator("jms setup") + public boolean isInstallJmsAvailable() { + return jmsOperations.isJmsInstallationPossible(); + } +} \ No newline at end of file diff --git a/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsDestinationType.java b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsDestinationType.java new file mode 100644 index 000000000..58b0a94c7 --- /dev/null +++ b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsDestinationType.java @@ -0,0 +1,11 @@ +package org.springframework.roo.addon.jms; + +/** + * JMS destination types known to the JMS add-on. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public enum JmsDestinationType { + DURABLE_TOPIC, QUEUE, TOPIC; +} \ No newline at end of file diff --git a/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsOperations.java b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsOperations.java new file mode 100644 index 000000000..eb69d10a8 --- /dev/null +++ b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsOperations.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.jms; + +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Interface to {@link JmsOperationsImpl}. + * + * @author Ben Alex + */ +public interface JmsOperations { + + void addJmsListener(JavaType targetType, String name, + JmsDestinationType destinationType); + + void injectJmsTemplate(JavaType targetType, JavaSymbolName fieldName, + boolean async); + + void installJms(JmsProvider jmsProvider, String name, + JmsDestinationType destinationType); + + boolean isJmsInstallationPossible(); + + boolean isManageJmsAvailable(); +} \ No newline at end of file diff --git a/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsOperationsImpl.java b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsOperationsImpl.java new file mode 100644 index 000000000..16195ec4f --- /dev/null +++ b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsOperationsImpl.java @@ -0,0 +1,376 @@ +package org.springframework.roo.addon.jms; + +import static java.lang.reflect.Modifier.PRIVATE; +import static java.lang.reflect.Modifier.PUBLIC; +import static java.lang.reflect.Modifier.TRANSIENT; +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.SpringJavaType.ASYNC; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; +import static org.springframework.roo.model.SpringJavaType.JMS_OPERATIONS; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Provides JMS configuration operations. + * + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class JmsOperationsImpl implements JmsOperations { + + @Reference private FileManager fileManager; + @Reference private ProjectOperations projectOperations; + @Reference private PropFileOperations propFileOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + private void addDefaultDestination(final Document appCtx, final String name) { + // If we do already have a default destination configured then do + // nothing + final Element root = appCtx.getDocumentElement(); + if (null != XmlUtils + .findFirstElement( + "/beans/bean[@class = 'org.springframework.jms.core.JmsTemplate']/property[@name = 'defaultDestination']", + root)) { + return; + } + // Otherwise add it + final Element jmsTemplate = XmlUtils + .findRequiredElement( + "/beans/bean[@class = 'org.springframework.jms.core.JmsTemplate']", + root); + final Element defaultDestination = appCtx.createElement("property"); + defaultDestination.setAttribute("ref", name); + defaultDestination.setAttribute("name", "defaultDestination"); + jmsTemplate.appendChild(defaultDestination); + } + + public void addJmsListener(final JavaType targetType, final String name, + final JmsDestinationType destinationType) { + Validate.notNull(targetType, "Java type required"); + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(targetType, projectOperations + .getPathResolver().getFocusedPath(Path.SRC_MAIN_JAVA)); + + final List methods = new ArrayList(); + final List parameterTypes = Arrays.asList(OBJECT); + final List parameterNames = Arrays + .asList(new JavaSymbolName("message")); + + // Create some method content to get people started + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("System.out.println(\"JMS message received: \" + message);"); + methods.add(new MethodMetadataBuilder(declaredByMetadataId, PUBLIC, + new JavaSymbolName("onMessage"), JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder)); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, PUBLIC, targetType, + PhysicalTypeCategory.CLASS); + cidBuilder.setDeclaredMethods(methods); + + // Determine the canonical filename + final String physicalLocationCanonicalPath = getPhysicalLocationCanonicalPath(declaredByMetadataId); + + // Check the file doesn't already exist + Validate.isTrue(!fileManager.exists(physicalLocationCanonicalPath), + "%s already exists", projectOperations.getPathResolver() + .getFriendlyName(physicalLocationCanonicalPath)); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + + final String jmsContextPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext-jms.xml"); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(jmsContextPath)); + final Element root = document.getDocumentElement(); + + Element listenerContainer = DomUtils.findFirstElementByName( + "jms:listener-container", root); + if (listenerContainer != null + && destinationType.name().equalsIgnoreCase( + listenerContainer.getAttribute("destination-type"))) { + listenerContainer = document + .createElement("jms:listener-container"); + listenerContainer.setAttribute("connection-factory", "jmsFactory"); + listenerContainer.setAttribute("destination-type", destinationType + .name().toLowerCase()); + root.appendChild(listenerContainer); + } + + if (listenerContainer != null) { + final Element jmsListener = document.createElement("jms:listener"); + jmsListener.setAttribute("ref", + StringUtils.uncapitalize(targetType.getSimpleTypeName())); + jmsListener.setAttribute("method", "onMessage"); + jmsListener.setAttribute("destination", name); + + final Element bean = document.createElement("bean"); + bean.setAttribute("class", targetType.getFullyQualifiedTypeName()); + bean.setAttribute("id", + StringUtils.uncapitalize(targetType.getSimpleTypeName())); + root.appendChild(bean); + + listenerContainer.appendChild(jmsListener); + } + + fileManager.createOrUpdateTextFileIfRequired(jmsContextPath, + XmlUtils.nodeToString(document), false); + } + + /** + * Creates the injected method that sends a JMS message + * + * @param fieldName + * @param declaredByMetadataId + * @param asynchronous whether the JMS message should be sent asynchronously + * @return a non-null builder + */ + private MethodMetadataBuilder createSendMessageMethod( + final JavaSymbolName fieldName, final String declaredByMetadataId, + final boolean asynchronous) { + final List parameterTypes = Arrays.asList(JavaType.OBJECT); + final List parameterNames = Arrays + .asList(new JavaSymbolName("messageObject")); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(fieldName + + ".convertAndSend(messageObject);"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + declaredByMetadataId, PUBLIC, + new JavaSymbolName("sendMessage"), JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + if (asynchronous) { + methodBuilder.addAnnotation(new AnnotationMetadataBuilder(ASYNC)); + } + return methodBuilder; + } + + /** + * Ensures that the Spring config files contain the necessary elements and + * properties to support asynchronous tasks. + */ + private void ensureSpringAsynchronousSupportEnabled() { + final String contextPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext.xml"); + final Document appContext = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = appContext.getDocumentElement(); + + if (DomUtils.findFirstElementByName("task:annotation-driven", root) == null) { + if (root.getAttribute("xmlns:task").length() == 0) { + root.setAttribute("xmlns:task", + "http://www.springframework.org/schema/task"); + root.setAttribute( + "xsi:schemaLocation", + root.getAttribute("xsi:schemaLocation") + + " http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd"); + } + root.appendChild(new XmlElementBuilder("task:annotation-driven", + appContext).addAttribute("executor", "asyncExecutor") + .build()); + root.appendChild(new XmlElementBuilder("task:executor", appContext) + .addAttribute("id", "asyncExecutor") + .addAttribute("pool-size", "${executor.poolSize}").build()); + + fileManager.createOrUpdateTextFileIfRequired(contextPath, + XmlUtils.nodeToString(appContext), false); + + propFileOperations.addPropertyIfNotExists(Path.SPRING_CONFIG_ROOT + .getModulePathId(projectOperations.getFocusedModuleName()), + "jms.properties", "executor.poolSize", "10", true); + } + } + + private String getPhysicalLocationCanonicalPath( + final String physicalTypeIdentifier) { + Validate.isTrue(PhysicalTypeIdentifier.isValid(physicalTypeIdentifier), + "Physical type identifier is invalid"); + final JavaType javaType = PhysicalTypeIdentifier + .getJavaType(physicalTypeIdentifier); + final LogicalPath path = PhysicalTypeIdentifier + .getPath(physicalTypeIdentifier); + return projectOperations.getPathResolver().getIdentifier(path, + javaType.getRelativeFileName()); + } + + private boolean hasJmsContext() { + return fileManager.exists(projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext-jms.xml")); + } + + public void injectJmsTemplate(final JavaType targetType, + final JavaSymbolName fieldName, final boolean asynchronous) { + Validate.notNull(targetType, "Java type required"); + Validate.notNull(fieldName, "Field name required"); + + final ClassOrInterfaceTypeDetails targetTypeDetails = typeLocationService + .getTypeDetails(targetType); + Validate.isTrue(targetTypeDetails != null, + "Cannot locate source for '%s'", + targetType.getFullyQualifiedTypeName()); + + final String declaredByMetadataId = targetTypeDetails + .getDeclaredByMetadataId(); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + targetTypeDetails); + + // Create the field + cidBuilder.addField(new FieldMetadataBuilder(declaredByMetadataId, + PRIVATE | TRANSIENT, Arrays + .asList(new AnnotationMetadataBuilder(AUTOWIRED)), + fieldName, JMS_OPERATIONS)); + + // Create the method + cidBuilder.addMethod(createSendMessageMethod(fieldName, + declaredByMetadataId, asynchronous)); + + if (asynchronous) { + ensureSpringAsynchronousSupportEnabled(); + } + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void installJms(final JmsProvider jmsProvider, final String name, + final JmsDestinationType destinationType) { + Validate.isTrue(isJmsInstallationPossible(), "Project not available"); + Validate.notNull(jmsProvider, "JMS provider required"); + + final String jmsContextPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext-jms.xml"); + + String amq; + String destType; + switch (destinationType) { + case TOPIC: + amq = destType = "topic"; + break; + case DURABLE_TOPIC: + amq = "topic"; + destType = "durableTopic"; + break; + default: + amq = destType = "queue"; + break; + } + + final InputStream in; + if (fileManager.exists(jmsContextPath)) { + in = fileManager.getInputStream(jmsContextPath); + } + else { + in = FileUtils.getInputStream(getClass(), + "applicationContext-jms-template.xml"); + Validate.notNull(in, + "Could not acquire applicationContext-jms.xml template"); + } + final Document document = XmlUtils.readXml(in); + + final Element root = document.getDocumentElement(); + + if (StringUtils.isNotBlank(name)) { + final Element destination = document.createElement("amq:" + amq); + destination.setAttribute("physicalName", name); + destination.setAttribute("id", name); + root.appendChild(destination); + addDefaultDestination(document, name); + } + + Element listenerContainer = XmlUtils.findFirstElement( + "/beans/listener-container[@destination-type = '" + destType + + "']", root); + if (listenerContainer == null) { + listenerContainer = document + .createElement("jms:listener-container"); + listenerContainer.setAttribute("connection-factory", "jmsFactory"); + listenerContainer.setAttribute("destination-type", destType); + root.appendChild(listenerContainer); + } + + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(jmsContextPath, + XmlUtils.nodeToString(document), false); + + updateConfiguration(jmsProvider); + } + + public boolean isJmsInstallationPossible() { + return projectOperations.isFocusedProjectAvailable(); + } + + public boolean isManageJmsAvailable() { + return projectOperations.isFocusedProjectAvailable() && hasJmsContext(); + } + + private void updateConfiguration(final JmsProvider jmsProvider) { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + + final List springDependencies = XmlUtils.findElements( + "/configuration/springJms/dependencies/dependency", + configuration); + for (final Element dependencyElement : springDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + + final List jmsDependencies = XmlUtils.findElements( + "/configuration/jmsProviders/provider[@id = '" + + jmsProvider.name() + "']/dependencies/dependency", + configuration); + for (final Element dependencyElement : jmsDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + + projectOperations.addDependencies( + projectOperations.getFocusedModuleName(), dependencies); + } +} \ No newline at end of file diff --git a/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsProvider.java b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsProvider.java new file mode 100644 index 000000000..6e6dc38a5 --- /dev/null +++ b/addon-jms/src/main/java/org/springframework/roo/addon/jms/JmsProvider.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.jms; + +/** + * JMS providers known to the JMS add-on. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public enum JmsProvider { + ACTIVEMQ_IN_MEMORY( + "org.apache.activemq.store.memory.MemoryPersistenceAdapter"); + + private String adapter; + + private JmsProvider(final String adapter) { + this.adapter = adapter; + } + + public String getAdapter() { + return adapter; + } +} \ No newline at end of file diff --git a/addon-jms/src/main/resources/org/springframework/roo/addon/jms/applicationContext-jms-template.xml b/addon-jms/src/main/resources/org/springframework/roo/addon/jms/applicationContext-jms-template.xml new file mode 100644 index 000000000..c8e7a6538 --- /dev/null +++ b/addon-jms/src/main/resources/org/springframework/roo/addon/jms/applicationContext-jms-template.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-jms/src/main/resources/org/springframework/roo/addon/jms/configuration.xml b/addon-jms/src/main/resources/org/springframework/roo/addon/jms/configuration.xml new file mode 100644 index 000000000..03fc35179 --- /dev/null +++ b/addon-jms/src/main/resources/org/springframework/roo/addon/jms/configuration.xml @@ -0,0 +1,54 @@ + + + + + + org.springframework + spring-beans + ${spring.version} + + + org.springframework + spring-jms + ${spring.version} + + + org.apache.geronimo.specs + geronimo-jms_1.1_spec + 1.1 + + + + + + + + org.apache.activemq + activemq-core + 5.4.2 + + + commons-logging + commons-logging + + + commons-logging + commons-logging-api + + + + + org.apache.xbean + xbean-spring + 3.6 + + + commons-logging + commons-logging + + + + + + + \ No newline at end of file diff --git a/addon-jpa/pom.xml b/addon-jpa/pom.xml new file mode 100644 index 000000000..073af7082 --- /dev/null +++ b/addon-jpa/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.jpa + bundle + Spring Roo - Addon - JPA + Support for Java Persistence API (JPA) features and domain entities in the target project + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.addon.test + + + org.springframework.roo + org.springframework.roo.addon.serializable + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/AbstractIdentifierServiceAwareMetadataProvider.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/AbstractIdentifierServiceAwareMetadataProvider.java new file mode 100644 index 000000000..6ca618dbd --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/AbstractIdentifierServiceAwareMetadataProvider.java @@ -0,0 +1,77 @@ +package org.springframework.roo.addon.jpa; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.springframework.roo.addon.jpa.identifier.Identifier; +import org.springframework.roo.addon.jpa.identifier.IdentifierService; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.model.JavaType; + +/** + * Abstract class to make {@link IdentifierService} collection available to + * subclasses. + * + * @author Alan Stewart + * @author Ben Alex + * @since 1.1 + */ +@Component(componentAbstract = true) +@Reference(name = "identifierService", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = IdentifierService.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public abstract class AbstractIdentifierServiceAwareMetadataProvider extends + AbstractItdMetadataProvider { + + private final Set identifierServices = new HashSet(); + + protected void bindIdentifierService( + final IdentifierService identifierService) { + synchronized (identifierServices) { + identifierServices.add(identifierService); + } + } + + /** + * Locates any {@link Identifier} that is applicable to this + * {@link JavaType}. + *

    + * See {@link IdentifierService#getIdentifiers(JavaType)} for the full + * contract of what this method returns. Note this method simply returns the + * first non-null result of invoking + * {@link IdentifierService#getIdentifiers(JavaType)}. It returns null if no + * provider is authoritative. + * + * @param javaType the entity or PK identifier class for which column + * information is desired (required) + * @return the applicable identifiers, or null if no registered + * {@link IdentifierService} was authoritative for this type TODO + * made obsolete by {@link PersistenceMemberLocator}? + */ + protected List getIdentifiersForType(final JavaType javaType) { + List identifierServiceResult = null; + synchronized (identifierServices) { + for (final IdentifierService service : identifierServices) { + identifierServiceResult = service.getIdentifiers(javaType); + if (identifierServiceResult != null) { + // Someone has authoritatively indicated the fields for this + // PK, so we don't need to continue looping + break; + } + } + } + return identifierServiceResult; + } + + protected void unbindIdentifierService( + final IdentifierService identifierService) { + synchronized (identifierServices) { + identifierServices.remove(identifierService); + } + } +} \ No newline at end of file diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/DatabaseDotComOperations.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/DatabaseDotComOperations.java new file mode 100644 index 000000000..43f42d835 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/DatabaseDotComOperations.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.jpa; + +import org.springframework.roo.project.Feature; + +/** + * Provides Database.com configuration operations. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface DatabaseDotComOperations extends Feature { +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/DatabaseDotComOperationsImpl.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/DatabaseDotComOperationsImpl.java new file mode 100644 index 000000000..02d2bd894 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/DatabaseDotComOperationsImpl.java @@ -0,0 +1,82 @@ +package org.springframework.roo.addon.jpa; + +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link DatabaseDotComOperations}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class DatabaseDotComOperationsImpl implements DatabaseDotComOperations { + +protected final static Logger LOGGER = HandlerUtils.getLogger(GaeOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private ProjectOperations projectOperations; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + public String getName() { + return FeatureNames.DATABASE_DOT_COM; + } + + public boolean isInstalledInModule(final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final Pom pom = projectOperations.getPomFromModuleName(moduleName); + if (pom == null) { + return false; + } + for (final Plugin buildPlugin : pom.getBuildPlugins()) { + if ("com.force.sdk".equals(buildPlugin.getArtifactId())) { + return true; + } + } + return false; + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on DatabaseDotComOperationsImpl."); + return null; + } + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/GaeOperations.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/GaeOperations.java new file mode 100644 index 000000000..ff1ea7043 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/GaeOperations.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.jpa; + +import org.springframework.roo.project.Feature; + +/** + * Provides GAE configuration operations. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface GaeOperations extends Feature { +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/GaeOperationsImpl.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/GaeOperationsImpl.java new file mode 100644 index 000000000..76d34beb3 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/GaeOperationsImpl.java @@ -0,0 +1,82 @@ +package org.springframework.roo.addon.jpa; + +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link GaeOperations}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class GaeOperationsImpl implements GaeOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(GaeOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private ProjectOperations projectOperations; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + public String getName() { + return FeatureNames.GAE; + } + + public boolean isInstalledInModule(final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final Pom pom = projectOperations.getPomFromModuleName(moduleName); + if (pom == null) { + return false; + } + for (final Plugin buildPlugin : pom.getBuildPlugins()) { + if ("appengine-maven-plugin".equals(buildPlugin.getArtifactId())) { + return true; + } + } + return false; + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on GaeOperationsImpl."); + return null; + } + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JdbcDatabase.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JdbcDatabase.java new file mode 100644 index 000000000..4e6a4b2e2 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JdbcDatabase.java @@ -0,0 +1,91 @@ +package org.springframework.roo.addon.jpa; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides information related to JDBC database configuration. + * + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +public enum JdbcDatabase { + + DATABASE_DOT_COM("DATABASE.COM", "", + "force://HOST_NAME;user=USER_NAME;password=PASSWORD"), // + DB2_400("DB2_400", "com.ibm.as400.access.AS400JDBCDriver", + "jdbc:as400://HOST_NAME"), // + DB2_EXPRESS_C("DB2_EXPRESS_C", "com.ibm.db2.jcc.DB2Driver", + "jdbc:db2://HOST_NAME:50000"), // + DERBY_CLIENT("DERBY_CLIENT", "org.apache.derby.jdbc.ClientDriver", + "jdbc:derby://HOST_NAME:1527/TO_BE_CHANGED_BY_ADDON;create=true"), // + DERBY_EMBEDDED("DERBY_EMBEDDED", "org.apache.derby.jdbc.EmbeddedDriver", + "jdbc:derby:TO_BE_CHANGED_BY_ADDON;create=true"), // + FIREBIRD("FIREBIRD", "org.firebirdsql.jdbc.FBDriver", + "jdbc:firebirdsql://HOST_NAME:3050"), // + GOOGLE_APP_ENGINE("GAE", "", "appengine"), // + H2_IN_MEMORY("H2", "org.h2.Driver", + "jdbc:h2:mem:TO_BE_CHANGED_BY_ADDON;DB_CLOSE_DELAY=-1"), // + HYPERSONIC_IN_MEMORY("HYPERSONIC", "org.hsqldb.jdbcDriver", + "jdbc:hsqldb:mem:TO_BE_CHANGED_BY_ADDON"), // + HYPERSONIC_PERSISTENT("HYPERSONIC", "org.hsqldb.jdbcDriver", + "jdbc:hsqldb:file:TO_BE_CHANGED_BY_ADDON;shutdown=true"), // + MSSQL("MSSQL", "net.sourceforge.jtds.jdbc.Driver", + "jdbc:jtds:sqlserver://HOST_NAME:1433/TO_BE_CHANGED_BY_ADDON"), // + MYSQL("MYSQL", "com.mysql.jdbc.Driver", "jdbc:mysql://HOST_NAME:3306"), // + ORACLE("ORACLE", "oracle.jdbc.OracleDriver", + "jdbc:oracle:thin:@HOST_NAME:1521"), // + POSTGRES("POSTGRES", "org.postgresql.Driver", + "jdbc:postgresql://HOST_NAME:5432"), // + SYBASE("SYBASE", "net.sourceforge.jtds.jdbc.Driver", + "jdbc:jtds:sybase://HOST_NAME:5000/TO_BE_CHANGED_BY_ADDON;TDS=4.2"); + + private final String connectionString; + private final String driverClassName; + private final String key; + + /** + * Constructor + * + * @param key the internal name for this type of database (required) + * @param driverClassName + * @param connectionString the JDBC connection URL template for this type of + * database (required) + */ + private JdbcDatabase(final String key, final String driverClassName, + final String connectionString) { + Validate.notBlank(connectionString, "Connection string is required"); + Validate.notBlank(key, "Key is required"); + this.connectionString = connectionString; + this.driverClassName = driverClassName; + this.key = key; + } + + public String getConfigPrefix() { + return "/configuration/databases/database[@id='" + key + "']"; + } + + public String getConnectionString() { + return connectionString; + } + + public String getDriverClassName() { + return driverClassName; + } + + public String getKey() { + return key; + + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + builder.append("key", key); + builder.append("driver class name", driverClassName); + builder.append("connection string", connectionString); + return builder.toString(); + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaCommands.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaCommands.java new file mode 100644 index 000000000..b2715b6ee --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaCommands.java @@ -0,0 +1,410 @@ +package org.springframework.roo.addon.jpa; + +import static org.springframework.roo.model.GoogleJavaType.GAE_DATASTORE_KEY; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.RooJavaType.ROO_EQUALS; +import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; +import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE; +import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; +import static org.springframework.roo.shell.OptionContexts.INTERFACE; +import static org.springframework.roo.shell.OptionContexts.SUPERCLASS; +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jpa.entity.RooJpaEntity; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.addon.test.IntegrationTestOperations; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.operations.InheritanceType; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.ReservedWords; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Commands for the JPA add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class JpaCommands implements CommandMarker { + + private static Logger LOGGER = HandlerUtils.getLogger(JpaCommands.class); + private static final AnnotationMetadataBuilder ROO_EQUALS_BUILDER = new AnnotationMetadataBuilder( + ROO_EQUALS); + private static final AnnotationMetadataBuilder ROO_JAVA_BEAN_BUILDER = new AnnotationMetadataBuilder( + ROO_JAVA_BEAN); + private static final AnnotationMetadataBuilder ROO_SERIALIZABLE_BUILDER = new AnnotationMetadataBuilder( + ROO_SERIALIZABLE); + private static final AnnotationMetadataBuilder ROO_TO_STRING_BUILDER = new AnnotationMetadataBuilder( + ROO_TO_STRING); + + @Reference private IntegrationTestOperations integrationTestOperations; + @Reference private JpaOperations jpaOperations; + @Reference private ProjectOperations projectOperations; + @Reference private PropFileOperations propFileOperations; + @Reference private StaticFieldConverter staticFieldConverter; + @Reference private TypeLocationService typeLocationService; + + @CliCommand(value = "embeddable", help = "Creates a new Java class source file with the JPA @Embeddable annotation in SRC_MAIN_JAVA") + public void createEmbeddableClass( + @CliOption(key = "class", optionContext = UPDATE_PROJECT, mandatory = true, help = "The name of the class to create") final JavaType name, + @CliOption(key = "serializable", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether the generated class should implement java.io.Serializable") final boolean serializable, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(name); + } + + jpaOperations.newEmbeddableClass(name, serializable); + } + + @CliCommand(value = "database properties list", help = "Shows database configuration details") + public SortedSet databaseProperties() { + return jpaOperations.getDatabaseProperties(); + } + + @CliCommand(value = "database properties remove", help = "Removes a particular database property") + public void databaseRemove( + @CliOption(key = { "", "key" }, mandatory = true, help = "The property key that should be removed") final String key) { + + propFileOperations.removeProperty(Path.SPRING_CONFIG_ROOT + .getModulePathId(projectOperations.getFocusedModuleName()), + "database.properties", key); + } + + @CliCommand(value = "database properties set", help = "Changes a particular database property") + public void databaseSet( + @CliOption(key = "key", mandatory = true, help = "The property key that should be changed") final String key, + @CliOption(key = "value", mandatory = true, help = "The new vale for this property key") final String value) { + + propFileOperations.changeProperty(Path.SPRING_CONFIG_ROOT + .getModulePathId(projectOperations.getFocusedModuleName()), + "database.properties", key, value); + } + + @CliAvailabilityIndicator({ "database properties list", + "database properties remove", "database properties set" }) + public boolean hasDatabaseProperties() { + return isJpaSetupAvailable() && jpaOperations.hasDatabaseProperties(); + } + + @CliCommand(value = "jpa setup", help = "Install or updates a JPA persistence provider in your project") + public void installJpa( + @CliOption(key = "provider", mandatory = true, help = "The persistence provider to support") final OrmProvider ormProvider, + @CliOption(key = "database", mandatory = true, help = "The database to support") final JdbcDatabase jdbcDatabase, + @CliOption(key = "applicationId", mandatory = false, unspecifiedDefaultValue = "the project's name", help = "The Google App Engine application identifier to use") final String applicationId, + @CliOption(key = "jndiDataSource", mandatory = false, help = "The JNDI datasource to use") final String jndi, + @CliOption(key = "hostName", mandatory = false, help = "The host name to use") final String hostName, + @CliOption(key = "databaseName", mandatory = false, help = "The database name to use") final String databaseName, + @CliOption(key = "userName", mandatory = false, help = "The username to use") final String userName, + @CliOption(key = "password", mandatory = false, help = "The password to use") final String password, + @CliOption(key = "transactionManager", mandatory = false, help = "The transaction manager name") final String transactionManager, + @CliOption(key = "persistenceUnit", mandatory = false, help = "The persistence unit name to be used in the persistence.xml file") final String persistenceUnit) { + + if (jdbcDatabase == JdbcDatabase.GOOGLE_APP_ENGINE + && ormProvider != OrmProvider.DATANUCLEUS) { + LOGGER.warning("Provider must be " + OrmProvider.DATANUCLEUS.name() + + " for the Google App Engine"); + return; + } + + if (jdbcDatabase == JdbcDatabase.DATABASE_DOT_COM + && ormProvider != OrmProvider.DATANUCLEUS) { + LOGGER.warning("Provider must be " + OrmProvider.DATANUCLEUS.name() + + " for Database.com"); + return; + } + + if (jdbcDatabase == JdbcDatabase.FIREBIRD && !isJdk6OrHigher()) { + LOGGER.warning("JDK must be 1.6 or higher to use Firebird"); + return; + } + + jpaOperations.configureJpa(ormProvider, jdbcDatabase, jndi, + applicationId, hostName, databaseName, userName, password, + transactionManager, persistenceUnit, + projectOperations.getFocusedModuleName()); + } + + @Deprecated + @CliCommand(value = "persistence setup", help = "Install or updates a JPA persistence provider in your project - deprecated, use 'jpa setup' instead") + public void installPersistence( + @CliOption(key = "provider", mandatory = true, help = "The persistence provider to support") final OrmProvider ormProvider, + @CliOption(key = "database", mandatory = true, help = "The database to support") final JdbcDatabase jdbcDatabase, + @CliOption(key = "applicationId", mandatory = false, unspecifiedDefaultValue = "the project's name", help = "The Google App Engine application identifier to use") final String applicationId, + @CliOption(key = "jndiDataSource", mandatory = false, help = "The JNDI datasource to use") final String jndi, + @CliOption(key = "hostName", mandatory = false, help = "The host name to use") final String hostName, + @CliOption(key = "databaseName", mandatory = false, help = "The database name to use") final String databaseName, + @CliOption(key = "userName", mandatory = false, help = "The username to use") final String userName, + @CliOption(key = "password", mandatory = false, help = "The password to use") final String password, + @CliOption(key = "transactionManager", mandatory = false, help = "The transaction manager name") final String transactionManager, + @CliOption(key = "persistenceUnit", mandatory = false, help = "The persistence unit name to be used in the persistence.xml file") final String persistenceUnit) { + + installJpa(ormProvider, jdbcDatabase, applicationId, jndi, hostName, + databaseName, userName, password, transactionManager, + persistenceUnit); + } + + @CliAvailabilityIndicator({ "jpa setup", "persistence setup" }) + public boolean isJpaSetupAvailable() { + return jpaOperations.isJpaInstallationPossible(); + } + + @CliAvailabilityIndicator({ "entity jpa", "embeddable" }) + public boolean isPersistentClassAvailable() { + return jpaOperations.isPersistentClassAvailable(); + } + + @CliCommand(value = "entity jpa", help = "Creates a new JPA persistent entity in SRC_MAIN_JAVA") + public void newPersistenceClassJpa( + @CliOption(key = "class", optionContext = UPDATE_PROJECT, mandatory = true, help = "Name of the entity to create") final JavaType name, + @CliOption(key = "extends", mandatory = false, unspecifiedDefaultValue = "java.lang.Object", optionContext = SUPERCLASS, help = "The superclass (defaults to java.lang.Object)") final JavaType superclass, + @CliOption(key = "implements", mandatory = false, optionContext = INTERFACE, help = "The interface to implement") final JavaType implementsType, + @CliOption(key = "abstract", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Whether the generated class should be marked as abstract") final boolean createAbstract, + @CliOption(key = "testAutomatically", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Create automatic integration tests for this entity") final boolean testAutomatically, + @CliOption(key = "table", mandatory = false, help = "The JPA table name to use for this entity") final String table, + @CliOption(key = "schema", mandatory = false, help = "The JPA table schema name to use for this entity") final String schema, + @CliOption(key = "catalog", mandatory = false, help = "The JPA table catalog name to use for this entity") final String catalog, + @CliOption(key = "identifierField", mandatory = false, help = "The JPA identifier field name to use for this entity") final String identifierField, + @CliOption(key = "identifierColumn", mandatory = false, help = "The JPA identifier field column to use for this entity") final String identifierColumn, + @CliOption(key = "identifierType", mandatory = false, optionContext = "java-lang,project", unspecifiedDefaultValue = "java.lang.Long", specifiedDefaultValue = "java.lang.Long", help = "The data type that will be used for the JPA identifier field (defaults to java.lang.Long)") final JavaType identifierType, + @CliOption(key = "versionField", mandatory = false, help = "The JPA version field name to use for this entity") final String versionField, + @CliOption(key = "versionColumn", mandatory = false, help = "The JPA version field column to use for this entity") final String versionColumn, + @CliOption(key = "versionType", mandatory = false, optionContext = "java-lang,project", unspecifiedDefaultValue = "java.lang.Integer", help = "The data type that will be used for the JPA version field (defaults to java.lang.Integer)") final JavaType versionType, + @CliOption(key = "inheritanceType", mandatory = false, help = "The JPA @Inheritance value (apply to base class)") final InheritanceType inheritanceType, + @CliOption(key = "mappedSuperclass", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Apply @MappedSuperclass for this entity") final boolean mappedSuperclass, + @CliOption(key = "equals", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether the generated class should implement equals and hashCode methods") final boolean equals, + @CliOption(key = "serializable", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether the generated class should implement java.io.Serializable") final boolean serializable, + @CliOption(key = "persistenceUnit", mandatory = false, help = "The persistence unit name to be used in the persistence.xml file") final String persistenceUnit, + @CliOption(key = "transactionManager", mandatory = false, help = "The transaction manager name") final String transactionManager, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords, + @CliOption(key = "entityName", mandatory = false, help = "The name used to refer to the entity in queries") final String entityName, + @CliOption(key = "sequenceName", mandatory = false, help = "The name of the sequence for incrementing sequence-driven primary keys") final String sequenceName, + @CliOption(key = "activeRecord", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "true", help = "Generate CRUD active record methods for this entity") final boolean activeRecord) { + Validate.isTrue(!identifierType.isPrimitive(), + "Identifier type cannot be a primitive"); + + // Check if exists other entity with the same name + Set currentEntities = typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(ROO_JAVA_BEAN); + + for(ClassOrInterfaceTypeDetails entity : currentEntities){ + // If exists, we can't create a duplicate entity + if(name.equals(entity.getName())){ + throw new IllegalArgumentException( + String.format("Entity '%s' already exists and cannot be created. Try to use other entity name on --class parameter.", name)); + } + } + + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(name); + } + if (testAutomatically && createAbstract) { + // We can't test an abstract class + throw new IllegalArgumentException( + "Automatic tests cannot be created for an abstract entity; remove the --testAutomatically or --abstract option"); + } + + // Reject attempts to name the entity "Test", due to possible clashes + // with data on demand (see ROO-50) + // We will allow this to happen, though if the user insists on it via + // --permitReservedWords (see ROO-666) + if (!BeanInfoUtils.isEntityReasonablyNamed(name)) { + if (permitReservedWords && testAutomatically) { + throw new IllegalArgumentException( + "Entity name cannot contain 'Test' or 'TestCase' as you are requesting tests; remove --testAutomatically or rename the proposed entity"); + } + if (!permitReservedWords) { + throw new IllegalArgumentException( + "Entity name rejected as conflicts with test execution defaults; please remove 'Test' and/or 'TestCase'"); + } + } + + // Create entity's annotations + final List annotationBuilder = new ArrayList(); + annotationBuilder.add(ROO_JAVA_BEAN_BUILDER); + annotationBuilder.add(ROO_TO_STRING_BUILDER); + annotationBuilder.add(getEntityAnnotationBuilder(table, schema, + catalog, identifierField, identifierColumn, identifierType, + versionField, versionColumn, versionType, inheritanceType, + mappedSuperclass, persistenceUnit, transactionManager, + entityName, sequenceName, activeRecord)); + if (equals) { + annotationBuilder.add(ROO_EQUALS_BUILDER); + } + if (serializable) { + annotationBuilder.add(ROO_SERIALIZABLE_BUILDER); + } + + // Produce the entity itself + jpaOperations.newEntity(name, createAbstract, superclass, + implementsType, annotationBuilder); + + // Create entity identifier class if required + if (!(identifierType.getPackage().getFullyQualifiedPackageName() + .startsWith("java.") || identifierType + .equals(GAE_DATASTORE_KEY))) { + jpaOperations.newIdentifier(identifierType, identifierField, + identifierColumn); + } + + if (testAutomatically) { + integrationTestOperations.newIntegrationTest(name); + } + } + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(JdbcDatabase.class); + staticFieldConverter.add(OrmProvider.class); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(JdbcDatabase.class); + staticFieldConverter.remove(OrmProvider.class); + } + + /** + * Returns a builder for the entity-related annotation to be added to a + * newly created JPA entity + * + * @param table + * @param schema + * @param catalog + * @param identifierField + * @param identifierColumn + * @param identifierType + * @param versionField + * @param versionColumn + * @param versionType + * @param inheritanceType + * @param mappedSuperclass + * @param persistenceUnit + * @param transactionManager + * @param entityName + * @param sequenceName + * @param activeRecord whether to generate active record CRUD methods for + * the entity + * @return a non-null builder + */ + private AnnotationMetadataBuilder getEntityAnnotationBuilder( + final String table, final String schema, final String catalog, + final String identifierField, final String identifierColumn, + final JavaType identifierType, final String versionField, + final String versionColumn, final JavaType versionType, + final InheritanceType inheritanceType, + final boolean mappedSuperclass, final String persistenceUnit, + final String transactionManager, final String entityName, + final String sequenceName, final boolean activeRecord) { + final AnnotationMetadataBuilder entityAnnotationBuilder = new AnnotationMetadataBuilder( + getEntityAnnotationType(activeRecord)); + + // Attributes that apply to all JPA entities (active record or not) + if (catalog != null) { + entityAnnotationBuilder.addStringAttribute("catalog", catalog); + } + if (entityName != null) { + entityAnnotationBuilder + .addStringAttribute("entityName", entityName); + } + if (sequenceName != null) { + entityAnnotationBuilder.addStringAttribute("sequenceName", + sequenceName); + } + if (identifierColumn != null) { + entityAnnotationBuilder.addStringAttribute("identifierColumn", + identifierColumn); + } + if (identifierField != null) { + entityAnnotationBuilder.addStringAttribute("identifierField", + identifierField); + } + if (!LONG_OBJECT.equals(identifierType)) { + entityAnnotationBuilder.addClassAttribute("identifierType", + identifierType); + } + if (inheritanceType != null) { + entityAnnotationBuilder.addStringAttribute("inheritanceType", + inheritanceType.name()); + } + if (mappedSuperclass) { + entityAnnotationBuilder.addBooleanAttribute("mappedSuperclass", + mappedSuperclass); + } + if (schema != null) { + entityAnnotationBuilder.addStringAttribute("schema", schema); + } + if (table != null) { + entityAnnotationBuilder.addStringAttribute("table", table); + } + if (versionColumn != null + && !RooJpaEntity.VERSION_COLUMN_DEFAULT.equals(versionColumn)) { + entityAnnotationBuilder.addStringAttribute("versionColumn", + versionColumn); + } + if (versionField != null + && !RooJpaEntity.VERSION_FIELD_DEFAULT.equals(versionField)) { + entityAnnotationBuilder.addStringAttribute("versionField", + versionField); + } + if (!JavaType.INT_OBJECT.equals(versionType)) { + entityAnnotationBuilder.addClassAttribute("versionType", + versionType); + } + + // Attributes that only apply to entities with CRUD active record + // methods + if (activeRecord) { + if (persistenceUnit != null) { + entityAnnotationBuilder.addStringAttribute("persistenceUnit", + persistenceUnit); + } + if (transactionManager != null) { + entityAnnotationBuilder.addStringAttribute( + "transactionManager", transactionManager); + } + } + + return entityAnnotationBuilder; + } + + /** + * Returns the type of annotation to put on the entity + * + * @param activeRecord whether the entity is to have CRUD active record + * methods generated + * @return a non-null type + */ + private JavaType getEntityAnnotationType(final boolean activeRecord) { + return activeRecord ? ROO_JPA_ACTIVE_RECORD : ROO_JPA_ENTITY; + } + + private boolean isJdk6OrHigher() { + final String ver = System.getProperty("java.version"); + return ver.indexOf("1.6.") > -1 || ver.indexOf("1.7.") > -1; + } +} \ No newline at end of file diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaOperations.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaOperations.java new file mode 100644 index 000000000..4e349e9de --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaOperations.java @@ -0,0 +1,94 @@ +package org.springframework.roo.addon.jpa; + +import java.util.List; +import java.util.SortedSet; + +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Feature; + +/** + * Provides JPA configuration and entity operations. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +public interface JpaOperations extends Feature { + + /** + * This method is responsible for managing all JPA related artifacts + * (META-INF/persistence.xml, applicationContext.xml, database.properties + * and the project pom.xml) + * + * @param ormProvider the ORM provider selected (Hibernate, OpenJPA, + * EclipseLink) + * @param database the database (HSQL, H2, MySql, etc) + * @param jndi the JNDI datasource + * @param applicationId the Google App Engine application identifier. + * Defaults to the project's name if not specified. + * @param hostName the host name where the database is + * @param databaseName the name of the database + * @param userName the username to connect to the database + * @param password the password to connect to the database + * @param transactionManager the transaction manager name defined in the + * applicationContext.xml file + * @param persistenceUnit the name of the persistence unit defined in the + * persistence.xml file + * @param moduleName + */ + void configureJpa(OrmProvider ormProvider, JdbcDatabase database, + String jndi, String applicationId, String hostName, + String databaseName, String userName, String password, + String transactionManager, String persistenceUnit, String moduleName); + + SortedSet getDatabaseProperties(); + + boolean hasDatabaseProperties(); + + /** + * Indicates whether JPA can be installed in the currently focused module. + * + * @return false if no module has the focus + */ + boolean isJpaInstallationPossible(); + + /** + * Checks for the existence the META-INF/persistence.xml + * + * @return true if the META-INF/persistence.xml exists, otherwise false + */ + boolean isPersistentClassAvailable(); + + /** + * Creates a new JPA embeddable class. + * + * @param name the name of the embeddable class (required) + * @param serializable whether the class implements + * {@link java.io.Serializable} + */ + void newEmbeddableClass(JavaType name, boolean serializable); + + /** + * Creates a new entity. + * + * @param name the entity name (required) + * @param createAbstract indicates whether the entity will be an abstract + * class + * @param superclass the super class of the entity + * @param implementsType the interface to implement + * @param annotations the entity's annotations + */ + void newEntity(JavaType name, boolean createAbstract, JavaType superclass, + JavaType implementsType, List annotations); + + /** + * Creates a new JPA identifier class. + * + * @param identifierType the identifier type + * @param identifierField the identifier field name + * @param identifierColumn the identifier column name + */ + void newIdentifier(JavaType identifierType, String identifierField, + String identifierColumn); +} \ No newline at end of file diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaOperationsImpl.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaOperationsImpl.java new file mode 100644 index 000000000..b896941c1 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/JpaOperationsImpl.java @@ -0,0 +1,1946 @@ +package org.springframework.roo.addon.jpa; + +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.JpaJavaType.EMBEDDABLE; +import static org.springframework.roo.model.RooJavaType.ROO_EQUALS; +import static org.springframework.roo.model.RooJavaType.ROO_IDENTIFIER; +import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN; +import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE; +import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; +import static org.springframework.roo.model.SpringJavaType.JPA_TRANSACTION_MANAGER; +import static org.springframework.roo.model.SpringJavaType.LOCAL_CONTAINER_ENTITY_MANAGER_FACTORY_BEAN; +import static org.springframework.roo.model.SpringJavaType.LOCAL_ENTITY_MANAGER_FACTORY_BEAN; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.DependencyScope; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Filter; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.Property; +import org.springframework.roo.project.Repository; +import org.springframework.roo.project.Resource; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.service.component.ComponentContext; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link JpaOperations}. + * + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class JpaOperationsImpl implements JpaOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JpaOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + + static class LinkedProperties extends Properties { + private static final long serialVersionUID = -8828266911075836165L; + private final Set keys = new LinkedHashSet(); + + @Override + public Enumeration keys() { + return Collections. enumeration(keys); + } + + @Override + public Object put(final Object key, final Object value) { + keys.add(key); + return super.put(key, value); + } + } + + static final String APPLICATION_CONTEXT_XML = "applicationContext.xml"; + private static final String DATABASE_DRIVER = "database.driverClassName"; + private static final String DATABASE_PASSWORD = "database.password"; + private static final String DATABASE_PROPERTIES_FILE = "database.properties"; + private static final String DATABASE_URL = "database.url"; + private static final String DATABASE_USERNAME = "database.username"; + private static final String DEFAULT_PERSISTENCE_UNIT = "persistenceUnit"; + private static final String GAE_PERSISTENCE_UNIT_NAME = "transactions-optional"; + static final String JPA_DIALECTS_FILE = "jpa-dialects.properties"; + + private static final Dependency JSTL_IMPL_DEPENDENCY = new Dependency( + "org.glassfish.web", "jstl-impl", "1.2"); + private static final String PERSISTENCE_UNIT = "persistence-unit"; + static final String PERSISTENCE_XML = "META-INF/persistence.xml"; + + static final String POM_XML = "pom.xml"; + + FileManager fileManager; + PathResolver pathResolver; + ProjectOperations projectOperations; + PropFileOperations propFileOperations; + TypeLocationService typeLocationService; + TypeManagementService typeManagementService; + + public void configureJpa(final OrmProvider ormProvider, + final JdbcDatabase jdbcDatabase, final String jndi, + final String applicationId, final String hostName, + final String databaseName, final String userName, + final String password, final String transactionManager, + final String persistenceUnit, final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + Validate.notNull(ormProvider, "ORM provider required"); + Validate.notNull(jdbcDatabase, "JDBC database required"); + + // Parse the configuration.xml file + final Element configuration = XmlUtils.getConfiguration(getClass()); + + // Get the first part of the XPath expressions for unwanted databases + // and ORM providers + final String databaseXPath = getDbXPath(getUnwantedDatabases(jdbcDatabase)); + final String providersXPath = getProviderXPath(getUnwantedOrmProviders(ormProvider)); + + if (jdbcDatabase != JdbcDatabase.GOOGLE_APP_ENGINE) { + updateEclipsePlugin(false); + updateDataNucleusPlugin(false); + projectOperations.updateDependencyScope(moduleName, + JSTL_IMPL_DEPENDENCY, null); + } + + updateApplicationContext(ormProvider, jdbcDatabase, jndi, + transactionManager, persistenceUnit); + updatePersistenceXml(ormProvider, jdbcDatabase, hostName, databaseName, + userName, password, persistenceUnit, moduleName); + manageGaeXml(ormProvider, jdbcDatabase, applicationId, moduleName); + updateDatabaseDotComConfigProperties(ormProvider, jdbcDatabase, + hostName, userName, password, StringUtils.defaultIfEmpty( + persistenceUnit, DEFAULT_PERSISTENCE_UNIT), moduleName); + + if (StringUtils.isBlank(jndi)) { + updateDatabaseProperties(ormProvider, jdbcDatabase, hostName, + databaseName, userName, password, moduleName); + } + else { + updateJndiProperties(); + } + + updateLog4j(ormProvider); + updatePomProperties(configuration, ormProvider, jdbcDatabase, + moduleName); + updateDependencies(configuration, ormProvider, jdbcDatabase, + databaseXPath, providersXPath, moduleName); + updateRepositories(configuration, ormProvider, jdbcDatabase, moduleName); + updatePluginRepositories(configuration, ormProvider, jdbcDatabase, + moduleName); + updateFilters(configuration, ormProvider, jdbcDatabase, databaseXPath, + providersXPath, moduleName); + updateResources(configuration, ormProvider, jdbcDatabase, + databaseXPath, providersXPath, moduleName); + updateBuildPlugins(configuration, ormProvider, jdbcDatabase, + databaseXPath, providersXPath, moduleName); + } + + private Element createPropertyElement(final String name, + final String value, final Document document) { + final Element property = document.createElement("property"); + property.setAttribute("name", name); + property.setAttribute("value", value); + return property; + } + + private Element createRefElement(final String name, final String value, + final Document document) { + final Element property = document.createElement("property"); + property.setAttribute("name", name); + property.setAttribute("ref", value); + return property; + } + + private String getConnectionString(final JdbcDatabase jdbcDatabase, + String hostName, final String databaseName, final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + String connectionString = jdbcDatabase.getConnectionString(); + if (connectionString.contains("TO_BE_CHANGED_BY_ADDON")) { + connectionString = connectionString.replace( + "TO_BE_CHANGED_BY_ADDON", StringUtils + .isNotBlank(databaseName) ? databaseName + : projectOperations.getProjectName(moduleName)); + } + else { + if (StringUtils.isNotBlank(databaseName)) { + // Oracle uses a different connection URL - see ROO-1203 + final String dbDelimiter = jdbcDatabase == JdbcDatabase.ORACLE ? ":" + : "/"; + connectionString += dbDelimiter + databaseName; + } + } + if (StringUtils.isBlank(hostName)) { + hostName = "localhost"; + } + return connectionString.replace("HOST_NAME", hostName); + } + + public SortedSet getDatabaseProperties() { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + if(propFileOperations == null){ + propFileOperations = getPropFileOperations(); + } + Validate.notNull(propFileOperations, "PropFileOperations is required"); + + if (hasDatabaseProperties()) { + return propFileOperations.getPropertyKeys(Path.SPRING_CONFIG_ROOT + .getModulePathId(projectOperations.getFocusedModuleName()), + DATABASE_PROPERTIES_FILE, true); + } + return getPropertiesFromDataNucleusConfiguration(); + } + + private String getDatabasePropertiesPath() { + return getPropertiesPath(DATABASE_PROPERTIES_FILE); + } + + private String getDbXPath(final List databases) { + final StringBuilder builder = new StringBuilder( + "/configuration/databases/database["); + for (int i = 0; i < databases.size(); i++) { + if (i > 0) { + builder.append(" or "); + } + builder.append("@id = '"); + builder.append(databases.get(i).getKey()); + builder.append("'"); + } + builder.append("]"); + return builder.toString(); + } + + private List getDependencies(final String xPathExpression, + final Element configuration, final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final List dependencies = new ArrayList(); + for (final Element dependencyElement : XmlUtils.findElements( + xPathExpression + "/dependencies/dependency", configuration)) { + final Dependency dependency = new Dependency(dependencyElement); + if (dependency.getGroupId().equals("com.google.appengine") + && dependency.getArtifactId().equals( + "appengine-api-1.0-sdk") + && projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GWT)) { + continue; + } + dependencies.add(dependency); + } + return dependencies; + } + + private List getFilters(final String xPathExpression, + final Element configuration) { + final List filters = new ArrayList(); + for (final Element filterElement : XmlUtils.findElements( + xPathExpression + "/filters/filter", configuration)) { + filters.add(new Filter(filterElement)); + } + return filters; + } + + private String getJndiPropertiesPath() { + return getPropertiesPath("jndi.properties"); + } + + public String getName() { + return FeatureNames.JPA; + } + + private String getPersistencePathOfFocussedModule() { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + return pathResolver.getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, + PERSISTENCE_XML); + } + + private List getPlugins(final String xPathExpression, + final Element configuration) { + final List buildPlugins = new ArrayList(); + for (final Element pluginElement : XmlUtils.findElements( + xPathExpression + "/plugins/plugin", configuration)) { + buildPlugins.add(new Plugin(pluginElement)); + } + return buildPlugins; + } + + private String getProjectName(final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + return projectOperations.getProjectName(moduleName); + } + + private SortedSet getPropertiesFromDataNucleusConfiguration() { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final String persistenceXmlPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, PERSISTENCE_XML); + if (!fileManager.exists(persistenceXmlPath)) { + throw new IllegalStateException("Failed to find " + + persistenceXmlPath); + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(persistenceXmlPath)); + final Element root = document.getDocumentElement(); + + final List propertyElements = XmlUtils.findElements( + "/persistence/persistence-unit/properties/property", root); + Validate.notEmpty(propertyElements, + "Failed to find property elements in %s", persistenceXmlPath); + final SortedSet properties = new TreeSet(); + + for (final Element propertyElement : propertyElements) { + final String key = propertyElement.getAttribute("name"); + final String value = propertyElement.getAttribute("value"); + if ("datanucleus.ConnectionDriverName".equals(key)) { + properties.add("datanucleus.ConnectionDriverName = " + value); + } + if ("datanucleus.ConnectionURL".equals(key)) { + properties.add("datanucleus.ConnectionURL = " + value); + } + if ("datanucleus.ConnectionUserName".equals(key)) { + properties.add("datanucleus.ConnectionUserName = " + value); + } + if ("datanucleus.ConnectionPassword".equals(key)) { + properties.add("datanucleus.ConnectionPassword = " + value); + } + + if (properties.size() == 4) { + // All required properties have been found so ignore rest of + // elements + break; + } + } + return properties; + } + + private String getPropertiesPath(final String propertiesFile) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + String path = pathResolver.getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, propertiesFile); + if (StringUtils.isBlank(path)) { + final String tmpDir = System.getProperty("java.io.tmpdir"); + // For unit testing, as path will be null otherwise + path = tmpDir + + (!tmpDir.endsWith(File.separator) ? File.separator : "") + + propertiesFile; + } + return path; + } + + private String getProviderXPath(final List ormProviders) { + final StringBuilder builder = new StringBuilder( + "/configuration/ormProviders/provider["); + for (int i = 0; i < ormProviders.size(); i++) { + if (i > 0) { + builder.append(" or "); + } + builder.append("@id = '"); + builder.append(ormProviders.get(i).name()); + builder.append("'"); + } + builder.append("]"); + return builder.toString(); + } + + private List getResources(final String xPathExpression, + final Element configuration) { + final List resources = new ArrayList(); + for (final Element resourceElement : XmlUtils.findElements( + xPathExpression + "/resources/resource", configuration)) { + resources.add(new Resource(resourceElement)); + } + return resources; + } + + private List getUnwantedDatabases( + final JdbcDatabase jdbcDatabase) { + final List unwantedDatabases = new ArrayList(); + for (final JdbcDatabase database : JdbcDatabase.values()) { + if (!database.getKey().equals(jdbcDatabase.getKey()) + && !database.getDriverClassName().equals( + jdbcDatabase.getDriverClassName())) { + unwantedDatabases.add(database); + } + } + return unwantedDatabases; + } + + private List getUnwantedOrmProviders( + final OrmProvider ormProvider) { + final List unwantedOrmProviders = new LinkedList( + Arrays.asList(OrmProvider.values())); + unwantedOrmProviders.remove(ormProvider); + return unwantedOrmProviders; + } + + public boolean hasDatabaseProperties() { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + return fileManager.exists(getDatabasePropertiesPath()); + } + + public boolean isInstalledInModule(final String moduleName) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final LogicalPath resourcesPath = LogicalPath.getInstance( + Path.SRC_MAIN_RESOURCES, moduleName); + return isJpaInstallationPossible() + && fileManager.exists(projectOperations.getPathResolver() + .getIdentifier(resourcesPath, PERSISTENCE_XML)); + } + + public boolean isJpaInstallationPossible() { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + return projectOperations.isFocusedProjectAvailable(); + } + + public boolean isPersistentClassAvailable() { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + return isInstalledInModule(projectOperations.getFocusedModuleName()); + } + + private void manageGaeBuildCommand(final boolean addGaeSettingsToPlugin, + final Document document, final Collection changes) { + final Element root = document.getDocumentElement(); + final Element additionalBuildcommandsElement = XmlUtils + .findFirstElement( + "/project/build/plugins/plugin[artifactId = 'maven-eclipse-plugin']/configuration/additionalBuildcommands", + root); + Validate.notNull(additionalBuildcommandsElement, + "additionalBuildCommands element of the maven-eclipse-plugin required"); + final String gaeBuildCommandName = "com.google.appengine.eclipse.core.enhancerbuilder"; + Element gaeBuildCommandElement = XmlUtils.findFirstElement( + "buildCommand[name = '" + gaeBuildCommandName + "']", + additionalBuildcommandsElement); + if (addGaeSettingsToPlugin && gaeBuildCommandElement == null) { + final Element nameElement = document.createElement("name"); + nameElement.setTextContent(gaeBuildCommandName); + gaeBuildCommandElement = document.createElement("buildCommand"); + gaeBuildCommandElement.appendChild(nameElement); + additionalBuildcommandsElement.appendChild(gaeBuildCommandElement); + changes.add("added GAE buildCommand to maven-eclipse-plugin"); + } + else if (!addGaeSettingsToPlugin && gaeBuildCommandElement != null) { + additionalBuildcommandsElement.removeChild(gaeBuildCommandElement); + changes.add("removed GAE buildCommand from maven-eclipse-plugin"); + } + } + + private void manageGaeProjectNature(final boolean addGaeSettingsToPlugin, + final Document document, final Collection changes) { + final Element root = document.getDocumentElement(); + final Element additionalProjectnaturesElement = XmlUtils + .findFirstElement( + "/project/build/plugins/plugin[artifactId = 'maven-eclipse-plugin']/configuration/additionalProjectnatures", + root); + Validate.notNull(additionalProjectnaturesElement, + "additionalProjectnatures element of the maven-eclipse-plugin required"); + final String gaeProjectnatureName = "com.google.appengine.eclipse.core.gaeNature"; + Element gaeProjectnatureElement = XmlUtils.findFirstElement( + "projectnature[text() = '" + gaeProjectnatureName + "']", + additionalProjectnaturesElement); + if (addGaeSettingsToPlugin && gaeProjectnatureElement == null) { + gaeProjectnatureElement = new XmlElementBuilder("projectnature", + document).setText(gaeProjectnatureName).build(); + additionalProjectnaturesElement + .appendChild(gaeProjectnatureElement); + changes.add("added GAE projectnature to maven-eclipse-plugin"); + } + else if (!addGaeSettingsToPlugin && gaeProjectnatureElement != null) { + additionalProjectnaturesElement + .removeChild(gaeProjectnatureElement); + changes.add("removed GAE projectnature from maven-eclipse-plugin"); + } + } + + private void manageGaeXml(final OrmProvider ormProvider, + final JdbcDatabase jdbcDatabase, final String applicationId, + final String moduleName) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final String appenginePath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/appengine-web.xml"); + final boolean appenginePathExists = fileManager.exists(appenginePath); + + final String loggingPropertiesPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/logging.properties"); + final boolean loggingPropertiesPathExists = fileManager + .exists(loggingPropertiesPath); + + if (jdbcDatabase != JdbcDatabase.GOOGLE_APP_ENGINE) { + if (appenginePathExists) { + fileManager.delete(appenginePath, + "database is " + jdbcDatabase.name()); + } + if (loggingPropertiesPathExists) { + fileManager.delete(loggingPropertiesPath, "database is " + + jdbcDatabase.name()); + } + return; + } + + final InputStream inputStream; + if (appenginePathExists) { + inputStream = fileManager.getInputStream(appenginePath); + } + else { + inputStream = FileUtils.getInputStream(getClass(), + "appengine-web-template.xml"); + } + final Document appengine = XmlUtils.readXml(inputStream); + + final Element root = appengine.getDocumentElement(); + final Element applicationElement = XmlUtils.findFirstElement( + "/appengine-web-app/application", root); + final String textContent = StringUtils.defaultIfEmpty(applicationId, + getProjectName(moduleName)); + if (!textContent.equals(applicationElement.getTextContent())) { + applicationElement.setTextContent(textContent); + fileManager.createOrUpdateTextFileIfRequired(appenginePath, + XmlUtils.nodeToString(appengine), false); + LOGGER.warning("Please update your database details in src/main/resources/META-INF/persistence.xml."); + } + + if (!loggingPropertiesPathExists) { + InputStream templateInputStream = null; + OutputStream outputStream = null; + try { + templateInputStream = FileUtils.getInputStream(getClass(), + "logging.properties"); + outputStream = fileManager.createFile(loggingPropertiesPath) + .getOutputStream(); + IOUtils.copy(templateInputStream, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(templateInputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + public void newEmbeddableClass(final JavaType name, + final boolean serializable) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(typeManagementService == null){ + typeManagementService = getTypeManagementService(); + } + Validate.notNull(typeManagementService, "TypeManagementService is required"); + + Validate.notNull(name, "Embeddable name required"); + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + + final List annotations = new ArrayList( + Arrays.asList(new AnnotationMetadataBuilder(ROO_JAVA_BEAN), + new AnnotationMetadataBuilder(ROO_TO_STRING), + new AnnotationMetadataBuilder(EMBEDDABLE))); + + if (serializable) { + annotations.add(new AnnotationMetadataBuilder(ROO_SERIALIZABLE)); + } + + final int modifier = Modifier.PUBLIC; + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, modifier, name, + PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(annotations); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void newEntity(final JavaType name, final boolean createAbstract, + final JavaType superclass, final JavaType implementsType, + final List annotations) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + if(typeManagementService == null){ + typeManagementService = getTypeManagementService(); + } + Validate.notNull(typeManagementService, "TypeManagementService is required"); + + Validate.notNull(name, "Entity name required"); + Validate.isTrue( + !JdkJavaType.isPartOfJavaLang(name.getSimpleTypeName()), + "Entity name '%s' must not be part of java.lang", + name.getSimpleTypeName()); + + int modifier = Modifier.PUBLIC; + if (createAbstract) { + modifier |= Modifier.ABSTRACT; + } + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, modifier, name, + PhysicalTypeCategory.CLASS); + + if (!superclass.equals(OBJECT)) { + final ClassOrInterfaceTypeDetails superclassClassOrInterfaceTypeDetails = typeLocationService + .getTypeDetails(superclass); + if (superclassClassOrInterfaceTypeDetails != null) { + cidBuilder + .setSuperclass(new ClassOrInterfaceTypeDetailsBuilder( + superclassClassOrInterfaceTypeDetails)); + } + } + + cidBuilder.setExtendsTypes(Arrays.asList(superclass)); + + if (implementsType != null) { + final Set implementsTypes = new LinkedHashSet(); + final ClassOrInterfaceTypeDetails typeDetails = typeLocationService + .getTypeDetails(declaredByMetadataId); + if (typeDetails != null) { + implementsTypes.addAll(typeDetails.getImplementsTypes()); + } + implementsTypes.add(implementsType); + cidBuilder.setImplementsTypes(implementsTypes); + } + + cidBuilder.setAnnotations(annotations); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void newIdentifier(final JavaType identifierType, + final String identifierField, final String identifierColumn) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(typeManagementService == null){ + typeManagementService = getTypeManagementService(); + } + Validate.notNull(typeManagementService, "TypeManagementService is required"); + + Validate.notNull(identifierType, "Identifier type required"); + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(identifierType, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + final List identifierAnnotations = Arrays + .asList(new AnnotationMetadataBuilder(ROO_TO_STRING), + new AnnotationMetadataBuilder(ROO_EQUALS), + new AnnotationMetadataBuilder(ROO_IDENTIFIER)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC | Modifier.FINAL, + identifierType, PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(identifierAnnotations); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + private Properties readProperties(final String path, final boolean exists, + final String templateFilename) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + + final Properties props = new LinkedProperties(); + InputStream inputStream = null; + try { + if (exists) { + inputStream = fileManager.getInputStream(path); + } + else { + inputStream = FileUtils.getInputStream(getClass(), + templateFilename); + } + props.load(inputStream); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + return props; + } + + private void updateApplicationContext(final OrmProvider ormProvider, + final JdbcDatabase jdbcDatabase, final String jndi, + String transactionManager, final String persistenceUnit) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final String contextPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + APPLICATION_CONTEXT_XML); + final Document appCtx = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = appCtx.getDocumentElement(); + + // Checking for existence of configurations, if found abort + Element dataSource = XmlUtils.findFirstElement( + "/beans/bean[@id = 'dataSource']", root); + Element dataSourceJndi = XmlUtils.findFirstElement( + "/beans/jndi-lookup[@id = 'dataSource']", root); + + if (ormProvider == OrmProvider.DATANUCLEUS) { + if (dataSource != null) { + root.removeChild(dataSource); + } + if (dataSourceJndi != null) { + root.removeChild(dataSourceJndi); + } + } + else if (StringUtils.isBlank(jndi) && dataSource == null) { + dataSource = appCtx.createElement("bean"); + dataSource.setAttribute("class", + "org.apache.commons.dbcp.BasicDataSource"); + dataSource.setAttribute("destroy-method", "close"); + dataSource.setAttribute("id", "dataSource"); + dataSource.appendChild(createPropertyElement("driverClassName", + "${database.driverClassName}", appCtx)); + dataSource.appendChild(createPropertyElement("url", + "${database.url}", appCtx)); + dataSource.appendChild(createPropertyElement("username", + "${database.username}", appCtx)); + dataSource.appendChild(createPropertyElement("password", + "${database.password}", appCtx)); + dataSource.appendChild(createPropertyElement("testOnBorrow", + "true", appCtx)); + dataSource.appendChild(createPropertyElement("testOnReturn", + "true", appCtx)); + dataSource.appendChild(createPropertyElement("testWhileIdle", + "true", appCtx)); + dataSource.appendChild(createPropertyElement( + "timeBetweenEvictionRunsMillis", "1800000", appCtx)); + dataSource.appendChild(createPropertyElement( + "numTestsPerEvictionRun", "3", appCtx)); + dataSource.appendChild(createPropertyElement( + "minEvictableIdleTimeMillis", "1800000", appCtx)); + root.appendChild(dataSource); + if (dataSourceJndi != null) { + dataSourceJndi.getParentNode().removeChild(dataSourceJndi); + } + } + else if (StringUtils.isNotBlank(jndi)) { + if (dataSourceJndi == null) { + dataSourceJndi = appCtx.createElement("jee:jndi-lookup"); + dataSourceJndi.setAttribute("id", "dataSource"); + root.appendChild(dataSourceJndi); + } + dataSourceJndi.setAttribute("jndi-name", jndi); + if (dataSource != null) { + dataSource.getParentNode().removeChild(dataSource); + } + } + + if (dataSource != null) { + final Element validationQueryElement = XmlUtils.findFirstElement( + "property[@name = 'validationQuery']", dataSource); + if (validationQueryElement != null) { + dataSource.removeChild(validationQueryElement); + } + String validationQuery = ""; + switch (jdbcDatabase) { + case ORACLE: + validationQuery = "SELECT 1 FROM DUAL"; + break; + case POSTGRES: + validationQuery = "SELECT version();"; + break; + case MYSQL: + validationQuery = "SELECT 1"; + break; + } + if (StringUtils.isNotBlank(validationQuery)) { + dataSource.appendChild(createPropertyElement("validationQuery", + validationQuery, appCtx)); + } + } + + transactionManager = StringUtils.defaultIfEmpty(transactionManager, + "transactionManager"); + Element transactionManagerElement = XmlUtils.findFirstElement( + "/beans/bean[@id = '" + transactionManager + "']", root); + if (transactionManagerElement == null) { + transactionManagerElement = appCtx.createElement("bean"); + transactionManagerElement.setAttribute("id", transactionManager); + transactionManagerElement.setAttribute("class", + JPA_TRANSACTION_MANAGER.getFullyQualifiedTypeName()); + transactionManagerElement.appendChild(createRefElement( + "entityManagerFactory", "entityManagerFactory", appCtx)); + root.appendChild(transactionManagerElement); + } + + Element aspectJTxManager = XmlUtils.findFirstElement( + "/beans/annotation-driven", root); + if (aspectJTxManager == null) { + aspectJTxManager = appCtx.createElement("tx:annotation-driven"); + aspectJTxManager.setAttribute("mode", "aspectj"); + aspectJTxManager.setAttribute("transaction-manager", + transactionManager); + root.appendChild(aspectJTxManager); + } + else { + aspectJTxManager.setAttribute("transaction-manager", + transactionManager); + } + + Element entityManagerFactory = XmlUtils.findFirstElement( + "/beans/bean[@id = 'entityManagerFactory']", root); + if (entityManagerFactory != null) { + root.removeChild(entityManagerFactory); + } + + entityManagerFactory = appCtx.createElement("bean"); + entityManagerFactory.setAttribute("id", "entityManagerFactory"); + + switch (jdbcDatabase) { + case GOOGLE_APP_ENGINE: + entityManagerFactory.setAttribute("class", + LOCAL_ENTITY_MANAGER_FACTORY_BEAN + .getFullyQualifiedTypeName()); + entityManagerFactory + .appendChild(createPropertyElement("persistenceUnitName", + StringUtils.defaultIfEmpty(persistenceUnit, + GAE_PERSISTENCE_UNIT_NAME), appCtx)); + break; + default: + entityManagerFactory.setAttribute("class", + LOCAL_CONTAINER_ENTITY_MANAGER_FACTORY_BEAN + .getFullyQualifiedTypeName()); + entityManagerFactory + .appendChild(createPropertyElement("persistenceUnitName", + StringUtils.defaultIfEmpty(persistenceUnit, + DEFAULT_PERSISTENCE_UNIT), appCtx)); + if (ormProvider != OrmProvider.DATANUCLEUS) { + entityManagerFactory.appendChild(createRefElement("dataSource", + "dataSource", appCtx)); + } + break; + } + + root.appendChild(entityManagerFactory); + + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(contextPath, + XmlUtils.nodeToString(appCtx), false); + } + + private void updateBuildPlugins(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String databaseXPath, final String providersXPath, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + // Identify the required plugins + final List requiredPlugins = new ArrayList(); + + final List databasePlugins = XmlUtils.findElements( + jdbcDatabase.getConfigPrefix() + "/plugins/plugin", + configuration); + for (final Element pluginElement : databasePlugins) { + requiredPlugins.add(new Plugin(pluginElement)); + } + + final List ormPlugins = XmlUtils.findElements( + ormProvider.getConfigPrefix() + "/plugins/plugin", + configuration); + for (final Element pluginElement : ormPlugins) { + requiredPlugins.add(new Plugin(pluginElement)); + } + + // Identify any redundant plugins + final List redundantPlugins = new ArrayList(); + redundantPlugins.addAll(getPlugins(databaseXPath, configuration)); + redundantPlugins.addAll(getPlugins(providersXPath, configuration)); + // Don't remove any that are still required + redundantPlugins.removeAll(requiredPlugins); + + // Update the POM + projectOperations.addBuildPlugins(moduleName, requiredPlugins); + projectOperations.removeBuildPlugins(moduleName, redundantPlugins); + + if (jdbcDatabase == JdbcDatabase.GOOGLE_APP_ENGINE) { + updateEclipsePlugin(true); + updateDataNucleusPlugin(true); + projectOperations.updateDependencyScope(moduleName, + JSTL_IMPL_DEPENDENCY, DependencyScope.PROVIDED); + } + } + + private void updateDatabaseDotComConfigProperties( + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String hostName, final String userName, + final String password, final String persistenceUnit, + final String moduleName) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final String configPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, persistenceUnit + ".properties"); + final boolean configExists = fileManager.exists(configPath); + + if (jdbcDatabase != JdbcDatabase.DATABASE_DOT_COM) { + if (configExists) { + fileManager.delete(configPath, + "database is " + jdbcDatabase.name()); + } + return; + } + + final String connectionString = getConnectionString(jdbcDatabase, + hostName, null /* databaseName */, moduleName).replace( + "USER_NAME", + StringUtils.defaultIfEmpty(userName, "${userName}")) + .replace("PASSWORD", + StringUtils.defaultIfEmpty(password, "${password}")); + final Properties props = readProperties(configPath, configExists, + "database-dot-com-template.properties"); + + final boolean hasChanged = !props.get("url").equals( + StringUtils.stripToEmpty(connectionString)); + if (!hasChanged) { + return; + } + + props.put("url", StringUtils.stripToEmpty(connectionString)); + + writeProperties(configPath, configExists, props); + + LOGGER.warning("Please update your database details in src/main/resources/" + + persistenceUnit + ".properties."); + } + + private void updateDatabaseProperties(final OrmProvider ormProvider, + final JdbcDatabase jdbcDatabase, final String hostName, + final String databaseName, String userName, final String password, + final String moduleName) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + final String databasePath = getDatabasePropertiesPath(); + final boolean databaseExists = fileManager.exists(databasePath); + + if (ormProvider == OrmProvider.DATANUCLEUS) { + if (databaseExists) { + fileManager.delete(databasePath, "ORM provider is " + + ormProvider.name()); + } + return; + } + + final String jndiPath = getJndiPropertiesPath(); + if (fileManager.exists(jndiPath)) { + fileManager.delete(jndiPath, "JNDI is not used"); + } + + final Properties props = readProperties(databasePath, databaseExists, + "database-template.properties"); + + final String connectionString = getConnectionString(jdbcDatabase, + hostName, databaseName, moduleName); + if (jdbcDatabase.getKey().equals("HYPERSONIC") + || jdbcDatabase == JdbcDatabase.H2_IN_MEMORY + || jdbcDatabase == JdbcDatabase.SYBASE) { + userName = StringUtils.defaultIfEmpty(userName, "sa"); + } + + final String driver = props.getProperty(DATABASE_DRIVER); + final String url = props.getProperty(DATABASE_URL); + final String uname = props.getProperty(DATABASE_USERNAME); + final String pwd = props.getProperty(DATABASE_PASSWORD); + + boolean hasChanged = driver == null + || !driver.equals(jdbcDatabase.getDriverClassName()); + hasChanged |= url == null || !url.equals(connectionString); + hasChanged |= uname == null + || !uname.equals(StringUtils.stripToEmpty(userName)); + hasChanged |= pwd == null + || !pwd.equals(StringUtils.stripToEmpty(password)); + if (!hasChanged) { + // No changes from existing database configuration so exit now + return; + } + + // Write changes to database.properties file + props.put(DATABASE_URL, connectionString); + props.put(DATABASE_DRIVER, jdbcDatabase.getDriverClassName()); + props.put(DATABASE_USERNAME, StringUtils.stripToEmpty(userName)); + props.put(DATABASE_PASSWORD, StringUtils.stripToEmpty(password)); + + writeProperties(databasePath, databaseExists, props); + + // Log message to console + switch (jdbcDatabase) { + case ORACLE: + case DB2_EXPRESS_C: + case DB2_400: + LOGGER.warning("The " + + jdbcDatabase.name() + + " JDBC driver is not available in public Maven repositories. Please adjust the pom.xml dependency to suit your needs"); + break; + case POSTGRES: + case DERBY_EMBEDDED: + case DERBY_CLIENT: + case MSSQL: + case SYBASE: + case MYSQL: + LOGGER.warning("Please update your database details in src/main/resources/META-INF/spring/database.properties."); + break; + } + } + + private void updateDataNucleusPlugin(final boolean addToPlugin) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final String pom = pathResolver + .getFocusedIdentifier(Path.ROOT, POM_XML); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom)); + final Element root = document.getDocumentElement(); + + // Manage mappingExcludes + final Element configurationElement = XmlUtils + .findFirstElement( + "/project/build/plugins/plugin[artifactId = 'maven-datanucleus-plugin']/configuration", + root); + if (configurationElement == null) { + return; + } + + String descriptionOfChange = ""; + Element mappingExcludesElement = XmlUtils.findFirstElement( + "mappingExcludes", configurationElement); + if (addToPlugin && mappingExcludesElement == null) { + mappingExcludesElement = new XmlElementBuilder("mappingExcludes", + document) + .setText( + "**/CustomRequestFactoryServlet.class, **/GaeAuthFilter.class") + .build(); + configurationElement.appendChild(mappingExcludesElement); + descriptionOfChange = "added GAEAuthFilter mappingExcludes to maven-datanuclueus-plugin"; + } + else if (!addToPlugin && mappingExcludesElement != null) { + configurationElement.removeChild(mappingExcludesElement); + descriptionOfChange = "removed GAEAuthFilter mappingExcludes from maven-datanuclueus-plugin"; + } + + fileManager.createOrUpdateTextFileIfRequired(pom, + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + /** + * Updates the POM with the dependencies required for the given database and + * ORM provider, removing any other persistence-related dependencies + * + * @param configuration + * @param ormProvider + * @param jdbcDatabase + * @param databaseXPath + * @param providersXPath + */ + private void updateDependencies(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String databaseXPath, final String providersXPath, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final List requiredDependencies = new ArrayList(); + + final List databaseDependencies = XmlUtils.findElements( + jdbcDatabase.getConfigPrefix() + "/dependencies/dependency", + configuration); + for (final Element dependencyElement : databaseDependencies) { + requiredDependencies.add(new Dependency(dependencyElement)); + } + + final List ormDependencies = XmlUtils.findElements( + ormProvider.getConfigPrefix() + "/dependencies/dependency", + configuration); + for (final Element dependencyElement : ormDependencies) { + requiredDependencies.add(new Dependency(dependencyElement)); + } + + // Hard coded to JPA & Hibernate Validator for now + final List jpaDependencies = XmlUtils + .findElements( + "/configuration/persistence/provider[@id = 'JPA']/dependencies/dependency", + configuration); + for (final Element dependencyElement : jpaDependencies) { + requiredDependencies.add(new Dependency(dependencyElement)); + } + + final List springDependencies = XmlUtils.findElements( + "/configuration/spring/dependencies/dependency", configuration); + for (final Element dependencyElement : springDependencies) { + requiredDependencies.add(new Dependency(dependencyElement)); + } + + // Remove redundant dependencies + final List redundantDependencies = new ArrayList(); + redundantDependencies.addAll(getDependencies(databaseXPath, + configuration, moduleName)); + redundantDependencies.addAll(getDependencies(providersXPath, + configuration, moduleName)); + // Don't remove any we actually need + redundantDependencies.removeAll(requiredDependencies); + + // Update the POM + projectOperations.addDependencies(moduleName, requiredDependencies); + projectOperations.removeDependencies(moduleName, redundantDependencies); + } + + private void updateEclipsePlugin(final boolean addGaeSettingsToPlugin) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final String pom = pathResolver + .getFocusedIdentifier(Path.ROOT, POM_XML); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom)); + final Collection changes = new ArrayList(); + + manageGaeBuildCommand(addGaeSettingsToPlugin, document, changes); + manageGaeProjectNature(addGaeSettingsToPlugin, document, changes); + + if (!changes.isEmpty()) { + final String changesMessage = StringUtils.join(changes, "; "); + fileManager.createOrUpdateTextFileIfRequired(pom, + XmlUtils.nodeToString(document), changesMessage, false); + } + } + + private void updateFilters(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String databaseXPath, final String providersXPath, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + // Remove redundant filters + final List redundantFilters = new ArrayList(); + redundantFilters.addAll(getFilters(databaseXPath, configuration)); + redundantFilters.addAll(getFilters(providersXPath, configuration)); + for (final Filter filter : redundantFilters) { + projectOperations.removeFilter(moduleName, filter); + } + + // Add required filters + final List filters = new ArrayList(); + + final List databaseFilters = XmlUtils.findElements( + jdbcDatabase.getConfigPrefix() + "/filters/filter", + configuration); + for (final Element filterElement : databaseFilters) { + filters.add(new Filter(filterElement)); + } + + final List ormFilters = XmlUtils.findElements( + ormProvider.getConfigPrefix() + "/filters/filter", + configuration); + for (final Element filterElement : ormFilters) { + filters.add(new Filter(filterElement)); + } + + for (final Filter filter : filters) { + projectOperations.addFilter(moduleName, filter); + } + } + + private void updateJndiProperties() { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + final String databasePath = getDatabasePropertiesPath(); + if (fileManager.exists(databasePath)) { + fileManager.delete(databasePath, "JNDI is used"); + } + + final String jndiPath = getJndiPropertiesPath(); + if (fileManager.exists(jndiPath)) { + return; + } + + final Properties props = readProperties(jndiPath, false, + "jndi-template.properties"); + writeProperties(jndiPath, false, props); + LOGGER.warning("Please update your JNDI details in src/main/resources/META-INF/spring/jndi.properties."); + } + + private void updateLog4j(final OrmProvider ormProvider) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final String log4jPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, "log4j.properties"); + if (!fileManager.exists(log4jPath)) { + return; + } + + final MutableFile log4jMutableFile = fileManager.updateFile(log4jPath); + final Properties props = new Properties(); + OutputStream outputStream = null; + try { + props.load(log4jMutableFile.getInputStream()); + final String dnKey = "log4j.category.DataNucleus"; + if (ormProvider == OrmProvider.DATANUCLEUS + && !props.containsKey(dnKey)) { + outputStream = log4jMutableFile.getOutputStream(); + props.put(dnKey, "WARN"); + props.store(outputStream, "Updated at " + new Date()); + } + else if (ormProvider != OrmProvider.DATANUCLEUS + && props.containsKey(dnKey)) { + outputStream = log4jMutableFile.getOutputStream(); + props.remove(dnKey); + props.store(outputStream, "Updated at " + new Date()); + } + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + + private void updatePersistenceXml(final OrmProvider ormProvider, + final JdbcDatabase jdbcDatabase, final String hostName, + final String databaseName, String userName, final String password, + final String persistenceUnit, final String moduleName) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + if(propFileOperations == null){ + propFileOperations = getPropFileOperations(); + } + Validate.notNull(propFileOperations, "PropFileOperations is required"); + + final String persistencePath = getPersistencePathOfFocussedModule(); + final InputStream inputStream; + if (fileManager.exists(persistencePath)) { + // There's an existing persistence config file; read it + inputStream = fileManager.getInputStream(persistencePath); + } + else { + // Use the addon's template file + inputStream = FileUtils.getInputStream(getClass(), + "persistence-template.xml"); + } + + final Document persistence = XmlUtils.readXml(inputStream); + final Element root = persistence.getDocumentElement(); + final Element persistenceElement = XmlUtils.findFirstElement( + "/persistence", root); + Validate.notNull(persistenceElement, "No persistence element found"); + + Element persistenceUnitElement; + if (StringUtils.isNotBlank(persistenceUnit)) { + persistenceUnitElement = XmlUtils + .findFirstElement(PERSISTENCE_UNIT + "[@name = '" + + persistenceUnit + "']", persistenceElement); + } + else { + persistenceUnitElement = XmlUtils + .findFirstElement( + PERSISTENCE_UNIT + + "[@name = '" + + (jdbcDatabase == JdbcDatabase.GOOGLE_APP_ENGINE ? GAE_PERSISTENCE_UNIT_NAME + : DEFAULT_PERSISTENCE_UNIT) + "']", + persistenceElement); + } + + if (persistenceUnitElement != null) { + while (persistenceUnitElement.getFirstChild() != null) { + persistenceUnitElement.removeChild(persistenceUnitElement + .getFirstChild()); + } + } + else { + persistenceUnitElement = persistence + .createElement(PERSISTENCE_UNIT); + persistenceElement.appendChild(persistenceUnitElement); + } + + // Add provider element + final Element provider = persistence.createElement("provider"); + switch (jdbcDatabase) { + case GOOGLE_APP_ENGINE: + persistenceUnitElement + .setAttribute("name", StringUtils.defaultIfEmpty( + persistenceUnit, GAE_PERSISTENCE_UNIT_NAME)); + persistenceUnitElement.removeAttribute("transaction-type"); + provider.setTextContent(ormProvider.getAdapter()); + break; + case DATABASE_DOT_COM: + persistenceUnitElement.setAttribute("name", StringUtils + .defaultIfEmpty(persistenceUnit, DEFAULT_PERSISTENCE_UNIT)); + persistenceUnitElement.removeAttribute("transaction-type"); + provider.setTextContent("com.force.sdk.jpa.PersistenceProviderImpl"); + break; + default: + persistenceUnitElement.setAttribute("name", StringUtils + .defaultIfEmpty(persistenceUnit, DEFAULT_PERSISTENCE_UNIT)); + persistenceUnitElement.setAttribute("transaction-type", + "RESOURCE_LOCAL"); + provider.setTextContent(ormProvider.getAdapter()); + break; + } + persistenceUnitElement.appendChild(provider); + + // Add properties + final Properties dialects = propFileOperations.loadProperties( + JPA_DIALECTS_FILE, getClass()); + final Element properties = persistence.createElement("properties"); + final boolean isDbreProject = fileManager.exists(pathResolver + .getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, "dbre.xml")); + final boolean isDbreProjectOrDB2400 = isDbreProject + || jdbcDatabase == JdbcDatabase.DB2_400; + + switch (ormProvider) { + case HIBERNATE: + final String dialectKey = ormProvider.name() + "." + + jdbcDatabase.name(); + properties.appendChild(createPropertyElement("hibernate.dialect", + dialects.getProperty(dialectKey), persistence)); + properties + .appendChild(persistence + .createComment(" value=\"create\" to build a new database on each run; value=\"update\" to modify an existing database; value=\"create-drop\" means the same as \"create\" but also drops tables when Hibernate closes; value=\"validate\" makes no changes to the database ")); // ROO-627 + properties + .appendChild(createPropertyElement( + "hibernate.hbm2ddl.auto", + isDbreProjectOrDB2400 ? "validate" : "create", + persistence)); + properties.appendChild(createPropertyElement( + "hibernate.ejb.naming_strategy", + "org.hibernate.cfg.ImprovedNamingStrategy", persistence)); + properties.appendChild(createPropertyElement( + "hibernate.connection.charSet", "UTF-8", persistence)); + properties + .appendChild(persistence + .createComment(" Uncomment the following two properties for JBoss only ")); + properties + .appendChild(persistence + .createComment(" property name=\"hibernate.validator.apply_to_ddl\" value=\"false\" /")); + properties + .appendChild(persistence + .createComment(" property name=\"hibernate.validator.autoregister_listeners\" value=\"false\" /")); + break; + case OPENJPA: + properties.appendChild(createPropertyElement( + "openjpa.jdbc.DBDictionary", + dialects.getProperty(ormProvider.name() + "." + + jdbcDatabase.name()), persistence)); + properties + .appendChild(persistence + .createComment(" value=\"buildSchema\" to runtime forward map the DDL SQL; value=\"validate\" makes no changes to the database ")); // ROO-627 + properties.appendChild(createPropertyElement( + "openjpa.jdbc.SynchronizeMappings", + isDbreProjectOrDB2400 ? "validate" : "buildSchema", + persistence)); + properties.appendChild(createPropertyElement( + "openjpa.RuntimeUnenhancedClasses", "supported", + persistence)); + break; + case ECLIPSELINK: + properties.appendChild(createPropertyElement( + "eclipselink.target-database", + dialects.getProperty(ormProvider.name() + "." + + jdbcDatabase.name()), persistence)); + properties + .appendChild(persistence + .createComment(" value=\"drop-and-create-tables\" to build a new database on each run; value=\"create-tables\" creates new tables if needed; value=\"none\" makes no changes to the database ")); // ROO-627 + properties.appendChild(createPropertyElement( + "eclipselink.ddl-generation", + isDbreProjectOrDB2400 ? "none" : "drop-and-create-tables", + persistence)); + properties.appendChild(createPropertyElement( + "eclipselink.ddl-generation.output-mode", "database", + persistence)); + properties.appendChild(createPropertyElement("eclipselink.weaving", + "static", persistence)); + break; + case DATANUCLEUS: + String connectionString = getConnectionString(jdbcDatabase, + hostName, databaseName, moduleName); + switch (jdbcDatabase) { + case GOOGLE_APP_ENGINE: + properties + .appendChild(createPropertyElement( + "datanucleus.NontransactionalRead", "true", + persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.NontransactionalWrite", "true", + persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.autoCreateSchema", "false", persistence)); + break; + case DATABASE_DOT_COM: + properties.appendChild(createPropertyElement( + "datanucleus.storeManagerType", "force", persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.Optimistic", "false", persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.datastoreTransactionDelayOperations", + "true", persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.autoCreateSchema", + Boolean.toString(!isDbreProject), persistence)); + break; + default: + properties.appendChild(createPropertyElement( + "datanucleus.ConnectionDriverName", + jdbcDatabase.getDriverClassName(), persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.autoCreateSchema", + Boolean.toString(!isDbreProject), persistence)); + connectionString = connectionString.replace( + "TO_BE_CHANGED_BY_ADDON", + projectOperations.getProjectName(moduleName)); + if (jdbcDatabase.getKey().equals("HYPERSONIC") + || jdbcDatabase == JdbcDatabase.H2_IN_MEMORY + || jdbcDatabase == JdbcDatabase.SYBASE) { + userName = StringUtils.defaultIfEmpty(userName, "sa"); + } + properties.appendChild(createPropertyElement( + "datanucleus.storeManagerType", "rdbms", persistence)); + } + + if (jdbcDatabase != JdbcDatabase.DATABASE_DOT_COM) { + // These are specified in the connection properties file + properties.appendChild(createPropertyElement( + "datanucleus.ConnectionURL", connectionString, + persistence)); + properties + .appendChild(createPropertyElement( + "datanucleus.ConnectionUserName", userName, + persistence)); + properties + .appendChild(createPropertyElement( + "datanucleus.ConnectionPassword", password, + persistence)); + } + + properties.appendChild(createPropertyElement( + "datanucleus.autoCreateTables", + Boolean.toString(!isDbreProject), persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.autoCreateColumns", "false", persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.autoCreateConstraints", "false", persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.validateTables", "false", persistence)); + properties.appendChild(createPropertyElement( + "datanucleus.validateConstraints", "false", persistence)); + properties + .appendChild(createPropertyElement( + "datanucleus.jpa.addClassTransformer", "false", + persistence)); + break; + } + + persistenceUnitElement.appendChild(properties); + + fileManager.createOrUpdateTextFileIfRequired(persistencePath, + XmlUtils.nodeToString(persistence), false); + + if (jdbcDatabase != JdbcDatabase.GOOGLE_APP_ENGINE + && ormProvider == OrmProvider.DATANUCLEUS) { + LOGGER.warning("Please update your database details in src/main/resources/META-INF/persistence.xml."); + } + } + + private void updatePluginRepositories(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final List pluginRepositories = new ArrayList(); + + final List databasePluginRepositories = XmlUtils + .findElements(jdbcDatabase.getConfigPrefix() + + "/pluginRepositories/pluginRepository", configuration); + for (final Element pluginRepositoryElement : databasePluginRepositories) { + pluginRepositories.add(new Repository(pluginRepositoryElement)); + } + + final List ormPluginRepositories = XmlUtils + .findElements(ormProvider.getConfigPrefix() + + "/pluginRepositories/pluginRepository", configuration); + for (final Element pluginRepositoryElement : ormPluginRepositories) { + pluginRepositories.add(new Repository(pluginRepositoryElement)); + } + + // Add all new plugin repositories to pom.xml + projectOperations.addPluginRepositories(moduleName, pluginRepositories); + } + + private void updatePomProperties(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final List databaseProperties = XmlUtils + .findElements(jdbcDatabase.getConfigPrefix() + "/properties/*", + configuration); + for (final Element property : databaseProperties) { + projectOperations.addProperty(moduleName, new Property(property)); + } + + final List providerProperties = XmlUtils.findElements( + ormProvider.getConfigPrefix() + "/properties/*", configuration); + for (final Element property : providerProperties) { + projectOperations.addProperty(moduleName, new Property(property)); + } + } + + private void updateRepositories(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final List repositories = new ArrayList(); + + final List databaseRepositories = XmlUtils.findElements( + jdbcDatabase.getConfigPrefix() + "/repositories/repository", + configuration); + for (final Element repositoryElement : databaseRepositories) { + repositories.add(new Repository(repositoryElement)); + } + + final List ormRepositories = XmlUtils.findElements( + ormProvider.getConfigPrefix() + "/repositories/repository", + configuration); + for (final Element repositoryElement : ormRepositories) { + repositories.add(new Repository(repositoryElement)); + } + + final List jpaRepositories = XmlUtils + .findElements( + "/configuration/persistence/provider[@id='JPA']/repositories/repository", + configuration); + for (final Element repositoryElement : jpaRepositories) { + repositories.add(new Repository(repositoryElement)); + } + + // Add all new repositories to pom.xml + projectOperations.addRepositories(moduleName, repositories); + } + + private void updateResources(final Element configuration, + final OrmProvider ormProvider, final JdbcDatabase jdbcDatabase, + final String databaseXPath, final String providersXPath, + final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + // Remove redundant resources + final List redundantResources = new ArrayList(); + redundantResources.addAll(getResources(databaseXPath, configuration)); + redundantResources.addAll(getResources(providersXPath, configuration)); + for (final Resource resource : redundantResources) { + projectOperations.removeResource(moduleName, resource); + } + + // Add required resources + final List resources = new ArrayList(); + + final List databaseResources = XmlUtils.findElements( + jdbcDatabase.getConfigPrefix() + "/resources/resource", + configuration); + for (final Element resourceElement : databaseResources) { + resources.add(new Resource(resourceElement)); + } + + final List ormResources = XmlUtils.findElements( + ormProvider.getConfigPrefix() + "/resources/resource", + configuration); + for (final Element resourceElement : ormResources) { + resources.add(new Resource(resourceElement)); + } + + for (final Resource resource : resources) { + projectOperations.addResource(moduleName, resource); + } + } + + private void writeProperties(final String path, final boolean exists, + final Properties props) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + Validate.notNull(fileManager, "FileManager is required"); + + OutputStream outputStream = null; + try { + final MutableFile mutableFile = exists ? fileManager + .updateFile(path) : fileManager.createFile(path); + outputStream = mutableFile == null ? new FileOutputStream(path) + : mutableFile.getOutputStream(); + props.store(outputStream, "Updated at " + new Date()); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + + + public FileManager getFileManager(){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on JpaOperationsImpl."); + return null; + } + } + + public PathResolver getPathResolver(){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on JpaOperationsImpl."); + return null; + } + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on JpaOperationsImpl."); + return null; + } + } + + public PropFileOperations getPropFileOperations(){ + // Get all Services implement PropFileOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PropFileOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (PropFileOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PropFileOperations on JpaOperationsImpl."); + return null; + } + } + + public TypeLocationService getTypeLocationService(){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on JpaOperationsImpl."); + return null; + } + } + + public TypeManagementService getTypeManagementService(){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on JpaOperationsImpl."); + return null; + } + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/OrmProvider.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/OrmProvider.java new file mode 100644 index 000000000..d7fc56c9d --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/OrmProvider.java @@ -0,0 +1,47 @@ +package org.springframework.roo.addon.jpa; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * ORM providers known to the JPA add-on. + * + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +public enum OrmProvider { + + DATANUCLEUS("org.datanucleus.api.jpa.PersistenceProviderImpl"), ECLIPSELINK( + "org.eclipse.persistence.jpa.PersistenceProvider"), HIBERNATE( + "org.hibernate.jpa.HibernatePersistenceProvider"), OPENJPA( + "org.apache.openjpa.persistence.PersistenceProviderImpl"); + + private final String adapter; + + /** + * Constructor + * + * @param adapter (required) + */ + private OrmProvider(final String adapter) { + Validate.notBlank(adapter, "Adapter is required"); + this.adapter = adapter; + } + + public String getAdapter() { + return adapter; + } + + public String getConfigPrefix() { + return "/configuration/ormProviders/provider[@id='" + name() + "']"; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("provider", name()); + builder.append("adapter", adapter); + return builder.toString(); + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerMethod.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerMethod.java new file mode 100644 index 000000000..8f1530e98 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerMethod.java @@ -0,0 +1,362 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaType; + +/** + * Methods implemented by a user project entity. + * + * @author Andrew Swan + * @author Stefan Schmidt + * @since 1.2.0 + */ +enum EntityLayerMethod { + + // The names of these enum constants are arbitrary + + CLEAR(CustomDataKeys.CLEAR_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getClearMethod())) { + return annotationValues.getClearMethod(); + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + }, + + COUNT_ALL(CustomDataKeys.COUNT_ALL_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getCountMethod())) { + return annotationValues.getCountMethod() + plural; + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + }, + + FIND(CustomDataKeys.FIND_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindMethod())) { + return annotationValues.getFindMethod() + + targetEntity.getSimpleTypeName(); + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(idType); + } + }, + + FIND_ALL(CustomDataKeys.FIND_ALL_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindAllMethod())) { + return annotationValues.getFindAllMethod() + plural; + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + }, + + FIND_ENTRIES(CustomDataKeys.FIND_ENTRIES_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindEntriesMethod())) { + return annotationValues.getFindEntriesMethod() + + targetEntity.getSimpleTypeName() + "Entries"; + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays + .asList(JavaType.INT_PRIMITIVE, JavaType.INT_PRIMITIVE); + } + }, + + FIND_SORTED_ALL(CustomDataKeys.FIND_ALL_SORTED_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindAllMethod())) { + return annotationValues.getFindAllMethod() + plural; + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays + .asList(JavaType.STRING, JavaType.STRING); + } + }, + + FIND_SORTED_ENTRIES(CustomDataKeys.FIND_ENTRIES_SORTED_METHOD, true) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindEntriesMethod())) { + return annotationValues.getFindEntriesMethod() + + targetEntity.getSimpleTypeName() + "Entries"; + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays + .asList(JavaType.INT_PRIMITIVE, JavaType.INT_PRIMITIVE, JavaType.STRING, JavaType.STRING); + } + }, + + FLUSH(CustomDataKeys.FLUSH_METHOD, false) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFlushMethod())) { + return annotationValues.getFlushMethod(); + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + }, + + MERGE(CustomDataKeys.MERGE_METHOD, false) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getMergeMethod())) { + return annotationValues.getMergeMethod(); + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + }, + + PERSIST(CustomDataKeys.PERSIST_METHOD, false) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getPersistMethod())) { + return annotationValues.getPersistMethod(); + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + }, + + REMOVE(CustomDataKeys.REMOVE_METHOD, false) { + @Override + public String getName(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getRemoveMethod())) { + return annotationValues.getRemoveMethod(); + } + return null; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + }; + + /** + * Returns the {@link EntityLayerMethod} with the given ID and parameter + * types + * + * @param methodIdentifier the ID to seek; will not match if blank + * @param callerParameters will not match if null + * @param targetEntity + * @param idType specifies the ID type used by the target entity (required) + * @return + */ + public static EntityLayerMethod valueOf(final String methodIdentifier, + final List callerParameters, final JavaType targetEntity, + final JavaType idType) { + // Look for matching method name and parameter types + for (final EntityLayerMethod method : values()) { + if (method.id.equals(methodIdentifier) + && method.getParameterTypes(targetEntity, idType).equals( + callerParameters)) { + return method; + } + } + return null; + } + + private final String id; + + private final boolean isStatic; + + /** + * Constructor + * + * @param id a unique id for this method (required) + * @param isStatic whether this method is static + */ + private EntityLayerMethod(final MethodMetadataCustomDataKey key, + final boolean isStatic) { + Validate.notNull(key, "Key is required"); + id = key.name(); + this.isStatic = isStatic; + } + + /** + * Returns the Java snippet that invokes this method, including the target + * if any + * + * @param annotationValues the CRUD-related values of the + * {@link RooJpaActiveRecord} annotation on the entity type + * @param targetEntity the type of entity being managed (required) + * @param plural the plural form of the entity (required) + * @param callerParameters the caller's method's parameters (required) + * @return a non-blank Java snippet + */ + public String getCall(final JpaCrudAnnotationValues annotationValues, + final JavaType targetEntity, final String plural, + final List callerParameters) { + final String target; + if (isStatic) { + target = targetEntity.getSimpleTypeName(); + } + else { + target = callerParameters.get(0).getValue().getSymbolName(); + } + final List parameters = getParameters(callerParameters); + return getCall(target, getName(annotationValues, targetEntity, plural), + parameters.iterator()); + } + + /** + * Generates a method call from the given inputs + * + * @param targetName the name of the target on which the method is being + * invoked (required) + * @param methodName the name of the method being invoked (required) + * @param parameterNames the names of the parameters (from the caller's POV) + * @return a non-blank Java snippet ending in ")" + */ + private String getCall(final String targetName, final String methodName, + final Iterator parameters) { + final StringBuilder methodCall = new StringBuilder(); + methodCall.append(targetName); + methodCall.append("."); + methodCall.append(methodName); + methodCall.append("("); + while (parameters.hasNext()) { + methodCall.append(parameters.next().getValue().getSymbolName()); + if (parameters.hasNext()) { + methodCall.append(", "); + } + } + methodCall.append(")"); + return methodCall.toString(); + } + + /** + * Returns the desired name of this method based on the given annotation + * values + * + * @param annotationValues the values of the {@link RooJpaActiveRecord} + * annotation on the entity type + * @param targetEntity the entity type (required) + * @param plural the plural form of the entity (required) + * @return null if the method isn't desired for that entity + */ + public abstract String getName(JpaCrudAnnotationValues annotationValues, + JavaType targetEntity, String plural); + + /** + * Returns the parameters to be passed when this method is invoked + * + * @param callerParameters the parameters provided by the caller (required) + * @return a non-null List + */ + public List getParameters( + final Collection callerParameters) { + final List parameters = new ArrayList( + callerParameters); + if (!isStatic) { + parameters.remove(0); // the instance doesn't need itself as a + // parameter + } + return parameters; + } + + /** + * Returns the type of parameters taken by this method + * + * @param targetEntity the type of entity being managed + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null list + */ + protected abstract List getParameterTypes(JavaType targetEntity, + JavaType idType); + + /** + * Indicates whether this method is static + * + * @return + */ + public boolean isStatic() { + return isStatic; + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerProvider.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerProvider.java new file mode 100644 index 000000000..775f7c615 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerProvider.java @@ -0,0 +1,144 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ImportMetadataBuilder; +import org.springframework.roo.classpath.layers.CoreLayerProvider; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.PairList; + +/** + * The {@link org.springframework.roo.classpath.layers.LayerProvider} for the + * {@link LayerType#ACTIVE_RECORD} layer. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class EntityLayerProvider extends CoreLayerProvider { + + @Reference private JpaActiveRecordMetadataProvider jpaActiveRecordMetadataProvider; + @Reference private MetadataService metadataService; + @Reference TypeLocationService typeLocationService; + + public int getLayerPosition() { + return LayerType.ACTIVE_RECORD.getPosition(); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final MethodParameter... callerParameters) { + + return getMemberTypeAdditions(callerMID, methodIdentifier, + targetEntity, idType, true, callerParameters); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, boolean autowire, + final MethodParameter... callerParameters) { + Validate.isTrue(StringUtils.isNotBlank(callerMID), + "Metadata identifier required"); + Validate.notBlank(methodIdentifier, "Method identifier required"); + Validate.notNull(targetEntity, "Target enitity type required"); + + // Get the CRUD-related values of this entity's @RooJpaActiveRecord + // annotation + final JpaCrudAnnotationValues annotationValues = jpaActiveRecordMetadataProvider + .getAnnotationValues(targetEntity); + if (annotationValues == null) { + return null; + } + + // Check the entity has a plural form + final String plural = getPlural(targetEntity); + if (StringUtils.isBlank(plural)) { + return null; + } + + // Look for an entity layer method with this ID and types of parameter + final List parameterTypes = new PairList( + callerParameters).getKeys(); + final EntityLayerMethod method = EntityLayerMethod.valueOf( + methodIdentifier, parameterTypes, targetEntity, idType); + if (method == null) { + return null; + } + + // It's an entity layer method; see if it's specified by the annotation + final String methodName = method.getName(annotationValues, + targetEntity, plural); + if (StringUtils.isBlank(methodName)) { + return null; + } + + // We have everything needed to generate a method call + final List callerParameterList = Arrays + .asList(callerParameters); + final String methodCall = method.getCall(annotationValues, + targetEntity, plural, callerParameterList); + final ClassOrInterfaceTypeDetailsBuilder additionsBuilder = new ClassOrInterfaceTypeDetailsBuilder( + callerMID); + if (method.isStatic()) { + additionsBuilder.add(ImportMetadataBuilder.getImport(callerMID, + targetEntity)); + } + return new MemberTypeAdditions(additionsBuilder, methodName, + methodCall, method.isStatic(), + method.getParameters(callerParameterList)); + } + + /** + * Returns the plural form of the given entity + * + * @param javaType the entity for which to get the plural (required) + * @return null if it can't be found or is actually + * null + */ + private String getPlural(final JavaType javaType) { + final String key = PluralMetadata.createIdentifier(javaType, + typeLocationService.getTypePath(javaType)); + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(key); + if (pluralMetadata == null) { + // Can't acquire the plural + return null; + } + return pluralMetadata.getPlural(); + } + + /** + * For use by unit tests + * + * @param jpaActiveRecordMetadataProvider + */ + void setJpaActiveRecordMetadataProvider( + final JpaActiveRecordMetadataProvider jpaActiveRecordMetadataProvider) { + this.jpaActiveRecordMetadataProvider = jpaActiveRecordMetadataProvider; + } + + /** + * For use by unit tests + * + * @param metadataService + */ + void setMetadataService(final MetadataService metadataService) { + this.metadataService = metadataService; + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadata.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadata.java new file mode 100644 index 000000000..dacfe9561 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadata.java @@ -0,0 +1,962 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JpaJavaType.ENTITY_MANAGER; +import static org.springframework.roo.model.JpaJavaType.PERSISTENCE_CONTEXT; +import static org.springframework.roo.model.SpringJavaType.PROPAGATION; +import static org.springframework.roo.model.SpringJavaType.TRANSACTIONAL; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for a type annotated with {@link RooJpaActiveRecord}. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +public class JpaActiveRecordMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final JavaType COUNT_RETURN_TYPE = JavaType.LONG_PRIMITIVE; + private static final String ENTITY_MANAGER_METHOD_NAME = "entityManager"; + private static final String PROVIDES_TYPE_STRING = JpaActiveRecordMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + private JpaCrudAnnotationValues crudAnnotationValues; + private MethodMetadata entityManagerMethod; + private String entityName; + private MethodMetadata findMethod; + private FieldMetadata identifierField; + private boolean isGaeEnabled; + private JpaActiveRecordMetadata parent; + private String plural; + + /** + * Constructor + * + * @param metadataIdentificationString (required) + * @param aspectName (required) + * @param governorPhysicalTypeMetadata (required) + * @param parent can be null + * @param projectMetadata (required) + * @param crudAnnotationValues the CRUD-related annotation values (required) + * @param plural the plural form of the entity (required) + * @param identifierField the entity's identifier field (required) + * @param entityName the JPA entity name (required) + */ + public JpaActiveRecordMetadata(final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final JpaActiveRecordMetadata parent, + final JpaCrudAnnotationValues crudAnnotationValues, + final String plural, final FieldMetadata identifierField, + final String entityName, final boolean isGaeEnabled) { + super(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(metadataIdentificationString), + "Metadata identification string '%s' does not appear to be a valid", + metadataIdentificationString); + Validate.notNull(crudAnnotationValues, + "CRUD-related annotation values required"); + Validate.notNull(identifierField, "Identifier required for '%s'", + metadataIdentificationString); + Validate.notBlank(entityName, "Entity name required for '%s'", + metadataIdentificationString); + Validate.notBlank(plural, "Plural required for '%s'", + metadataIdentificationString); + + if (!isValid()) { + return; + } + + this.crudAnnotationValues = crudAnnotationValues; + this.entityName = entityName; + this.identifierField = identifierField; + this.isGaeEnabled = isGaeEnabled; + this.parent = parent; + this.plural = StringUtils.capitalize(plural); + + // Determine the entity's "entityManager" field, which is guaranteed to + // be accessible to the ITD. + builder.addField(getEntityManagerField()); + + builder.addField(getFieldNames4OrderClauseFilter()); + + // Add static methods + setEntityManagerMethod(); + builder.addMethod(getCountMethod()); + builder.addMethod(getFindAllMethod()); + builder.addMethod(getFindAllSortedMethod()); + setFindMethod(); + builder.addMethod(getFindEntriesMethod()); + builder.addMethod(getFindEntriesSortedMethod()); + + // Add helper methods + builder.addMethod(getPersistMethod()); + builder.addMethod(getRemoveMethod()); + builder.addMethod(getFlushMethod()); + builder.addMethod(getClearMethod()); + builder.addMethod(getMergeMethod()); + + builder.putCustomData(CustomDataKeys.DYNAMIC_FINDER_NAMES, + getDynamicFinders()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentifierType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + /** + * @return the dynamic, custom finders (never returns null, but may return + * an empty list) + */ + public List getDynamicFinders() { + if (crudAnnotationValues.getFinders() == null) { + return Collections.emptyList(); + } + return Arrays.asList(crudAnnotationValues.getFinders()); + } + + /** + * Locates the entity manager field that should be used. + *

    + * If a parent is defined, it must provide the field unless a persistent + * unit name is supplied on the child entity. + *

    + * We generally expect the field to be named "entityManager" and be of type + * javax.persistence.EntityManager. We also require it to be public or + * protected, and annotated with @PersistenceContext. If there is an + * existing field which doesn't meet these latter requirements, we add an + * underscore prefix to the "entityManager" name and try again, until such + * time as we come up with a unique name that either meets the requirements + * or the name is not used and we will create it. + * + * @return the entity manager field (never returns null) + */ + public FieldMetadata getEntityManagerField() { + if (parent != null + && StringUtils.isBlank(crudAnnotationValues + .getPersistenceUnit())) { + // The parent is required to guarantee this is available + return parent.getEntityManagerField(); + } + + // Need to locate it ourself + int index = -1; + while (true) { + // Compute the required field name + index++; + final JavaSymbolName fieldSymbolName = new JavaSymbolName( + StringUtils.repeat("_", index) + "entityManager"); + final FieldMetadata candidate = governorTypeDetails + .getField(fieldSymbolName); + if (candidate != null) { + // Verify if candidate is suitable + + if (!Modifier.isPublic(candidate.getModifier()) + && !Modifier.isProtected(candidate.getModifier()) + && Modifier.TRANSIENT != candidate.getModifier()) { + // Candidate is not public and not protected and not simply + // a transient field (in which case subclasses + // will see the inherited field), so any subsequent + // subclasses won't be able to see it. Give up! + continue; + } + + if (!candidate.getFieldType().equals(ENTITY_MANAGER)) { + // Candidate isn't an EntityManager, so give up + continue; + } + + if (MemberFindingUtils.getAnnotationOfType( + candidate.getAnnotations(), PERSISTENCE_CONTEXT) == null) { + // Candidate doesn't have a PersistenceContext annotation, + // so give up + continue; + } + + // If we got this far, we found a valid candidate + return candidate; + } + + // Candidate not found, so let's create one + final List annotations = new ArrayList(); + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + PERSISTENCE_CONTEXT); + if (StringUtils.isNotBlank(crudAnnotationValues + .getPersistenceUnit())) { + annotationBuilder.addStringAttribute("unitName", + crudAnnotationValues.getPersistenceUnit()); + } + annotations.add(annotationBuilder); + + return new FieldMetadataBuilder(getId(), Modifier.TRANSIENT, + annotations, fieldSymbolName, ENTITY_MANAGER).build(); + } + } + + + /** + * @return list of filedNames allowed for the "order by" clause (used to avoid JPQL injection) + */ + public FieldMetadata getFieldNames4OrderClauseFilter() { + + JavaSymbolName fieldName = new JavaSymbolName("fieldNames4OrderClauseFilter"); + JavaType fieldType = JavaType.listOf(JavaType.STRING); + + // Locate user-defined method + final FieldMetadata userField = governorTypeDetails.getField(fieldName); + if (userField != null) { + return userField; + } + + List listOfFieldNames = new ArrayList(); + for (final FieldMetadata field : governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails().getDeclaredFields()) { + listOfFieldNames.add(field.getFieldName().getSymbolName()); + } + + String listOfFieldNamesAsStringExpr = "java.util.Arrays.asList(\""+ StringUtils.join(listOfFieldNames, "\", \"") + "\")"; + + return new FieldMetadataBuilder(getId(), Modifier.FINAL + Modifier.STATIC + Modifier.PUBLIC, + fieldName, fieldType, listOfFieldNamesAsStringExpr).build(); + } + + + /** + * @return the static utility entityManager() method used by other methods + * to obtain entity manager and available as a utility for user code + * (never returns nulls) + */ + public MethodMetadata getEntityManagerMethod() { + return entityManagerMethod; + } + + /** + * Returns the JPA name of this entity. + * + * @return a non-null name (might be empty) + */ + public String getEntityName() { + return entityName; + } + + /** + * @return the find (by ID) method (may return null) + */ + public MethodMetadata getFindMethod() { + return findMethod; + } + + /** + * @return the pluralised name (never returns null or an empty string) + */ + public String getPlural() { + return plural; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("finders", crudAnnotationValues.getFinders()); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } + + private void addTransactionalAnnotation( + final List annotations) { + addTransactionalAnnotation(annotations, false); + } + + private void addTransactionalAnnotation( + final List annotations, + final boolean isPersistMethod) { + final AnnotationMetadataBuilder transactionalBuilder = new AnnotationMetadataBuilder( + TRANSACTIONAL); + if (StringUtils + .isNotBlank(crudAnnotationValues.getTransactionManager())) { + transactionalBuilder.addStringAttribute("value", + crudAnnotationValues.getTransactionManager()); + } + if (isGaeEnabled && isPersistMethod) { + transactionalBuilder.addEnumAttribute("propagation", + new EnumDetails(PROPAGATION, new JavaSymbolName( + "REQUIRES_NEW"))); + } + annotations.add(transactionalBuilder); + } + + /** + * @return the clear method (never returns null) + */ + private MethodMetadataBuilder getClearMethod() { + if (parent != null) { + final MethodMetadataBuilder found = parent.getClearMethod(); + if (found != null) { + return found; + } + } + if ("".equals(crudAnnotationValues.getClearMethod())) { + return null; + } + return getDelegateMethod( + new JavaSymbolName(crudAnnotationValues.getClearMethod()), + "clear"); + } + + /** + * Finds (creating if necessary) the method that counts entities of this + * type + * + * @return the count method (never null) + */ + private MethodMetadata getCountMethod() { + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName( + crudAnnotationValues.getCountMethod() + plural); + final JavaType[] parameterTypes = {}; + final List parameterNames = Collections + . emptyList(); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType() + .equals(COUNT_RETURN_TYPE), + "Method '%s' on '%s' must return '%s'", methodName, + destination, COUNT_RETURN_TYPE + .getNameIncludingTypeParameters()); + return userMethod; + } + + // Create method + final List annotations = new ArrayList(); + if (isGaeEnabled) { + addTransactionalAnnotation(annotations); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + if (isGaeEnabled) { + bodyBuilder.appendFormalLine("return " + + getFindAllMethod().getMethodName() + "().size();"); + } + else { + bodyBuilder.appendFormalLine("return " + ENTITY_MANAGER_METHOD_NAME + + "().createQuery(\"SELECT COUNT(o) FROM " + entityName + + " o\", Long.class).getSingleResult();"); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + COUNT_RETURN_TYPE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder.build(); + } + + private MethodMetadataBuilder getDelegateMethod( + final JavaSymbolName methodName, final String methodDelegateName) { + // Method definition to find or build + final JavaType[] parameterTypes = {}; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + return new MethodMetadataBuilder(userMethod); + } + + // Create the method + final List annotations = new ArrayList(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + // Address non-injected entity manager field + final MethodMetadata entityManagerMethod = getEntityManagerMethod(); + Validate.notNull(entityManagerMethod, + "Entity manager method should not have returned null"); + + // Use the getEntityManager() method to acquire an entity manager (the + // method will throw an exception if it cannot be acquired) + final String entityManagerFieldName = getEntityManagerField() + .getFieldName().getSymbolName(); + bodyBuilder.appendFormalLine("if (this." + entityManagerFieldName + + " == null) this." + entityManagerFieldName + " = " + + entityManagerMethod.getMethodName().getSymbolName() + "();"); + + JavaType returnType = JavaType.VOID_PRIMITIVE; + if ("flush".equals(methodDelegateName)) { + addTransactionalAnnotation(annotations); + bodyBuilder.appendFormalLine("this." + entityManagerFieldName + + ".flush();"); + } + else if ("clear".equals(methodDelegateName)) { + addTransactionalAnnotation(annotations); + bodyBuilder.appendFormalLine("this." + entityManagerFieldName + + ".clear();"); + } + else if ("merge".equals(methodDelegateName)) { + addTransactionalAnnotation(annotations); + returnType = new JavaType(destination.getSimpleTypeName()); + bodyBuilder.appendFormalLine(destination.getSimpleTypeName() + + " merged = this." + entityManagerFieldName + + ".merge(this);"); + bodyBuilder.appendFormalLine("this." + entityManagerFieldName + + ".flush();"); + bodyBuilder.appendFormalLine("return merged;"); + } + else if ("remove".equals(methodDelegateName)) { + addTransactionalAnnotation(annotations); + bodyBuilder.appendFormalLine("if (this." + entityManagerFieldName + + ".contains(this)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("this." + entityManagerFieldName + + ".remove(this);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(destination.getSimpleTypeName() + + " attached = " + destination.getSimpleTypeName() + "." + + getFindMethod().getMethodName().getSymbolName() + + "(this." + identifierField.getFieldName().getSymbolName() + + ");"); + bodyBuilder.appendFormalLine("this." + entityManagerFieldName + + ".remove(attached);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else { + // Persist + addTransactionalAnnotation(annotations, true); + bodyBuilder.appendFormalLine("this." + entityManagerFieldName + "." + + methodDelegateName + "(this);"); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + new ArrayList(), bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return the find all method (may return null) + */ + private MethodMetadata getFindAllMethod() { + if ("".equals(crudAnnotationValues.getFindAllMethod())) { + return null; + } + + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName( + crudAnnotationValues.getFindAllMethod() + plural); + final JavaType[] parameterTypes = {}; + final List parameterNames = new ArrayList(); + final JavaType returnType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(destination)); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return userMethod; + } + + // Create method + final List annotations = new ArrayList(); + if (isGaeEnabled) { + addTransactionalAnnotation(annotations); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return " + ENTITY_MANAGER_METHOD_NAME + + "().createQuery(\"SELECT o FROM " + entityName + " o\", " + + destination.getSimpleTypeName() + ".class).getResultList();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder.build(); + } + + /** + * getFindAllMethod method with sortFieldName and sortOrder parameters + * + * @return the find all method (may return null) + */ + private MethodMetadata getFindAllSortedMethod() { + if ("".equals(crudAnnotationValues.getFindAllSortedMethod())) { + return null; + } + + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName( + crudAnnotationValues.getFindAllSortedMethod() + plural); + final JavaType[] parameterTypes = {STRING, STRING}; + final List parameterNames = Arrays.asList( + new JavaSymbolName("sortFieldName"), + new JavaSymbolName("sortOrder")); + final JavaType returnType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(destination)); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return userMethod; + } + + // Create method + final List annotations = new ArrayList(); + if (isGaeEnabled) { + addTransactionalAnnotation(annotations); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("String jpaQuery = \"SELECT o FROM " + entityName + " o\";"); + bodyBuilder.appendFormalLine("if (fieldNames4OrderClauseFilter.contains(sortFieldName)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("jpaQuery = jpaQuery + \" ORDER BY \" + sortFieldName;"); + bodyBuilder.appendFormalLine("if (\"ASC\".equalsIgnoreCase(sortOrder) || \"DESC\".equalsIgnoreCase(sortOrder)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("jpaQuery = jpaQuery + \" \" + sortOrder;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return " + ENTITY_MANAGER_METHOD_NAME + + "().createQuery(jpaQuery, " + destination.getSimpleTypeName() + ".class" + ").getResultList();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder.build(); + } + + /** + * @return the find entries method (may return null) + */ + private MethodMetadata getFindEntriesMethod() { + if ("".equals(crudAnnotationValues.getFindEntriesMethod())) { + return null; + } + + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName( + crudAnnotationValues.getFindEntriesMethod() + + destination.getSimpleTypeName() + "Entries"); + final JavaType[] parameterTypes = { INT_PRIMITIVE, INT_PRIMITIVE }; + final List parameterNames = Arrays.asList( + new JavaSymbolName("firstResult"), new JavaSymbolName( + "maxResults")); + final JavaType returnType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(destination)); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return userMethod; + } + + // Create method + final List annotations = new ArrayList(); + if (isGaeEnabled) { + addTransactionalAnnotation(annotations); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("return " + + ENTITY_MANAGER_METHOD_NAME + + "().createQuery(\"SELECT o FROM " + + entityName + + " o\", " + + destination.getSimpleTypeName() + + ".class).setFirstResult(firstResult).setMaxResults(maxResults).getResultList();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder.build(); + } + + /** + * getFindEntriesMethod method with sortFieldName and sortOrder parameters + * + * @return the find entries method (may return null) + */ + private MethodMetadata getFindEntriesSortedMethod() { + if ("".equals(crudAnnotationValues.getFindEntriesSortedMethod())) { + return null; + } + + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName( + crudAnnotationValues.getFindEntriesSortedMethod() + + destination.getSimpleTypeName() + "Entries"); + final JavaType[] parameterTypes = { INT_PRIMITIVE, INT_PRIMITIVE, JavaType.STRING, JavaType.STRING}; + final List parameterNames = Arrays.asList( + new JavaSymbolName("firstResult"), new JavaSymbolName( + "maxResults"), new JavaSymbolName("sortFieldName"), + new JavaSymbolName("sortOrder")); + final JavaType returnType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(destination)); + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return userMethod; + } + + // Create method + final List annotations = new ArrayList(); + if (isGaeEnabled) { + addTransactionalAnnotation(annotations); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("String jpaQuery = \"SELECT o FROM " + entityName + " o\";"); + bodyBuilder.appendFormalLine("if (fieldNames4OrderClauseFilter.contains(sortFieldName)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("jpaQuery = jpaQuery + \" ORDER BY \" + sortFieldName;"); + bodyBuilder.appendFormalLine("if (\"ASC\".equalsIgnoreCase(sortOrder) || \"DESC\".equalsIgnoreCase(sortOrder)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("jpaQuery = jpaQuery + \" \" + sortOrder;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return " + ENTITY_MANAGER_METHOD_NAME + + "().createQuery(jpaQuery, " + destination.getSimpleTypeName() + ".class).setFirstResult(firstResult).setMaxResults(maxResults).getResultList();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder.build(); + } + + /** + * @return the flush method (never returns null) + */ + private MethodMetadataBuilder getFlushMethod() { + if (parent != null) { + final MethodMetadataBuilder found = parent.getFlushMethod(); + if (found != null) { + return found; + } + } + if ("".equals(crudAnnotationValues.getFlushMethod())) { + return null; + } + return getDelegateMethod( + new JavaSymbolName(crudAnnotationValues.getFlushMethod()), + "flush"); + } + + /** + * @return the merge method (may return null) + */ + private MethodMetadataBuilder getMergeMethod() { + if ("".equals(crudAnnotationValues.getMergeMethod())) { + return null; + } + return getDelegateMethod( + new JavaSymbolName(crudAnnotationValues.getMergeMethod()), + "merge"); + } + + /** + * @return the persist method (may return null) + */ + private MethodMetadataBuilder getPersistMethod() { + if (parent != null) { + final MethodMetadataBuilder found = parent.getPersistMethod(); + if (found != null) { + return found; + } + } + if ("".equals(crudAnnotationValues.getPersistMethod())) { + return null; + } + return getDelegateMethod( + new JavaSymbolName(crudAnnotationValues.getPersistMethod()), + "persist"); + } + + /** + * @return the remove method (may return null) + */ + private MethodMetadataBuilder getRemoveMethod() { + if (parent != null) { + final MethodMetadataBuilder found = parent.getRemoveMethod(); + if (found != null) { + return found; + } + } + if ("".equals(crudAnnotationValues.getRemoveMethod())) { + return null; + } + return getDelegateMethod( + new JavaSymbolName(crudAnnotationValues.getRemoveMethod()), + "remove"); + } + + private void setEntityManagerMethod() { + if (parent != null) { + // The parent is required to guarantee this is available + entityManagerMethod = parent.getEntityManagerMethod(); + return; + } + + // Method definition to find or build + final JavaSymbolName methodName = new JavaSymbolName( + ENTITY_MANAGER_METHOD_NAME); + final JavaType[] parameterTypes = {}; + final JavaType returnType = ENTITY_MANAGER; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + entityManagerMethod = userMethod; + return; + } + + // Create method + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + if (Modifier.isAbstract(governorTypeDetails.getModifier())) { + // Create an anonymous inner class that extends the abstract class + // (no-arg constructor is available as this is a JPA entity) + bodyBuilder.appendFormalLine(ENTITY_MANAGER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()) + + " em = new " + destination.getSimpleTypeName() + "() {"); + // Handle any abstract methods in this class + bodyBuilder.indent(); + for (final MethodMetadata method : governorTypeDetails.getMethods()) { + if (Modifier.isAbstract(method.getModifier())) { + final StringBuilder params = new StringBuilder(); + int i = -1; + final List types = method + .getParameterTypes(); + for (final JavaSymbolName name : method.getParameterNames()) { + i++; + if (i > 0) { + params.append(", "); + } + final AnnotatedJavaType type = types.get(i); + params.append(type.toString()).append(" ").append(name); + } + final int newModifier = method.getModifier() + - Modifier.ABSTRACT; + bodyBuilder.appendFormalLine(Modifier.toString(newModifier) + + " " + + method.getReturnType() + .getNameIncludingTypeParameters() + " " + + method.getMethodName().getSymbolName() + "(" + + params.toString() + ") {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("throw new UnsupportedOperationException();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + } + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}." + + getEntityManagerField().getFieldName().getSymbolName() + + ";"); + } + else { + // Instantiate using the no-argument constructor (we know this is + // available as the entity must comply with the JPA no-arg + // constructor requirement) + bodyBuilder.appendFormalLine(ENTITY_MANAGER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()) + + " em = new " + + destination.getSimpleTypeName() + + "()." + + getEntityManagerField().getFieldName().getSymbolName() + + ";"); + } + + bodyBuilder + .appendFormalLine("if (em == null) throw new IllegalStateException(\"Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)\");"); + bodyBuilder.appendFormalLine("return em;"); + final int modifier = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL; + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), modifier, methodName, returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + new ArrayList(), bodyBuilder); + builder.addMethod(methodBuilder); + entityManagerMethod = methodBuilder.build(); + } + + /** + * @return the find (by ID) method (may return null) + */ + private void setFindMethod() { + if ("".equals(crudAnnotationValues.getFindMethod())) { + return; + } + + // Method definition to find or build + final String idFieldName = identifierField.getFieldName() + .getSymbolName(); + final JavaSymbolName methodName = new JavaSymbolName( + crudAnnotationValues.getFindMethod() + + destination.getSimpleTypeName()); + final JavaType parameterType = identifierField.getFieldType(); + final List parameterNames = Arrays + .asList(new JavaSymbolName(idFieldName)); + final JavaType returnType = destination; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterType); + if (userMethod != null) { + Validate.isTrue( + userMethod.getReturnType().equals(returnType), + "Method '" + methodName + "' on '" + returnType + + "' must return '" + + returnType.getNameIncludingTypeParameters() + "'"); + findMethod = userMethod; + return; + } + + // Create method + final List annotations = new ArrayList(); + if (isGaeEnabled) { + addTransactionalAnnotation(annotations); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + if (JavaType.STRING.equals(identifierField.getFieldType())) { + bodyBuilder.appendFormalLine("if (" + idFieldName + " == null || " + + idFieldName + ".length() == 0) return null;"); + } + else if (!identifierField.getFieldType().isPrimitive()) { + bodyBuilder.appendFormalLine("if (" + idFieldName + + " == null) return null;"); + } + + bodyBuilder.appendFormalLine("return " + ENTITY_MANAGER_METHOD_NAME + + "().find(" + returnType.getSimpleTypeName() + ".class, " + + idFieldName + ");"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + builder.addMethod(methodBuilder); + findMethod = methodBuilder.build(); + } + + + +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadataProvider.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadataProvider.java new file mode 100644 index 000000000..d35dc2dd5 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadataProvider.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; +import org.springframework.roo.model.JavaType; + +/** + * Provides {@link JpaActiveRecordMetadata}. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.1 + */ +public interface JpaActiveRecordMetadataProvider extends + ItdTriggerBasedMetadataProvider { + + /** + * Returns the values of the CRUD-related annotation on the given Java type + * (if any). + * + * @param javaType can be null + * @return null if no values can be found + * @since 1.2.0 + */ + JpaCrudAnnotationValues getAnnotationValues(JavaType javaType); +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadataProviderImpl.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadataProviderImpl.java new file mode 100644 index 000000000..630d06408 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaActiveRecordMetadataProviderImpl.java @@ -0,0 +1,358 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.CLEAR_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.COUNT_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_ALL_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FLUSH_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.MERGE_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.PERSIST_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.REMOVE_METHOD_DEFAULT; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.CLEAR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FLUSH_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; + +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.addon.jpa.entity.JpaEntityAnnotationValues; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.addon.plural.PluralMetadataProvider; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator; +import org.springframework.roo.classpath.customdata.taggers.MethodMatcher; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link JpaActiveRecordMetadataProvider}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class JpaActiveRecordMetadataProviderImpl extends + AbstractItdMetadataProvider implements JpaActiveRecordMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JpaActiveRecordMetadataProviderImpl.class); + + private ConfigurableMetadataProvider configurableMetadataProvider; + private CustomDataKeyDecorator customDataKeyDecorator; + private PluralMetadataProvider pluralMetadataProvider; + private ProjectOperations projectOperations; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_JPA_ACTIVE_RECORD); + getConfigurableMetadataProvider().addMetadataTrigger(ROO_JPA_ACTIVE_RECORD); + getPluralMetadataProvider().addMetadataTrigger(ROO_JPA_ACTIVE_RECORD); + registerMatchers(); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return JpaActiveRecordMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_JPA_ACTIVE_RECORD); + getConfigurableMetadataProvider() + .removeMetadataTrigger(ROO_JPA_ACTIVE_RECORD); + getPluralMetadataProvider().removeMetadataTrigger(ROO_JPA_ACTIVE_RECORD); + getCustomDataKeyDecorator().unregisterMatchers(getClass()); + } + + public JpaCrudAnnotationValues getAnnotationValues(final JavaType javaType) { + Validate.notNull(javaType, "JavaType required"); + final String physicalTypeId = getTypeLocationService() + .getPhysicalTypeIdentifier(javaType); + if (StringUtils.isBlank(physicalTypeId)) { + return null; + } + final MemberHoldingTypeDetailsMetadataItem governor = (MemberHoldingTypeDetailsMetadataItem) getMetadataService() + .get(physicalTypeId); + if (MemberFindingUtils.getAnnotationOfType(governor, + ROO_JPA_ACTIVE_RECORD) == null) { + // The type is not annotated with @RooJpaActiveRecord + return null; + } + return new JpaCrudAnnotationValues(governor); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = JpaActiveRecordMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JpaActiveRecordMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Jpa_ActiveRecord"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalType, + final String itdFilename) { + + // Get the CRUD-related annotation values + final JpaCrudAnnotationValues crudAnnotationValues = new JpaCrudAnnotationValues( + governorPhysicalType); + // Get the purely JPA-related annotation values, from @RooJpaEntity if + // present, otherwise from @RooJpaActiveRecord + JpaEntityAnnotationValues jpaEntityAnnotationValues = new JpaEntityAnnotationValues( + governorPhysicalType, ROO_JPA_ENTITY); + if (!jpaEntityAnnotationValues.isAnnotationFound()) { + jpaEntityAnnotationValues = new JpaEntityAnnotationValues( + governorPhysicalType, ROO_JPA_ACTIVE_RECORD); + Validate.validState(jpaEntityAnnotationValues.isAnnotationFound(), + "No @RooJpaEntity or @RooJpaActiveRecord on %s", + metadataIdentificationString); + } + + // Look up the inheritance hierarchy for existing + // JpaActiveRecordMetadata + final JpaActiveRecordMetadata parent = getParentMetadata(governorPhysicalType + .getMemberHoldingTypeDetails()); + + // If the parent is null, but the type has a super class it is likely + // that the we don't have information to proceed + if (parent == null + && governorPhysicalType.getMemberHoldingTypeDetails() + .getSuperclass() != null) { + // If the superclass is not annotated with the Entity trigger + // annotation then we can be pretty sure that we don't have enough + // information to proceed + if (MemberFindingUtils.getAnnotationOfType(governorPhysicalType + .getMemberHoldingTypeDetails().getAnnotations(), + ROO_JPA_ACTIVE_RECORD) != null) { + return null; + } + } + // We also need the plural + final JavaType entity = JpaActiveRecordMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JpaActiveRecordMetadata + .getPath(metadataIdentificationString); + final String pluralId = PluralMetadata.createIdentifier(entity, path); + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(pluralId); + if (pluralMetadata == null) { + // Can't acquire the plural + return null; + } + getMetadataDependencyRegistry().registerDependency(pluralId, + metadataIdentificationString); + + final List idFields = getPersistenceMemberLocator() + .getIdentifierFields(entity); + if (idFields.size() != 1) { + // The ID field metadata is either unavailable or not stable yet + return null; + } + final FieldMetadata idField = idFields.get(0); + + final String entityName = StringUtils.defaultIfEmpty( + jpaEntityAnnotationValues.getEntityName(), + entity.getSimpleTypeName()); + + boolean isGaeEnabled = false; + + final String moduleName = path.getModule(); + if (getProjectOperations().isProjectAvailable(moduleName)) { + // If the project itself changes, we want a chance to refresh this + // item + getMetadataDependencyRegistry().registerDependency( + ProjectMetadata.getProjectIdentifier(moduleName), + metadataIdentificationString); + isGaeEnabled = getProjectOperations().isFeatureInstalledInModule( + FeatureNames.GAE, moduleName); + } + + return new JpaActiveRecordMetadata(metadataIdentificationString, + aspectName, governorPhysicalType, parent, crudAnnotationValues, + pluralMetadata.getPlural(), idField, entityName, isGaeEnabled); + } + + public String getProvidesType() { + return JpaActiveRecordMetadata.getMetadataIdentifierType(); + } + + @SuppressWarnings("unchecked") + private void registerMatchers() { + + getCustomDataKeyDecorator() + .registerMatchers(getClass(), + new MethodMatcher(CLEAR_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("clearMethod"), + CLEAR_METHOD_DEFAULT), new MethodMatcher( + COUNT_ALL_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("countMethod"), + COUNT_METHOD_DEFAULT, true, false), + new MethodMatcher(FIND_ALL_METHOD, + ROO_JPA_ACTIVE_RECORD, new JavaSymbolName( + "findAllMethod"), + FIND_ALL_METHOD_DEFAULT, true, false), + new MethodMatcher(FIND_ENTRIES_METHOD, + ROO_JPA_ACTIVE_RECORD, new JavaSymbolName( + "findEntriesMethod"), "find", false, + true, "Entries"), + new MethodMatcher(FIND_ALL_SORTED_METHOD, + ROO_JPA_ACTIVE_RECORD, new JavaSymbolName( + "findAllSortedMethod"), + FIND_ALL_METHOD_DEFAULT, true, false, "Sorted"), + new MethodMatcher(FIND_ENTRIES_SORTED_METHOD, + ROO_JPA_ACTIVE_RECORD, new JavaSymbolName( + "findEntriesSortedMethod"), "find", false, + true, "EntriesSorted"), + new MethodMatcher( + FIND_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("findMethod"), + FIND_METHOD_DEFAULT, false, true), + new MethodMatcher(FLUSH_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("flushMethod"), + FLUSH_METHOD_DEFAULT), new MethodMatcher( + MERGE_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("mergeMethod"), + MERGE_METHOD_DEFAULT), new MethodMatcher( + PERSIST_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("persistMethod"), + PERSIST_METHOD_DEFAULT), new MethodMatcher( + REMOVE_METHOD, ROO_JPA_ACTIVE_RECORD, + new JavaSymbolName("removeMethod"), + REMOVE_METHOD_DEFAULT)); + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on JpaActiveRecordMetadataProviderImpl."); + return null; + } + }else{ + return configurableMetadataProvider; + } + + } + + public CustomDataKeyDecorator getCustomDataKeyDecorator(){ + if(customDataKeyDecorator == null){ + // Get all Services implement CustomDataKeyDecorator interface + try { + ServiceReference[] references = context.getAllServiceReferences(CustomDataKeyDecorator.class.getName(), null); + + for(ServiceReference ref : references){ + return (CustomDataKeyDecorator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CustomDataKeyDecorator on JpaActiveRecordMetadataProviderImpl."); + return null; + } + }else{ + return customDataKeyDecorator; + } + + } + + public PluralMetadataProvider getPluralMetadataProvider(){ + if(pluralMetadataProvider == null){ + // Get all Services implement PluralMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(PluralMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (PluralMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PluralMetadataProvider on JpaActiveRecordMetadataProviderImpl."); + return null; + } + }else{ + return pluralMetadataProvider; + } + + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on JpaActiveRecordMetadataProviderImpl."); + return null; + } + }else{ + return projectOperations; + } + + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaCrudAnnotationValues.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaCrudAnnotationValues.java new file mode 100644 index 000000000..038de218b --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/JpaCrudAnnotationValues.java @@ -0,0 +1,122 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.CLEAR_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.COUNT_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_ALL_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_ENTRIES_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_ENTRIES_SORTED_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FIND_ALL_SORTED_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.FLUSH_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.MERGE_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.PERSIST_METHOD_DEFAULT; +import static org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord.REMOVE_METHOD_DEFAULT; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; + +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; + +/** + * The purely CRUD-related values of a parsed {@link RooJpaActiveRecord} + * annotation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JpaCrudAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String clearMethod = CLEAR_METHOD_DEFAULT; + @AutoPopulate private String countMethod = COUNT_METHOD_DEFAULT; + @AutoPopulate private String findAllMethod = FIND_ALL_METHOD_DEFAULT; + @AutoPopulate private String findEntriesMethod = FIND_ENTRIES_METHOD_DEFAULT; + @AutoPopulate private String findAllSortedMethod = FIND_ALL_SORTED_METHOD_DEFAULT; + @AutoPopulate private String findEntriesSortedMethod = FIND_ENTRIES_SORTED_METHOD_DEFAULT; + @AutoPopulate private String[] finders; + @AutoPopulate private String findMethod = FIND_METHOD_DEFAULT; + @AutoPopulate private String flushMethod = FLUSH_METHOD_DEFAULT; + @AutoPopulate private String mergeMethod = MERGE_METHOD_DEFAULT; + @AutoPopulate private String persistenceUnit = ""; + @AutoPopulate private String persistMethod = PERSIST_METHOD_DEFAULT; + @AutoPopulate private String removeMethod = REMOVE_METHOD_DEFAULT; + @AutoPopulate private String transactionManager = ""; + + /** + * Constructor + * + * @param annotatedType + */ + public JpaCrudAnnotationValues( + final MemberHoldingTypeDetailsMetadataItem annotatedType) { + super(annotatedType, ROO_JPA_ACTIVE_RECORD); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getClearMethod() { + return clearMethod; + } + + public String getCountMethod() { + return countMethod; + } + + public String getFindAllMethod() { + return findAllMethod; + } + + public String getFindAllSortedMethod() { + return findAllSortedMethod; + } + + /** + * Returns the prefix for the "find entries" method, e.g. the "find" part of + * "findFooEntries" + * + * @return + */ + public String getFindEntriesMethod() { + return findEntriesMethod; + } + + public String getFindEntriesSortedMethod() { + return findEntriesSortedMethod; + } + + /** + * Returns the custom finder names specified by the annotation + * + * @return + */ + public String[] getFinders() { + return finders; + } + + public String getFindMethod() { + return findMethod; + } + + public String getFlushMethod() { + return flushMethod; + } + + public String getMergeMethod() { + return mergeMethod; + } + + public String getPersistenceUnit() { + return persistenceUnit; + } + + public String getPersistMethod() { + return persistMethod; + } + + public String getRemoveMethod() { + return removeMethod; + } + + public String getTransactionManager() { + return transactionManager; + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/RooJpaActiveRecord.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/RooJpaActiveRecord.java new file mode 100644 index 000000000..b0e2bd530 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/activerecord/RooJpaActiveRecord.java @@ -0,0 +1,231 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.springframework.roo.addon.jpa.entity.RooJpaEntity.ID_FIELD_DEFAULT; +import static org.springframework.roo.addon.jpa.entity.RooJpaEntity.VERSION_COLUMN_DEFAULT; +import static org.springframework.roo.addon.jpa.entity.RooJpaEntity.VERSION_FIELD_DEFAULT; + +import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.roo.addon.jpa.entity.RooJpaEntity; + +/** + * Provides services related to JPA, as a superset of those provided by + * {@link RooJpaEntity}. + * + * @author Ben Alex + * @since 1.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJpaActiveRecord { + + String CLEAR_METHOD_DEFAULT = "clear"; + String COUNT_METHOD_DEFAULT = "count"; + String FIND_ALL_METHOD_DEFAULT = "findAll"; + String FIND_ENTRIES_METHOD_DEFAULT = "find"; + String FIND_METHOD_DEFAULT = "find"; + String FIND_ENTRIES_SORTED_METHOD_DEFAULT = "find"; + String FIND_ALL_SORTED_METHOD_DEFAULT = "findAll"; + String FLUSH_METHOD_DEFAULT = "flush"; + String MERGE_METHOD_DEFAULT = "merge"; + String PERSIST_METHOD_DEFAULT = "persist"; + String REMOVE_METHOD_DEFAULT = "remove"; + + /** + * Specifies the database catalog name that should be used for the entity. + * + * @return the name of the catalog to use (defaults to "") + */ + String catalog() default ""; + + /** + * @return the name of the "clear" method to generate (defaults to + * {@value #CLEAR_METHOD_DEFAULT}; mandatory) + */ + String clearMethod() default CLEAR_METHOD_DEFAULT; + + /** + * @return the prefix of the "count" method to generate (defaults to + * {@value #COUNT_METHOD_DEFAULT}, with the plural of the entity + * appended after the specified method name; mandatory) + */ + String countMethod() default COUNT_METHOD_DEFAULT; + + /** + * Specifies the name used to refer to the entity in queries. + *

    + * The name must not be a reserved literal in JPQL. + * + * @return the name given to the entity (defaults to "") + */ + String entityName() default ""; + + /** + * @return the prefix of the "findAll" method to generate (defaults to + * {@value #FIND_ALL_METHOD_DEFAULT}, with the plural of the entity + * appended after the specified method name; if empty, does not + * create a "find all" method) + */ + String findAllMethod() default FIND_ALL_METHOD_DEFAULT; + + /** + * @return the prefix of the "find[Name]Entries" method to generate + * (defaults to {@value #FIND_ENTRIES_METHOD_DEFAULT}, with the + * simple name of the entity appended after the specified method + * name, followed by "Entries"; mandatory) + */ + String findEntriesMethod() default FIND_ENTRIES_METHOD_DEFAULT; + + /** + * @return an array of strings, with each string being the full name of a + * method that should be created as a "dynamic finder" by an + * additional add-on that can provide implementations of such + * methods (optional) + */ + String[] finders() default ""; + + /** + * @return the prefix of the "find" (by identifier) method to generate + * (defaults to {@value #FIND_METHOD_DEFAULT}, with the simple name + * of the entity appended after the specified method name; + * mandatory) + */ + String findMethod() default FIND_METHOD_DEFAULT; + + /** + * @return the name of the "flush" method to generate (defaults to + * {@value #FLUSH_METHOD_DEFAULT}; mandatory) + */ + String flushMethod() default FLUSH_METHOD_DEFAULT; + + /** + * Specifies the column name that should be used for the identifier field. + * By default this is generally made identical to the + * {@link #identifierField()}, although it will be made unique as required + * for the particular entity fields present. + * + * @return the name of the identifier column to use (defaults to ""; in this + * case it is automatic) + */ + String identifierColumn() default ""; + + /** + * Creates an identifier, unless there is already a JPA @Id field annotation + * in a superclass (either written in normal Java source or introduced by a + * superclass that has the {@link RooJpaActiveRecord} or + * {@link RooJpaEntity} annotation. + *

    + * If you annotate a field with JPA's @Id annotation, it is required that + * you provide a public accessor for that field. + * + * @return the name of the identifier field to use (defaults to + * {@value #ID_FIELD_DEFAULT}; must be provided) + */ + String identifierField() default ID_FIELD_DEFAULT; + + /** + * @return the class of identifier that should be used (defaults to + * {@link Long}; must be provided) + */ + Class identifierType() default Long.class; + + /** + * Specifies the JPA inheritance type that should be used for the entity. + * + * @return the inheritance type to use (defaults to "") + */ + String inheritanceType() default ""; + + /** + * @return whether to generated a @MappedSuperclass type annotation instead + * of @Entity (defaults to false). + */ + boolean mappedSuperclass() default false; + + /** + * @return the name of the "merge" method to generate (defaults to + * {@value #MERGE_METHOD_DEFAULT}; mandatory) + */ + String mergeMethod() default MERGE_METHOD_DEFAULT; + + /** + * @return the name of the persistence unit defined in the persistence.xml + * file (optional) + */ + String persistenceUnit() default ""; + + /** + * @return the name of the "persist" method to generate (defaults to + * {@value #PERSIST_METHOD_DEFAULT}; mandatory) + */ + String persistMethod() default PERSIST_METHOD_DEFAULT; + + /** + * @return the name of the "remove" method to generate (defaults to + * {@value #REMOVE_METHOD_DEFAULT}; mandatory) + */ + String removeMethod() default REMOVE_METHOD_DEFAULT; + + /** + * Specifies the database schema name that should be used for the entity. + * + * @return the name of the schema to use (defaults to "") + */ + String schema() default ""; + + /** + * Specifies the name of the sequence to use for incrementing + * sequence-driven primary keys. + * + * @return the name of the sequence + */ + String sequenceName() default ""; + + /** + * Specifies the table name that should be used for the entity. + * + * @return the name of the table to use (defaults to "") + */ + String table() default ""; + + /** + * @return the name of the transaction manager (optional) + */ + String transactionManager() default ""; + + /** + * Specifies the column name that should be used for the version field. By + * default this is generally made identical to the {@link #versionField()}, + * although it will be made unique as required for the particular entity + * fields present. + * + * @return the name of the version column to use (defaults to + * {@value #VERSION_COLUMN_DEFAULT}; in this case it is automatic) + */ + String versionColumn() default VERSION_COLUMN_DEFAULT; + + /** + * Creates an optimistic locking version field, unless there is already a + * JPA @Version field annotation on a superclass (either written in normal + * Java source or introduced by a superclass that uses the + * {@link RooJpaActiveRecord} or {@link RooJpaEntity} annotation. The + * produced field will be of the type specified by {@link #versionType()}. + *

    + * If you annotate a field with JPA's @Version annotation, it is required + * that you provide a public accessor for that field. + * + * @return the name of the version field to use (defaults to + * {@value #VERSION_FIELD_DEFAULT}; must be provided) + */ + String versionField() default VERSION_FIELD_DEFAULT; + + /** + * @return the class of version that should be used (defaults to + * {@link Integer}; must be provided) + */ + Class versionType() default Integer.class; +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityAnnotationValues.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityAnnotationValues.java new file mode 100644 index 000000000..96cac183f --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityAnnotationValues.java @@ -0,0 +1,102 @@ +package org.springframework.roo.addon.jpa.entity; + +import static org.springframework.roo.addon.jpa.entity.RooJpaEntity.VERSION_FIELD_DEFAULT; + +import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; +import org.springframework.roo.model.JavaType; + +/** + * The purely JPA-related values of a single {@link RooJpaEntity} or + * {@link RooJpaActiveRecord} annotation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JpaEntityAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String catalog = ""; + @AutoPopulate private String entityName = ""; + @AutoPopulate private String identifierColumn = ""; + @AutoPopulate private String identifierField = ""; + @AutoPopulate private JavaType identifierType; + @AutoPopulate private String inheritanceType = ""; + @AutoPopulate private boolean mappedSuperclass; + @AutoPopulate private String schema = ""; + @AutoPopulate private String sequenceName = null; + @AutoPopulate private String table = ""; + @AutoPopulate private String versionColumn = ""; + @AutoPopulate private String versionField = VERSION_FIELD_DEFAULT; + @AutoPopulate private JavaType versionType = JavaType.INT_OBJECT; + + /** + * Constructor for reading the values of the given annotation + * + * @param annotatedType the type from which to read the values (required) + * @param triggerAnnotation the type of annotation from which to read the + * values (required) + * @since 1.2.0 + */ + public JpaEntityAnnotationValues( + final MemberHoldingTypeDetailsMetadataItem annotatedType, + final JavaType annotationType) { + super(annotatedType, annotationType); + // TODO move to superclass for this and all sibling classes? + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getCatalog() { + return catalog; + } + + public String getEntityName() { + return entityName; + } + + public String getIdentifierColumn() { + return identifierColumn; + } + + public String getIdentifierField() { + return identifierField; + } + + public JavaType getIdentifierType() { + return identifierType; + } + + public String getInheritanceType() { + return inheritanceType; + } + + public String getSchema() { + return schema; + } + + public String getSequenceName() { + return sequenceName; + } + + public String getTable() { + return table; + } + + public String getVersionColumn() { + return versionColumn; + } + + public String getVersionField() { + return versionField; + } + + public JavaType getVersionType() { + return versionType; + } + + public boolean isMappedSuperclass() { + return mappedSuperclass; + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadata.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadata.java new file mode 100644 index 000000000..b0649e7d0 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadata.java @@ -0,0 +1,786 @@ +package org.springframework.roo.addon.jpa.entity; + +import static org.springframework.roo.model.GoogleJavaType.DATANUCLEUS_JPA_EXTENSION; +import static org.springframework.roo.model.GoogleJavaType.GAE_DATASTORE_KEY; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JpaJavaType.COLUMN; +import static org.springframework.roo.model.JpaJavaType.DISCRIMINATOR_COLUMN; +import static org.springframework.roo.model.JpaJavaType.EMBEDDED_ID; +import static org.springframework.roo.model.JpaJavaType.ENTITY; +import static org.springframework.roo.model.JpaJavaType.GENERATED_VALUE; +import static org.springframework.roo.model.JpaJavaType.GENERATION_TYPE; +import static org.springframework.roo.model.JpaJavaType.ID; +import static org.springframework.roo.model.JpaJavaType.INHERITANCE; +import static org.springframework.roo.model.JpaJavaType.INHERITANCE_TYPE; +import static org.springframework.roo.model.JpaJavaType.MAPPED_SUPERCLASS; +import static org.springframework.roo.model.JpaJavaType.SEQUENCE_GENERATOR; +import static org.springframework.roo.model.JpaJavaType.TABLE; +import static org.springframework.roo.model.JpaJavaType.VERSION; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord; +import org.springframework.roo.addon.jpa.identifier.Identifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.operations.InheritanceType; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * The metadata for a JPA entity's *_Roo_Jpa_Entity.aj ITD. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JpaEntityMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private final JpaEntityAnnotationValues annotationValues; + private final MemberDetails entityMemberDetails; + private final Identifier identifier; + private final boolean isDatabaseDotComEnabled; + private final boolean isGaeEnabled; + private final JpaEntityMetadata parent; + private FieldMetadata identifierField; + private FieldMetadata versionField; + + /** + * Constructor + * + * @param metadataIdentificationString the JPA_ID of this + * {@link MetadataItem} + * @param itdName the ITD's {@link JavaType} (required) + * @param entityPhysicalType the entity's physical type (required) + * @param parent can be null if none of the governor's + * ancestors provide {@link JpaEntityMetadata} + * @param entityMemberDetails details of the entity's members (required) + * @param identifier information about the entity's identifier field in the + * event that the annotation doesn't provide such information; + * can be null + * @param annotationValues the effective annotation values taking into + * account the presence of a {@link RooJpaActiveRecord} and/or + * {@link RooJpaEntity} annotation (required) + */ + public JpaEntityMetadata(final String metadataIdentificationString, + final JavaType itdName, + final PhysicalTypeMetadata entityPhysicalType, + final JpaEntityMetadata parent, + final MemberDetails entityMemberDetails, + final Identifier identifier, + final JpaEntityAnnotationValues annotationValues, + final boolean isGaeEnabled, final boolean isDatabaseDotComEnabled) { + super(metadataIdentificationString, itdName, entityPhysicalType); + Validate.notNull(annotationValues, "Annotation values are required"); + Validate.notNull(entityMemberDetails, + "Entity MemberDetails are required"); + + /* + * Ideally we'd pass these parameters to the methods below rather than + * storing them in fields, but this isn't an option due to various calls + * to the parent entity. + */ + this.annotationValues = annotationValues; + this.entityMemberDetails = entityMemberDetails; + this.identifier = identifier; + this.parent = parent; + this.isGaeEnabled = isGaeEnabled; + this.isDatabaseDotComEnabled = isDatabaseDotComEnabled; + + // Add @Entity or @MappedSuperclass annotation + builder.addAnnotation(annotationValues.isMappedSuperclass() ? getTypeAnnotation(MAPPED_SUPERCLASS) + : getEntityAnnotation()); + + // Add @Table annotation if required + builder.addAnnotation(getTableAnnotation()); + + // Add @Inheritance annotation if required + builder.addAnnotation(getInheritanceAnnotation()); + + // Add @DiscriminatorColumn if required + builder.addAnnotation(getDiscriminatorColumnAnnotation()); + + // Ensure there's a no-arg constructor (explicit or default) + builder.addConstructor(getNoArgConstructor()); + + // Add identifier field and accessor + identifierField = getIdentifierField(); + builder.addField(identifierField); + builder.addMethod(getIdentifierAccessor()); + builder.addMethod(getIdentifierMutator()); + + // Add version field and accessor + versionField = getVersionField(); + builder.addField(versionField); + builder.addMethod(getVersionAccessor()); + builder.addMethod(getVersionMutator()); + + // Build the ITD based on what we added to the builder above + itdTypeDetails = builder.build(); + } + + private AnnotationMetadata getDiscriminatorColumnAnnotation() { + if (StringUtils.isNotBlank(annotationValues.getInheritanceType()) + && InheritanceType.SINGLE_TABLE.name().equals( + annotationValues.getInheritanceType())) { + // Theoretically not required based on @DiscriminatorColumn + // JavaDocs, but Hibernate appears to fail if it's missing + return getTypeAnnotation(DISCRIMINATOR_COLUMN); + } + return null; + } + + /** + * Generates the JPA @Entity annotation to be applied to the entity + * + * @return + */ + private AnnotationMetadata getEntityAnnotation() { + AnnotationMetadata entityAnnotation = getTypeAnnotation(ENTITY); + if (entityAnnotation == null) { + return null; + } + + if (StringUtils.isNotBlank(annotationValues.getEntityName())) { + final AnnotationMetadataBuilder entityBuilder = new AnnotationMetadataBuilder( + entityAnnotation); + entityBuilder.addStringAttribute("name", + annotationValues.getEntityName()); + entityAnnotation = entityBuilder.build(); + } + + return entityAnnotation; + } + + /** + * Locates the identifier accessor method. + *

    + * If {@link #getIdentifierField()} returns a field created by this ITD or + * if the field is declared within the entity itself, a public accessor will + * automatically be produced in the declaring class. + * + * @return the accessor (never returns null) + */ + private MethodMetadataBuilder getIdentifierAccessor() { + if (parent != null) { + return parent.getIdentifierAccessor(); + } + + // Locate the identifier field, and compute the name of the accessor + // that will be produced + JavaSymbolName requiredAccessorName = BeanInfoUtils + .getAccessorMethodName(identifierField); + + // See if the user provided the field + if (!getId().equals(identifierField.getDeclaredByMetadataId())) { + // Locate an existing accessor + final MethodMetadata method = entityMemberDetails.getMethod( + requiredAccessorName, new ArrayList()); + if (method != null) { + if (Modifier.isPublic(method.getModifier())) { + // Method exists and is public so return it + return new MethodMetadataBuilder(method); + } + + // Method is not public so make the required accessor name + // unique + requiredAccessorName = new JavaSymbolName( + requiredAccessorName.getSymbolName() + "_"); + } + } + + // We declared the field in this ITD, so produce a public accessor for + // it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return this." + + identifierField.getFieldName().getSymbolName() + ";"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + requiredAccessorName, identifierField.getFieldType(), + bodyBuilder); + } + + private String getIdentifierColumn() { + if (StringUtils.isNotBlank(annotationValues.getIdentifierColumn())) { + return annotationValues.getIdentifierColumn(); + } + else if (identifier != null + && StringUtils.isNotBlank(identifier.getColumnName())) { + return identifier.getColumnName(); + } + return ""; + } + + /** + * Locates the identifier field. + *

    + * If a parent is defined, it must provide the field. + *

    + * If no parent is defined, one will be located or created. Any declared or + * inherited field which has the {@link javax.persistence.Id @Id} or + * {@link javax.persistence.EmbeddedId @EmbeddedId} annotation will be taken + * as the identifier and returned. If no such field is located, a private + * field will be created as per the details contained in the + * {@link RooJpaActiveRecord} or {@link RooJpaEntity} annotation, as + * applicable. + * + * @param parent (can be null) + * @param project the user's project (required) + * @param annotationValues + * @param identifier can be null + * @return the identifier (never returns null) + */ + private FieldMetadata getIdentifierField() { + if (parent != null) { + final FieldMetadata idField = parent.getIdentifierField(); + if (idField != null) { + if (MemberFindingUtils.getAnnotationOfType( + idField.getAnnotations(), ID) != null) { + return idField; + } + else if (MemberFindingUtils.getAnnotationOfType( + idField.getAnnotations(), EMBEDDED_ID) != null) { + return idField; + } + } + return parent.getIdentifierField(); + } + + // Try to locate an existing field with @javax.persistence.Id + final List idFields = governorTypeDetails + .getFieldsWithAnnotation(ID); + if (!idFields.isEmpty()) { + return getIdentifierField(idFields, ID); + } + + // Try to locate an existing field with @javax.persistence.EmbeddedId + final List embeddedIdFields = governorTypeDetails + .getFieldsWithAnnotation(EMBEDDED_ID); + if (!embeddedIdFields.isEmpty()) { + return getIdentifierField(embeddedIdFields, EMBEDDED_ID); + } + + // Ensure there isn't already a field called "id"; if so, compute a + // unique name (it's not really a fatal situation at the end of the day) + final JavaSymbolName idField = governorTypeDetails + .getUniqueFieldName(getIdentifierFieldName()); + + // We need to create one + final JavaType identifierType = getIdentifierType(); + + final List annotations = new ArrayList(); + final boolean hasIdClass = !(identifierType.isCoreType() || identifierType + .equals(GAE_DATASTORE_KEY)); + final JavaType annotationType = hasIdClass ? EMBEDDED_ID : ID; + annotations.add(new AnnotationMetadataBuilder(annotationType)); + + // Encode keys as strings on GAE to support entity group hierarchies + if (isGaeEnabled && identifierType.equals(JavaType.STRING)) { + AnnotationMetadataBuilder extensionBuilder = new AnnotationMetadataBuilder( + DATANUCLEUS_JPA_EXTENSION); + extensionBuilder.addStringAttribute("vendorName", "datanucleus"); + extensionBuilder.addStringAttribute("key", "gae.encoded-pk"); + extensionBuilder.addStringAttribute("value", "true"); + annotations.add(extensionBuilder); + } + + // Compute the column name, as required + if (!hasIdClass) { + if (!"".equals(annotationValues.getSequenceName())) { + String generationType = isGaeEnabled || isDatabaseDotComEnabled ? "IDENTITY" + : "AUTO"; + + // ROO-746: Use @GeneratedValue(strategy = GenerationType.TABLE) + // If the root of the governor declares @Inheritance(strategy = + // InheritanceType.TABLE_PER_CLASS) + if ("AUTO".equals(generationType)) { + AnnotationMetadata inheritance = governorTypeDetails + .getAnnotation(INHERITANCE); + if (inheritance == null) { + inheritance = getInheritanceAnnotation(); + } + if (inheritance != null) { + final AnnotationAttributeValue value = inheritance + .getAttribute(new JavaSymbolName("strategy")); + if (value instanceof EnumAttributeValue) { + final EnumAttributeValue enumAttributeValue = (EnumAttributeValue) value; + final EnumDetails details = enumAttributeValue + .getValue(); + if (details != null + && details.getType().equals( + INHERITANCE_TYPE)) { + if ("TABLE_PER_CLASS".equals(details.getField() + .getSymbolName())) { + generationType = "TABLE"; + } + } + } + } + } + + final AnnotationMetadataBuilder generatedValueBuilder = new AnnotationMetadataBuilder( + GENERATED_VALUE); + generatedValueBuilder.addEnumAttribute("strategy", + new EnumDetails(GENERATION_TYPE, new JavaSymbolName( + generationType))); + + if (StringUtils.isNotBlank(annotationValues.getSequenceName()) + && !(isGaeEnabled || isDatabaseDotComEnabled)) { + final String sequenceKey = StringUtils + .uncapitalize(destination.getSimpleTypeName()) + + "Gen"; + generatedValueBuilder.addStringAttribute("generator", + sequenceKey); + final AnnotationMetadataBuilder sequenceGeneratorBuilder = new AnnotationMetadataBuilder( + SEQUENCE_GENERATOR); + sequenceGeneratorBuilder.addStringAttribute("name", + sequenceKey); + sequenceGeneratorBuilder.addStringAttribute("sequenceName", + annotationValues.getSequenceName()); + annotations.add(sequenceGeneratorBuilder); + } + annotations.add(generatedValueBuilder); + } + + final String identifierColumn = StringUtils + .stripToEmpty(getIdentifierColumn()); + String columnName = idField.getSymbolName(); + if (StringUtils.isNotBlank(identifierColumn)) { + // User has specified an alternate column name + columnName = identifierColumn; + } + + final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( + COLUMN); + columnBuilder.addStringAttribute("name", columnName); + if (identifier != null + && StringUtils.isNotBlank(identifier.getColumnDefinition())) { + columnBuilder.addStringAttribute("columnDefinition", + identifier.getColumnDefinition()); + } + + // Add length attribute for String field + if (identifier != null && identifier.getColumnSize() > 0 + && identifier.getColumnSize() < 4000 + && identifierType.equals(JavaType.STRING)) { + columnBuilder.addIntegerAttribute("length", + identifier.getColumnSize()); + } + + // Add precision and scale attributes for numeric field + if (identifier != null + && identifier.getScale() > 0 + && (identifierType.equals(JavaType.DOUBLE_OBJECT) + || identifierType.equals(JavaType.DOUBLE_PRIMITIVE) || identifierType + .equals(BIG_DECIMAL))) { + columnBuilder.addIntegerAttribute("precision", + identifier.getColumnSize()); + columnBuilder.addIntegerAttribute("scale", + identifier.getScale()); + } + + annotations.add(columnBuilder); + } + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + idField, identifierType).build(); + } + + private FieldMetadata getIdentifierField( + final List identifierFields, + final JavaType identifierType) { + Validate.isTrue(identifierFields.size() == 1, + "More than one field was annotated with @%s in '%s'", + identifierType.getSimpleTypeName(), + destination.getFullyQualifiedTypeName()); + return new FieldMetadataBuilder(identifierFields.get(0)).build(); + } + + private String getIdentifierFieldName() { + if (StringUtils.isNotBlank(annotationValues.getIdentifierField())) { + return annotationValues.getIdentifierField(); + } + else if (identifier != null && identifier.getFieldName() != null) { + return identifier.getFieldName().getSymbolName(); + } + // Use the default + return RooJpaEntity.ID_FIELD_DEFAULT; + } + + /** + * Locates the identifier mutator method. + *

    + * If {@link #getIdentifierField()} returns a field created by this ITD or + * if the field is declared within the entity itself, a public mutator will + * automatically be produced in the declaring class. + * + * @return the mutator (never returns null) + */ + private MethodMetadataBuilder getIdentifierMutator() { + // TODO: This is a temporary workaround to support web data binding + // approaches; to be reviewed more thoroughly in future + if (parent != null) { + return parent.getIdentifierMutator(); + } + + // Locate the identifier field, and compute the name of the accessor + // that will be produced + JavaSymbolName requiredMutatorName = BeanInfoUtils + .getMutatorMethodName(identifierField); + + final List parameterTypes = Arrays.asList(identifierField + .getFieldType()); + final List parameterNames = Arrays + .asList(new JavaSymbolName("id")); + + // See if the user provided the field + if (!getId().equals(identifierField.getDeclaredByMetadataId())) { + // Locate an existing mutator + final MethodMetadata method = entityMemberDetails.getMethod( + requiredMutatorName, parameterTypes); + if (method != null) { + if (Modifier.isPublic(method.getModifier())) { + // Method exists and is public so return it + return new MethodMetadataBuilder(method); + } + + // Method is not public so make the required mutator name unique + requiredMutatorName = new JavaSymbolName( + requiredMutatorName.getSymbolName() + "_"); + } + } + + // We declared the field in this ITD, so produce a public mutator for it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("this." + + identifierField.getFieldName().getSymbolName() + " = id;"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + requiredMutatorName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + /** + * Returns the {@link JavaType} of the identifier field + * + * @param annotationValues the values of the {@link RooJpaEntity} annotation + * (required) + * @param identifier can be null + * @return a non-null type + */ + private JavaType getIdentifierType() { + if (isDatabaseDotComEnabled) { + return JavaType.STRING; + } + if (annotationValues.getIdentifierType() != null) { + return annotationValues.getIdentifierType(); + } + else if (identifier != null && identifier.getFieldType() != null) { + return identifier.getFieldType(); + } + // Use the default + return LONG_OBJECT; + } + + /** + * Returns the JPA @Inheritance annotation to be applied to the entity, if + * applicable + * + * @param annotationValues the values of the {@link RooJpaEntity} annotation + * (required) + * @return null if it's already present or not required + */ + private AnnotationMetadata getInheritanceAnnotation() { + if (governorTypeDetails.getAnnotation(INHERITANCE) != null) { + return null; + } + if (StringUtils.isNotBlank(annotationValues.getInheritanceType())) { + final AnnotationMetadataBuilder inheritanceBuilder = new AnnotationMetadataBuilder( + INHERITANCE); + inheritanceBuilder.addEnumAttribute("strategy", + new EnumDetails(INHERITANCE_TYPE, new JavaSymbolName( + annotationValues.getInheritanceType()))); + return inheritanceBuilder.build(); + } + return null; + } + + /** + * Locates the no-arg constructor for this class, if available. + *

    + * If a class defines a no-arg constructor, it is returned (irrespective of + * access modifiers). + *

    + * Otherwise, and if there is at least one other constructor declared in the + * source file, this method creates one with public access. + * + * @return null if no constructor is to be produced + */ + private ConstructorMetadataBuilder getNoArgConstructor() { + // Search for an existing constructor + final ConstructorMetadata existingExplicitConstructor = governorTypeDetails + .getDeclaredConstructor(null); + if (existingExplicitConstructor != null) { + // Found an existing no-arg constructor on this class, so return it + return new ConstructorMetadataBuilder(existingExplicitConstructor); + } + + // To get this far, the user did not define a no-arg constructor + if (governorTypeDetails.getDeclaredConstructors().isEmpty()) { + // Java creates the default constructor => no need to add one + return null; + } + + // Create the constructor + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("super();"); + + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + getId()); + constructorBuilder.setBodyBuilder(bodyBuilder); + constructorBuilder.setModifier(Modifier.PUBLIC); + return constructorBuilder; + } + + /** + * Generates the JPA @Table annotation to be applied to the entity + * + * @param annotationValues + * @return + */ + private AnnotationMetadata getTableAnnotation() { + final AnnotationMetadata tableAnnotation = getTypeAnnotation(TABLE); + if (tableAnnotation == null) { + return null; + } + final String catalog = annotationValues.getCatalog(); + final String schema = annotationValues.getSchema(); + final String table = annotationValues.getTable(); + if (StringUtils.isNotBlank(table) || StringUtils.isNotBlank(schema) + || StringUtils.isNotBlank(catalog)) { + final AnnotationMetadataBuilder tableBuilder = new AnnotationMetadataBuilder( + tableAnnotation); + if (StringUtils.isNotBlank(catalog)) { + tableBuilder.addStringAttribute("catalog", catalog); + } + if (StringUtils.isNotBlank(schema)) { + tableBuilder.addStringAttribute("schema", schema); + } + if (StringUtils.isNotBlank(table)) { + tableBuilder.addStringAttribute("name", table); + } + return tableBuilder.build(); + } + return null; + } + + /** + * Locates the version accessor method. + *

    + * If {@link #getVersionField()} returns a field created by this ITD or if + * the version field is declared within the entity itself, a public accessor + * will automatically be produced in the declaring class. + * + * @param memberDetails + * @return the version accessor (may return null if there is no version + * field declared in this class) + */ + private MethodMetadataBuilder getVersionAccessor() { + if (versionField == null) { + // There's no version field, so there certainly won't be an accessor + // for it + return null; + } + + if (parent != null) { + final FieldMetadata result = parent.getVersionField(); + if (result != null) { + // It's the parent's responsibility to provide the accessor, not + // ours + return parent.getVersionAccessor(); + } + } + + // Compute the name of the accessor that will be produced + JavaSymbolName requiredAccessorName = BeanInfoUtils + .getAccessorMethodName(versionField); + + // See if the user provided the field + if (!getId().equals(versionField.getDeclaredByMetadataId())) { + // Locate an existing accessor + final MethodMetadata method = entityMemberDetails.getMethod( + requiredAccessorName, new ArrayList(), getId()); + if (method != null) { + if (Modifier.isPublic(method.getModifier())) { + // Method exists and is public so return it + return new MethodMetadataBuilder(method); + } + + // Method is not public so make the required accessor name + // unique + requiredAccessorName = new JavaSymbolName( + requiredAccessorName.getSymbolName() + "_"); + } + } + + // We declared the field in this ITD, so produce a public accessor for + // it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return this." + + versionField.getFieldName().getSymbolName() + ";"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + requiredAccessorName, versionField.getFieldType(), bodyBuilder); + } + + /** + * Locates the version field. + *

    + * If a parent is defined, it may provide the field. + *

    + * If no parent is defined, one may be located or created. Any declared or + * inherited field which is annotated with javax.persistence.Version will be + * taken as the version and returned. If no such field is located, a private + * field may be created as per the details contained in + * {@link RooJpaActiveRecord} or {@link RooJpaEntity} annotation, as + * applicable. + * + * @return the version field (may be null) + */ + private FieldMetadata getVersionField() { + if (parent != null) { + final FieldMetadata result = parent.getVersionField(); + if (result != null) { + return result; + } + } + + // Try to locate an existing field with @Version + final List versionFields = governorTypeDetails + .getFieldsWithAnnotation(VERSION); + if (!versionFields.isEmpty()) { + Validate.isTrue(versionFields.size() == 1, + "More than 1 field was annotated with @Version in '%s'", + destination.getFullyQualifiedTypeName()); + return versionFields.get(0); + } + + // Quit at this stage if the user doesn't want a version field + final String versionField = annotationValues.getVersionField(); + if ("".equals(versionField)) { + return null; + } + + // Ensure there isn't already a field called "version"; if so, compute a + // unique name (it's not really a fatal situation at the end of the day) + final JavaSymbolName verField = governorTypeDetails + .getUniqueFieldName(versionField); + + // We're creating one + JavaType versionType = annotationValues.getVersionType(); + String versionColumn = StringUtils.defaultIfEmpty( + annotationValues.getVersionColumn(), verField.getSymbolName()); + if (isDatabaseDotComEnabled) { + versionType = CALENDAR; + versionColumn = "lastModifiedDate"; + } + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(VERSION)); + + final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( + COLUMN); + columnBuilder.addStringAttribute("name", versionColumn); + annotations.add(columnBuilder); + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, + verField, versionType).build(); + } + + /** + * Locates the version mutator method. + *

    + * If {@link #getVersionField()} returns a field created by this ITD or if + * the version field is declared within the entity itself, a public mutator + * will automatically be produced in the declaring class. + * + * @return the mutator (may return null if there is no version field + * declared in this class) + */ + private MethodMetadataBuilder getVersionMutator() { + // TODO: This is a temporary workaround to support web data binding + // approaches; to be reviewed more thoroughly in future + if (parent != null) { + return parent.getVersionMutator(); + } + + // Locate the version field, and compute the name of the mutator that + // will be produced + if (versionField == null) { + // There's no version field, so there certainly won't be a mutator + // for it + return null; + } + + // Compute the name of the mutator that will be produced + JavaSymbolName requiredMutatorName = BeanInfoUtils + .getMutatorMethodName(versionField); + + final List parameterTypes = Arrays.asList(versionField + .getFieldType()); + final List parameterNames = Arrays + .asList(new JavaSymbolName("version")); + + // See if the user provided the field + if (!getId().equals(versionField.getDeclaredByMetadataId())) { + // Locate an existing mutator + final MethodMetadata method = entityMemberDetails.getMethod( + requiredMutatorName, parameterTypes, getId()); + if (method != null) { + if (Modifier.isPublic(method.getModifier())) { + // Method exists and is public so return it + return new MethodMetadataBuilder(method); + } + + // Method is not public so make the required mutator name unique + requiredMutatorName = new JavaSymbolName( + requiredMutatorName.getSymbolName() + "_"); + } + } + + // We declared the field in this ITD, so produce a public mutator for it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("this." + + versionField.getFieldName().getSymbolName() + " = version;"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + requiredMutatorName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } +} \ No newline at end of file diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadataProvider.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadataProvider.java new file mode 100644 index 000000000..ecc27abc4 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadataProvider.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.jpa.entity; + +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides metadata relating to JPA entities. Has taken over from + * {@link JpaActiveRecordMetadataProvider} the management of core JPA concerns + * such as the id and version fields and applying the JPA @Entity and @Table + * annotations. The {@link JpaActiveRecordMetadataProvider} remains responsible + * for CRUD methods such as merge(), persist(), finders, etc. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface JpaEntityMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadataProviderImpl.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadataProviderImpl.java new file mode 100644 index 000000000..b3a47eb43 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/JpaEntityMetadataProviderImpl.java @@ -0,0 +1,349 @@ +package org.springframework.roo.addon.jpa.entity; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COLUMN_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_ID_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ENUMERATED_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_MUTATOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_TYPE; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.LOB_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSISTENT_TYPE; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.TRANSIENT_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.VERSION_ACCESSOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.VERSION_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.VERSION_MUTATOR_METHOD; +import static org.springframework.roo.model.JpaJavaType.COLUMN; +import static org.springframework.roo.model.JpaJavaType.EMBEDDED; +import static org.springframework.roo.model.JpaJavaType.EMBEDDED_ID; +import static org.springframework.roo.model.JpaJavaType.ENUMERATED; +import static org.springframework.roo.model.JpaJavaType.ID; +import static org.springframework.roo.model.JpaJavaType.LOB; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.TRANSIENT; +import static org.springframework.roo.model.JpaJavaType.VERSION; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; + +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jpa.AbstractIdentifierServiceAwareMetadataProvider; +import org.springframework.roo.addon.jpa.identifier.Identifier; +import org.springframework.roo.addon.jpa.identifier.IdentifierMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.taggers.AnnotatedTypeMatcher; +import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator; +import org.springframework.roo.classpath.customdata.taggers.FieldMatcher; +import org.springframework.roo.classpath.customdata.taggers.MethodMatcher; +import org.springframework.roo.classpath.customdata.taggers.MidTypeMatcher; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.CollectionUtils; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * The {@link JpaEntityMetadataProvider} implementation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class JpaEntityMetadataProviderImpl extends + AbstractIdentifierServiceAwareMetadataProvider implements + JpaEntityMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JpaEntityMetadataProviderImpl.class); + + // JPA-related field matchers + private static final FieldMatcher JPA_COLUMN_FIELD_MATCHER = new FieldMatcher( + COLUMN_FIELD, AnnotationMetadataBuilder.getInstance(COLUMN)); + private static final FieldMatcher JPA_EMBEDDED_FIELD_MATCHER = new FieldMatcher( + EMBEDDED_FIELD, AnnotationMetadataBuilder.getInstance(EMBEDDED)); + private static final FieldMatcher JPA_EMBEDDED_ID_FIELD_MATCHER = new FieldMatcher( + EMBEDDED_ID_FIELD, + AnnotationMetadataBuilder.getInstance(EMBEDDED_ID)); + private static final FieldMatcher JPA_ENUMERATED_FIELD_MATCHER = new FieldMatcher( + ENUMERATED_FIELD, AnnotationMetadataBuilder.getInstance(ENUMERATED)); + private static final FieldMatcher JPA_ID_AND_EMBEDDED_ID_FIELD_MATCHER = new FieldMatcher( + IDENTIFIER_FIELD, AnnotationMetadataBuilder.getInstance(ID), + AnnotationMetadataBuilder.getInstance(EMBEDDED_ID)); + private static final FieldMatcher JPA_ID_FIELD_MATCHER = new FieldMatcher( + IDENTIFIER_FIELD, AnnotationMetadataBuilder.getInstance(ID)); + private static final FieldMatcher JPA_LOB_FIELD_MATCHER = new FieldMatcher( + LOB_FIELD, AnnotationMetadataBuilder.getInstance(LOB)); + private static final FieldMatcher JPA_MANY_TO_MANY_FIELD_MATCHER = new FieldMatcher( + MANY_TO_MANY_FIELD, + AnnotationMetadataBuilder.getInstance(MANY_TO_MANY)); + private static final FieldMatcher JPA_MANY_TO_ONE_FIELD_MATCHER = new FieldMatcher( + MANY_TO_ONE_FIELD, + AnnotationMetadataBuilder.getInstance(MANY_TO_ONE)); + private static final FieldMatcher JPA_ONE_TO_MANY_FIELD_MATCHER = new FieldMatcher( + ONE_TO_MANY_FIELD, + AnnotationMetadataBuilder.getInstance(ONE_TO_MANY)); + private static final FieldMatcher JPA_ONE_TO_ONE_FIELD_MATCHER = new FieldMatcher( + ONE_TO_ONE_FIELD, AnnotationMetadataBuilder.getInstance(ONE_TO_ONE)); + private static final FieldMatcher JPA_TRANSIENT_FIELD_MATCHER = new FieldMatcher( + TRANSIENT_FIELD, AnnotationMetadataBuilder.getInstance(TRANSIENT)); + private static final FieldMatcher JPA_VERSION_FIELD_MATCHER = new FieldMatcher( + VERSION_FIELD, AnnotationMetadataBuilder.getInstance(VERSION)); + private static final String PROVIDES_TYPE_STRING = JpaEntityMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + // The order of this array is the order in which we look for annotations. We + // use the values of the first one found. + private static final JavaType[] TRIGGER_ANNOTATIONS = { + // We trigger off RooJpaEntity in case the user doesn't want Active + // Record methods + ROO_JPA_ENTITY, + // We trigger off RooJpaActiveRecord so that existing projects don't + // need to add RooJpaEntity + ROO_JPA_ACTIVE_RECORD, }; + + private CustomDataKeyDecorator customDataKeyDecorator; + private ProjectOperations projectOperations; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + PROVIDES_TYPE); + addMetadataTriggers(TRIGGER_ANNOTATIONS); + registerMatchers(); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + PROVIDES_TYPE); + removeMetadataTriggers(TRIGGER_ANNOTATIONS); + getCustomDataKeyDecorator().unregisterMatchers(getClass()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = getType(metadataIdentificationString); + final LogicalPath path = PhysicalTypeIdentifierNamingUtils.getPath( + PROVIDES_TYPE_STRING, metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + /** + * Returns the {@link Identifier} for the entity identified by the given + * metadata ID. + * + * @param metadataIdentificationString + * @return null if there isn't one + */ + private Identifier getIdentifier(final String metadataIdentificationString) { + final JavaType entity = getType(metadataIdentificationString); + final List identifiers = getIdentifiersForType(entity); + if (CollectionUtils.isEmpty(identifiers)) { + return null; + } + // We have potential identifier information from an IdentifierService. + // We only use this identifier information if the user did NOT provide + // ANY identifier-related attributes on @RooJpaEntity.... + Validate.isTrue( + identifiers.size() == 1, + "Identifier service indicates %d fields illegally for the entity '%s' (should only be one identifier field given this is an entity, not an Identifier class)", + identifiers.size(), entity.getSimpleTypeName()); + return identifiers.iterator().next(); + } + + public String getItdUniquenessFilenameSuffix() { + return "Jpa_Entity"; + } + + /** + * Returns the {@link JpaEntityAnnotationValues} for the given domain type + * + * @param governorPhysicalType (required) + * @return a non-null instance + */ + private JpaEntityAnnotationValues getJpaEntityAnnotationValues( + final PhysicalTypeMetadata governorPhysicalType) { + for (final JavaType triggerAnnotation : TRIGGER_ANNOTATIONS) { + final JpaEntityAnnotationValues annotationValues = new JpaEntityAnnotationValues( + governorPhysicalType, triggerAnnotation); + if (annotationValues.isAnnotationFound()) { + return annotationValues; + } + } + throw new IllegalStateException(getClass().getSimpleName() + + " was triggered but not by any of " + + Arrays.toString(TRIGGER_ANNOTATIONS)); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalType, + final String itdFilename) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + // Find out the entity-level JPA details from the trigger annotation + final JpaEntityAnnotationValues jpaEntityAnnotationValues = getJpaEntityAnnotationValues(governorPhysicalType); + + /* + * Walk the inheritance hierarchy for any existing JpaEntityMetadata. We + * don't need to monitor any such parent, as any changes to its Java + * type will trickle down to the governing java type. + */ + final JpaEntityMetadata parent = getParentMetadata(governorPhysicalType + .getMemberHoldingTypeDetails()); + + // Get the governor's members + final MemberDetails governorMemberDetails = getMemberDetails(governorPhysicalType); + + // Get the governor's ID field, if any + final Identifier identifier = getIdentifier(metadataIdentificationString); + + boolean isGaeEnabled = false; + boolean isDatabaseDotComEnabled = false; + final String moduleName = PhysicalTypeIdentifierNamingUtils.getPath( + metadataIdentificationString).getModule(); + if (projectOperations.isProjectAvailable(moduleName)) { + // If the project itself changes, we want a chance to refresh this + // item + getMetadataDependencyRegistry().registerDependency( + ProjectMetadata.getProjectIdentifier(moduleName), + metadataIdentificationString); + isGaeEnabled = projectOperations.isFeatureInstalledInModule( + FeatureNames.GAE, moduleName); + isDatabaseDotComEnabled = projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.DATABASE_DOT_COM); + } + + return new JpaEntityMetadata(metadataIdentificationString, aspectName, + governorPhysicalType, parent, governorMemberDetails, + identifier, jpaEntityAnnotationValues, isGaeEnabled, + isDatabaseDotComEnabled); + } + + public String getProvidesType() { + return PROVIDES_TYPE; + } + + private JavaType getType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + @SuppressWarnings("unchecked") + private void registerMatchers() { + + getCustomDataKeyDecorator().registerMatchers( + getClass(), + // Type matchers + new MidTypeMatcher(IDENTIFIER_TYPE, IdentifierMetadata.class + .getName()), + new AnnotatedTypeMatcher(PERSISTENT_TYPE, + RooJavaType.ROO_JPA_ACTIVE_RECORD, ROO_JPA_ENTITY), + // Field matchers + JPA_COLUMN_FIELD_MATCHER, JPA_EMBEDDED_FIELD_MATCHER, + JPA_EMBEDDED_ID_FIELD_MATCHER, + JPA_ENUMERATED_FIELD_MATCHER, + JPA_ID_FIELD_MATCHER, + JPA_LOB_FIELD_MATCHER, + JPA_MANY_TO_MANY_FIELD_MATCHER, + JPA_MANY_TO_ONE_FIELD_MATCHER, + JPA_ONE_TO_MANY_FIELD_MATCHER, + JPA_ONE_TO_ONE_FIELD_MATCHER, + JPA_TRANSIENT_FIELD_MATCHER, + JPA_VERSION_FIELD_MATCHER, + // Method matchers + new MethodMatcher(Arrays + .asList(JPA_ID_AND_EMBEDDED_ID_FIELD_MATCHER), + IDENTIFIER_ACCESSOR_METHOD, true), new MethodMatcher( + Arrays.asList(JPA_ID_AND_EMBEDDED_ID_FIELD_MATCHER), + IDENTIFIER_MUTATOR_METHOD, false), new MethodMatcher( + Arrays.asList(JPA_VERSION_FIELD_MATCHER), + VERSION_ACCESSOR_METHOD, true), new MethodMatcher( + Arrays.asList(JPA_VERSION_FIELD_MATCHER), + VERSION_MUTATOR_METHOD, false)); + } + + public CustomDataKeyDecorator getCustomDataKeyDecorator(){ + if(customDataKeyDecorator == null){ + // Get all Services implement CustomDataKeyDecorator interface + try { + ServiceReference[] references = context.getAllServiceReferences(CustomDataKeyDecorator.class.getName(), null); + + for(ServiceReference ref : references){ + return (CustomDataKeyDecorator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CustomDataKeyDecorator on JpaEntityMetadataProviderImpl."); + return null; + } + }else{ + return customDataKeyDecorator; + } + + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on JpaEntityMetadataProviderImpl."); + return null; + } + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/RooJpaEntity.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/RooJpaEntity.java new file mode 100644 index 000000000..0120fd0e6 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/entity/RooJpaEntity.java @@ -0,0 +1,139 @@ +package org.springframework.roo.addon.jpa.entity; + +import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord; + +/** + * Indicates a type that is a JPA entity. Created to reduce the number of + * concerns managed by {@link RooJpaActiveRecord}. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooJpaEntity { + + String ID_FIELD_DEFAULT = "id"; + String VERSION_COLUMN_DEFAULT = "version"; + String VERSION_FIELD_DEFAULT = "version"; + + /** + * Specifies the database catalog name that should be used for the entity. + * + * @return the name of the catalog to use (defaults to "") + */ + String catalog() default ""; + + /** + * Specifies the name used to refer to the entity in queries. + *

    + * The name must not be a reserved literal in JPQL. + * + * @return the name given to the entity (defaults to "") + */ + String entityName() default ""; + + /** + * Specifies the column name that should be used for the identifier field. + * By default this is generally made identical to the + * {@link #identifierField()}, although it will be made unique as required + * for the particular entity fields present. + * + * @return the name of the identifier column to use (defaults to ""; in this + * case it is automatic) + */ + String identifierColumn() default ""; + + /** + * Creates an identifier, unless there is already a JPA @Id field annotation + * in a superclass (either written in normal Java source ,or introduced by a + * superclass that is annotated with either {@link RooJpaActiveRecord} or + * {@link RooJpaEntity}. + *

    + * If you annotate a field with JPA's @Id annotation, it is required that + * you provide a public accessor for that field. + * + * @return the name of the identifier field to use (defaults to + * {@value #ID_FIELD_DEFAULT}; must be provided) + */ + String identifierField() default ID_FIELD_DEFAULT; + + /** + * @return the class of identifier that should be used (defaults to + * {@link Long}; must be provided) + */ + Class identifierType() default Long.class; + + /** + * Specifies the JPA inheritance type that should be used for the entity. + * + * @return the inheritance type to use (defaults to "") + */ + String inheritanceType() default ""; + + /** + * @return whether to generated a @MappedSuperclass type annotation instead + * of @Entity (defaults to false). + */ + boolean mappedSuperclass() default false; + + /** + * Specifies the database schema name that should be used for the entity. + * + * @return the name of the schema to use (defaults to "") + */ + String schema() default ""; + + /** + * Specifies the name of the sequence to use for incrementing + * sequence-driven primary keys. + * + * @return the name of the sequence (defaults to "") + */ + String sequenceName() default ""; + + /** + * Specifies the table name that should be used for the entity. + * + * @return the name of the table to use (defaults to "") + */ + String table() default ""; + + /** + * Specifies the column name that should be used for the version field. By + * default this is generally made identical to the {@link #versionField()}, + * although it will be made unique as required for the particular entity + * fields present. + * + * @return the name of the version column to use (defaults to + * {@value #VERSION_COLUMN_DEFAULT}; in this case it is automatic) + */ + String versionColumn() default VERSION_COLUMN_DEFAULT; + + /** + * Creates an optimistic locking version field, unless there is already a + * JPA @Version field annotation in a superclass (either written in normal + * Java source, or introduced by a superclass annotated with + * {@link RooJpaActiveRecord} or {@link RooJpaEntity}. The produced field + * will be of the type specified by {@link #versionType()}. + *

    + * If you annotate a field with JPA's @Version annotation, it is required + * that you provide a public accessor for that field. + * + * @return the name of the version field to use (defaults to + * {@value #VERSION_FIELD_DEFAULT}; must be provided) + */ + String versionField() default VERSION_FIELD_DEFAULT; + + /** + * @return the class of version that should be used (defaults to + * {@link Integer}; must be provided) + */ + Class versionType() default Integer.class; +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/Identifier.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/Identifier.java new file mode 100644 index 000000000..d4e66cd2c --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/Identifier.java @@ -0,0 +1,147 @@ +package org.springframework.roo.addon.jpa.identifier; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Represents an entity identifier. Instances are immutable. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Identifier { + + private final String columnDefinition; + private final String columnName; + private final int columnSize; + private final JavaSymbolName fieldName; + private final JavaType fieldType; + private final int scale; + + /** + * Constructor + * + * @param fieldName required + * @param fieldType required + * @param columnName required + * @param columnSize + * @param scale + * @param columnDefinition + */ + public Identifier(final JavaSymbolName fieldName, final JavaType fieldType, + final String columnName, final int columnSize, final int scale, + final String columnDefinition) { + Validate.notNull(fieldName, "Field name required"); + Validate.notNull(fieldType, "Field type required"); + Validate.notBlank(columnName, "Column name required"); + this.columnDefinition = columnDefinition; + this.columnName = columnName; + this.columnSize = columnSize; + this.fieldName = fieldName; + this.fieldType = fieldType; + this.scale = scale; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Identifier other = (Identifier) obj; + if (columnDefinition == null) { + if (other.columnDefinition != null) { + return false; + } + } + else if (!columnDefinition.equals(other.columnDefinition)) { + return false; + } + if (columnName == null) { + if (other.columnName != null) { + return false; + } + } + else if (!columnName.equals(other.columnName)) { + return false; + } + if (columnSize != other.columnSize) { + return false; + } + if (fieldName == null) { + if (other.fieldName != null) { + return false; + } + } + else if (!fieldName.equals(other.fieldName)) { + return false; + } + if (fieldType == null) { + if (other.fieldType != null) { + return false; + } + } + else if (!fieldType.equals(other.fieldType)) { + return false; + } + if (scale != other.scale) { + return false; + } + return true; + } + + public String getColumnDefinition() { + return columnDefinition; + } + + public String getColumnName() { + return columnName; + } + + public int getColumnSize() { + return columnSize; + } + + public JavaSymbolName getFieldName() { + return fieldName; + } + + public JavaType getFieldType() { + return fieldType; + } + + public int getScale() { + return scale; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (columnDefinition == null ? 0 : columnDefinition.hashCode()); + result = prime * result + + (columnName == null ? 0 : columnName.hashCode()); + result = prime * result + columnSize; + result = prime * result + + (fieldName == null ? 0 : fieldName.hashCode()); + result = prime * result + + (fieldType == null ? 0 : fieldType.hashCode()); + result = prime * result + scale; + return result; + } + + @Override + public String toString() { + return String + .format("Identifier [fieldName=%s, fieldType=%s, columnName=%s, columnSize=%s, scale=%s, columnDefinition=%s]", + fieldName, fieldType, columnName, columnSize, scale, + columnDefinition); + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierAnnotationValues.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierAnnotationValues.java new file mode 100644 index 000000000..4d36db154 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierAnnotationValues.java @@ -0,0 +1,68 @@ +package org.springframework.roo.addon.jpa.identifier; + +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; + +/** + * The values of a {@link RooIdentifier} annotation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class IdentifierAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private boolean dbManaged; + @AutoPopulate private boolean gettersByDefault = true; + @AutoPopulate private boolean noArgConstructor = true; + @AutoPopulate private boolean settersByDefault; + + /** + * Constructor that reads the {@link RooIdentifier} annotation on the given + * governor + * + * @param governor the governor's metadata (required) + */ + public IdentifierAnnotationValues( + final MemberHoldingTypeDetailsMetadataItem governor) { + super(governor, RooIdentifier.class); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Indicates whether the identifier class is managed by DBRE + * + * @return + */ + public boolean isDbManaged() { + return dbManaged; + } + + /** + * Indicates whether to generate getters for the id fields + * + * @return + */ + public boolean isGettersByDefault() { + return gettersByDefault; + } + + /** + * Indicates whether to generate a no-argument constructor for the class + * + * @return + */ + public boolean isNoArgConstructor() { + return noArgConstructor; + } + + /** + * Indicates whether to generate setters for the id fields + * + * @return + */ + public boolean isSettersByDefault() { + return settersByDefault; + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadata.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadata.java new file mode 100644 index 000000000..226b195e0 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadata.java @@ -0,0 +1,443 @@ +package org.springframework.roo.addon.jpa.identifier; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_TYPE; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JpaJavaType.COLUMN; +import static org.springframework.roo.model.JpaJavaType.EMBEDDABLE; +import static org.springframework.roo.model.JpaJavaType.TEMPORAL; +import static org.springframework.roo.model.JpaJavaType.TEMPORAL_TYPE; +import static org.springframework.roo.model.JpaJavaType.TRANSIENT; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooIdentifier}. + * + * @author Alan Stewart + * @since 1.1 + */ +public class IdentifierMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = IdentifierMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentifierType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + // See {@link IdentifierService} for further information (populated via + // {@link IdentifierMetadataProviderImpl}); may be null + private List identifierServiceResult; + + private boolean publicNoArgConstructor; + + public IdentifierMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final IdentifierAnnotationValues annotationValues, + final List identifierServiceResult) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), "Metadata identification string '" + + identifier + "' does not appear to be a valid"); + Validate.notNull(annotationValues, "Annotation values required"); + + if (!isValid()) { + return; + } + + this.identifierServiceResult = identifierServiceResult; + + // Add @Embeddable annotation + builder.addAnnotation(getEmbeddableAnnotation()); + + // Add declared fields and accessors and mutators + final List fields = getFieldBuilders(); + for (final FieldMetadataBuilder field : fields) { + builder.addField(field); + } + + // Obtain a parameterised constructor + builder.addConstructor(getParameterizedConstructor(fields)); + + // Obtain a no-arg constructor, if one is appropriate to provide + if (annotationValues.isNoArgConstructor()) { + builder.addConstructor(getNoArgConstructor()); + } + + if (annotationValues.isGettersByDefault()) { + for (final MethodMetadataBuilder accessor : getAccessors(fields)) { + builder.addMethod(accessor); + } + } + if (annotationValues.isSettersByDefault()) { + for (final MethodMetadataBuilder mutator : getMutators(fields)) { + builder.addMethod(mutator); + } + } + + // Add custom data tag for Roo Identifier type + builder.putCustomData(IDENTIFIER_TYPE, null); + + // Create a representation of the desired output ITD + buildItd(); + } + + /** + * Locates the accessor methods. + *

    + * If {@link #getFieldBuilders()} returns fields created by this ITD, public + * accessors will automatically be produced in the declaring class. + * + * @param fields + * @return the accessors (never returns null) + */ + private List getAccessors( + final List fields) { + final List accessors = new ArrayList(); + + // Compute the names of the accessors that will be produced + for (final FieldMetadataBuilder field : fields) { + final JavaSymbolName requiredAccessorName = BeanInfoUtils + .getAccessorMethodName(field.getFieldName(), + field.getFieldType()); + final MethodMetadata accessor = getGovernorMethod(requiredAccessorName); + if (accessor == null) { + accessors.add(getAccessorMethod(field.getFieldName(), + field.getFieldType())); + } + else { + Validate.isTrue( + Modifier.isPublic(accessor.getModifier()), + "User provided field but failed to provide a public '%s()' method in '%s'", + requiredAccessorName.getSymbolName(), + destination.getFullyQualifiedTypeName()); + accessors.add(new MethodMetadataBuilder(accessor)); + } + } + return accessors; + } + + private AnnotationMetadataBuilder getColumnBuilder( + final Identifier identifier) { + final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( + COLUMN); + columnBuilder.addStringAttribute("name", identifier.getColumnName()); + if (StringUtils.isNotBlank(identifier.getColumnDefinition())) { + columnBuilder.addStringAttribute("columnDefinition", + identifier.getColumnDefinition()); + } + columnBuilder.addBooleanAttribute("nullable", false); + + // Add length attribute for Strings + if (identifier.getColumnSize() < 4000 + && identifier.getFieldType().equals(JavaType.STRING)) { + columnBuilder.addIntegerAttribute("length", + identifier.getColumnSize()); + } + + // Add precision and scale attributes for numeric fields + if (identifier.getScale() > 0 + && (identifier.getFieldType().equals(JavaType.DOUBLE_OBJECT) + || identifier.getFieldType().equals( + JavaType.DOUBLE_PRIMITIVE) || identifier + .getFieldType().equals(BIG_DECIMAL))) { + columnBuilder.addIntegerAttribute("precision", + identifier.getColumnSize()); + columnBuilder.addIntegerAttribute("scale", identifier.getScale()); + } + + return columnBuilder; + } + + private AnnotationMetadata getEmbeddableAnnotation() { + if (governorTypeDetails.getAnnotation(EMBEDDABLE) != null) { + return null; + } + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + EMBEDDABLE); + return annotationBuilder.build(); + } + + /** + * Locates declared fields. + *

    + * If no parent is defined, one will be located or created. All declared + * fields will be returned. + * + * @return fields (never returns null) + */ + private List getFieldBuilders() { + // Locate all declared fields + final List declaredFields = governorTypeDetails + .getDeclaredFields(); + + // Add fields to ITD from annotation + final List fields = new ArrayList(); + if (identifierServiceResult != null) { + for (final Identifier identifier : identifierServiceResult) { + final List annotations = new ArrayList(); + annotations.add(getColumnBuilder(identifier)); + if (identifier.getFieldType().equals(DATE)) { + setDateAnnotations(identifier.getColumnDefinition(), + annotations); + } + + final FieldMetadata idField = new FieldMetadataBuilder(getId(), + Modifier.PRIVATE, annotations, + identifier.getFieldName(), identifier.getFieldType()) + .build(); + + // Only add field to ITD if not declared on governor + if (!hasField(declaredFields, idField)) { + fields.add(idField); + } + } + } + + fields.addAll(declaredFields); + + // Remove fields with static and transient modifiers + for (final Iterator iter = fields.iterator(); iter + .hasNext();) { + final FieldMetadata field = iter.next(); + if (Modifier.isStatic(field.getModifier()) + || Modifier.isTransient(field.getModifier())) { + iter.remove(); + } + } + + // Remove fields with the @Transient annotation + final List transientAnnotatedFields = governorTypeDetails + .getFieldsWithAnnotation(TRANSIENT); + if (fields.containsAll(transientAnnotatedFields)) { + fields.removeAll(transientAnnotatedFields); + } + + final List fieldBuilders = new ArrayList(); + if (!fields.isEmpty()) { + for (final FieldMetadata field : fields) { + fieldBuilders.add(new FieldMetadataBuilder(field)); + } + return fieldBuilders; + } + + // We need to create a default identifier field + final List annotations = new ArrayList(); + + // Compute the column name, as required + final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( + COLUMN); + columnBuilder.addStringAttribute("name", "id"); + columnBuilder.addBooleanAttribute("nullable", false); + annotations.add(columnBuilder); + + fieldBuilders.add(new FieldMetadataBuilder(getId(), Modifier.PRIVATE, + annotations, new JavaSymbolName("id"), LONG_OBJECT)); + + return fieldBuilders; + } + + /** + * Locates the mutator methods. + *

    + * If {@link #getFieldBuilders()} returns fields created by this ITD, public + * mutators will automatically be produced in the declaring class. + * + * @param fields + * @return the mutators (never returns null) + */ + private List getMutators( + final List fields) { + final List mutators = new ArrayList(); + + // Compute the names of the mutators that will be produced + for (final FieldMetadataBuilder field : fields) { + final JavaSymbolName requiredMutatorName = BeanInfoUtils + .getMutatorMethodName(field.getFieldName()); + final JavaType parameterType = field.getFieldType(); + final MethodMetadata mutator = getGovernorMethod( + requiredMutatorName, parameterType); + if (mutator == null) { + mutators.add(getMutatorMethod(field.getFieldName(), + field.getFieldType())); + } + else { + Validate.isTrue( + Modifier.isPublic(mutator.getModifier()), + "User provided field but failed to provide a public '%s(%s)' method in '%s'", + requiredMutatorName.getSymbolName(), field + .getFieldName().getSymbolName(), destination + .getFullyQualifiedTypeName()); + mutators.add(new MethodMetadataBuilder(mutator)); + } + } + return mutators; + } + + /** + * Locates the no-arg constructor for this class, if available. + *

    + * If a class defines a no-arg constructor, it is returned (irrespective of + * access modifiers). + *

    + * If a class does not define a no-arg constructor, one might be created. It + * will only be created if the {@link RooIdentifier#noArgConstructor} is + * true AND there is at least one other constructor declared in the source + * file. If a constructor is created, it will have a private access + * modifier. + * + * @return the constructor (may return null if no constructor is to be + * produced) + */ + private ConstructorMetadataBuilder getNoArgConstructor() { + // Search for an existing constructor + final List parameterTypes = new ArrayList(); + final ConstructorMetadata result = governorTypeDetails + .getDeclaredConstructor(parameterTypes); + if (result != null) { + // Found an existing no-arg constructor on this class + return null; + } + + // Create the constructor + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("super();"); + + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + getId()); + constructorBuilder.setModifier(publicNoArgConstructor ? Modifier.PUBLIC + : Modifier.PRIVATE); + constructorBuilder.setParameterTypes(AnnotatedJavaType + .convertFromJavaTypes(parameterTypes)); + constructorBuilder.setBodyBuilder(bodyBuilder); + return constructorBuilder; + } + + /** + * Locates the parameterised constructor consisting of the id fields for + * this class. + * + * @param fields + * @return the constructor, never null. + */ + private ConstructorMetadataBuilder getParameterizedConstructor( + final List fields) { + // Search for an existing constructor + final List parameterTypes = new ArrayList(); + for (final FieldMetadataBuilder field : fields) { + parameterTypes.add(field.getFieldType()); + } + + final ConstructorMetadata result = governorTypeDetails + .getDeclaredConstructor(parameterTypes); + if (result != null) { + // Found an existing parameterised constructor on this class + publicNoArgConstructor = true; + return null; + } + + final List parameterNames = new ArrayList(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("super();"); + for (final FieldMetadataBuilder field : fields) { + final String fieldName = field.getFieldName().getSymbolName(); + bodyBuilder.appendFormalLine("this." + fieldName + " = " + + fieldName + ";"); + parameterNames.add(field.getFieldName()); + } + + // Create the constructor + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + getId()); + constructorBuilder.setModifier(Modifier.PUBLIC); + constructorBuilder.setParameterTypes(AnnotatedJavaType + .convertFromJavaTypes(parameterTypes)); + constructorBuilder.setParameterNames(parameterNames); + constructorBuilder.setBodyBuilder(bodyBuilder); + return constructorBuilder; + } + + private boolean hasField( + final List declaredFields, + final FieldMetadata idField) { + for (final FieldMetadata declaredField : declaredFields) { + if (declaredField.getFieldName().equals(idField.getFieldName())) { + return true; + } + } + return false; + } + + private void setDateAnnotations(final String columnDefinition, + final List annotations) { + // Add JSR 220 @Temporal annotation to date fields + String temporalType = StringUtils.defaultIfEmpty( + StringUtils.upperCase(columnDefinition), "DATE"); + if ("DATETIME".equals(temporalType)) { + temporalType = "TIMESTAMP"; // ROO-2606 + } + final AnnotationMetadataBuilder temporalBuilder = new AnnotationMetadataBuilder( + TEMPORAL); + temporalBuilder.addEnumAttribute("value", new EnumDetails( + TEMPORAL_TYPE, new JavaSymbolName(temporalType))); + annotations.add(temporalBuilder); + + final AnnotationMetadataBuilder dateTimeFormatBuilder = new AnnotationMetadataBuilder( + DATE_TIME_FORMAT); + dateTimeFormatBuilder.addStringAttribute("style", "M-"); + annotations.add(dateTimeFormatBuilder); + } +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadataProvider.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadataProvider.java new file mode 100644 index 000000000..ff9cd8bbb --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.jpa.identifier; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link IdentifierMetadata}. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface IdentifierMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadataProviderImpl.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadataProviderImpl.java new file mode 100644 index 000000000..e6b839456 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierMetadataProviderImpl.java @@ -0,0 +1,191 @@ +package org.springframework.roo.addon.jpa.identifier; + +import static org.springframework.roo.model.RooJavaType.ROO_IDENTIFIER; + +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.addon.jpa.AbstractIdentifierServiceAwareMetadataProvider; +import org.springframework.roo.addon.serializable.SerializableMetadataProvider; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link IdentifierMetadataProvider}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class IdentifierMetadataProviderImpl extends + AbstractIdentifierServiceAwareMetadataProvider implements + IdentifierMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(IdentifierMetadataProviderImpl.class); + + private ConfigurableMetadataProvider configurableMetadataProvider; + private ProjectOperations projectOperations; + private SerializableMetadataProvider serializableMetadataProvider; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_IDENTIFIER); + getConfigurableMetadataProvider().addMetadataTrigger(ROO_IDENTIFIER); + getSerializableMetadataProvider().addMetadataTrigger(ROO_IDENTIFIER); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return IdentifierMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_IDENTIFIER); + getConfigurableMetadataProvider().removeMetadataTrigger(ROO_IDENTIFIER); + getSerializableMetadataProvider().removeMetadataTrigger(ROO_IDENTIFIER); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = IdentifierMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = IdentifierMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Identifier"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final IdentifierAnnotationValues annotationValues = new IdentifierAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound()) { + return null; + } + + // We know governor type details are non-null and can be safely cast + final JavaType javaType = IdentifierMetadata + .getJavaType(metadataIdentificationString); + final List identifierServiceResult = getIdentifiersForType(javaType); + + final LogicalPath path = PhysicalTypeIdentifierNamingUtils + .getPath(metadataIdentificationString); + if (projectOperations.isProjectAvailable(path.getModule())) { + // If the project itself changes, we want a chance to refresh this + // item + getMetadataDependencyRegistry().registerDependency( + ProjectMetadata.getProjectIdentifier(path.getModule()), + metadataIdentificationString); + } + + return new IdentifierMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues, + identifierServiceResult); + } + + public String getProvidesType() { + return IdentifierMetadata.getMetadataIdentifierType(); + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on IdentifierMetadataProviderImpl."); + return null; + } + }else{ + return configurableMetadataProvider; + } + + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on IdentifierMetadataProviderImpl."); + return null; + } + } + + public SerializableMetadataProvider getSerializableMetadataProvider(){ + if(serializableMetadataProvider == null){ + // Get all Services implement SerializableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(SerializableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (SerializableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load SerializableMetadataProvider on IdentifierMetadataProviderImpl."); + return null; + } + }else{ + return serializableMetadataProvider; + } + + } + +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierService.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierService.java new file mode 100644 index 000000000..985a133df --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/IdentifierService.java @@ -0,0 +1,31 @@ +package org.springframework.roo.addon.jpa.identifier; + +import java.util.List; + +import org.springframework.roo.model.JavaType; + +/** + * Provides a list of identifier fields that a given {@link JavaType} may + * require. + * + * @author Ben Alex + * @since 1.1 + */ +public interface IdentifierService { + + /** + * For the given type, returns zero or more identifier fields that the type + * requires. An implementation may return null if they do not have knowledge + * of any identifier fields for that type. An implementation returning a + * non-null value indicates the implementation is authoritative for + * determining identifier fields for the type. It is legal to return a + * non-null list, which would denote an authoritative implementation but the + * type simply has no identifier field requirement. + * + * @param pkType the PK class type for which identifier information is + * desired (required) + * @return null if the implementation is non-authoritative for the type, + * otherwise zero or more identifiers that the type should have + */ + List getIdentifiers(JavaType pkType); +} diff --git a/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/RooIdentifier.java b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/RooIdentifier.java new file mode 100644 index 000000000..2565da689 --- /dev/null +++ b/addon-jpa/src/main/java/org/springframework/roo/addon/jpa/identifier/RooIdentifier.java @@ -0,0 +1,51 @@ +package org.springframework.roo.addon.jpa.identifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.roo.addon.configurable.RooConfigurable; +import org.springframework.roo.addon.serializable.RooSerializable; + +/** + * Provides identifier services related to JPA. + *

    + * Using this annotation also triggers {@link RooSerializable} and + * {@link RooConfigurable}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooIdentifier { + + /** + * @return whether to delete the database-managed identifier (defaults to + * false). + */ + boolean dbManaged() default false; + + /** + * @return whether to generate getters for each non-transient field declared + * in this class (defaults to true) + */ + boolean gettersByDefault() default true; + + /** + * Allows disabling the automated creation of a no-arg constructor. This + * might be appropriate, for example, if another add-on is providing more + * sophisticated constructor creation facilities. + * + * @return whether to generate a no-argument constructor in this class + * (defaults to true) + */ + boolean noArgConstructor() default true; + + /** + * @return whether to generate setters for each non-transient field declared + * in this class (defaults to false) + */ + boolean settersByDefault() default false; +} diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/appengine-web-template.xml b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/appengine-web-template.xml new file mode 100644 index 000000000..dd5c21d6e --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/appengine-web-template.xml @@ -0,0 +1,12 @@ + + + TO_BE_CHANGED_BY_ADDON + 1 + true + true + + + + + + \ No newline at end of file diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/configuration.xml b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/configuration.xml new file mode 100644 index 000000000..33d00b1b8 --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/configuration.xml @@ -0,0 +1,420 @@ + + + + + + + com.h2database + h2 + 1.3.172 + + + + + + + org.hsqldb + hsqldb + 2.2.9 + + + + + + + postgresql + postgresql + 9.1-901-1.jdbc4 + + + + + + + mysql + mysql-connector-java + 5.1.18 + + + + + + + net.sourceforge.jtds + jtds + 1.2.4 + + + + + + + net.sourceforge.jtds + jtds + 1.2.4 + + + + + + + com.oracle + ojdbc14 + 10.2.0.5 + + + + + + + com.ibm + db2jcc4 + 9.7.2 + + + + + + + net.sf.jt400 + jt400 + 6.7 + + + + + + + org.apache.derby + derby + 10.8.2.2 + + + + + + + org.apache.derby + derbyclient + 10.8.2.2 + + + + + + + org.firebirdsql.jdbc + jaybird + 2.1.6 + + + xalan + xalan + + + + + + + + 1.7.4 + ${user.home}/.m2/repository/com/google/appengine/appengine-java-sdk/${gae.version}/appengine-java-sdk-${gae.version} + + + + com.google.appengine.orm + datanucleus-appengine + 2.1.1 + + + com.google.appengine + appengine-api-1.0-sdk + ${gae.version} + + + com.google.appengine + appengine-testing + ${gae.version} + test + + + com.google.appengine + appengine-api-stubs + ${gae.version} + test + + + com.google.appengine + appengine-api-labs + ${gae.version} + test + + + + + com.google.appengine + appengine-maven-plugin + ${gae.version} + + + + + + + com.force.sdk + force-jpa + 22.0.9-BETA + + + + + + + + + org.hibernate + hibernate-core + 4.3.6.Final + + + org.hibernate + hibernate-entitymanager + 4.3.6.Final + + + cglib + cglib + + + dom4j + dom4j + + + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.0.Final + + + commons-collections + commons-collections + 3.2.1 + + + + + + + org.apache.openjpa + openjpa + 2.2.2 + + + commons-logging + commons-logging + + + org.apache.geronimo.specs + geronimo-jms_1.1_spec + + + + + + + org.apache.openjpa + openjpa-maven-plugin + 2.2.2 + + **/*.class + **/*_Roo_*.class + true + + + + enhancer + process-classes + + enhance + + + + + + org.apache.openjpa + openjpa + 2.2.2 + + + commons-logging + commons-logging + + + org.apache.geronimo.specs + geronimo-jms_1.1_spec + + + + + + + + + + + EclipseLink Repo + EclipseLink Repo + http://download.eclipse.org/rt/eclipselink/maven.repo + + + + + org.eclipse.persistence + eclipselink + 2.5.0 + + + + + + + DataNucleus_Repos2 + DataNucleus Repository + http://www.datanucleus.org/downloads/maven2 + + true + + + + + + DataNucleus_2 + http://www.datanucleus.org/downloads/maven2/ + + + + + org.apache.geronimo.specs + geronimo-jpa_2.0_spec + 1.1 + + + org.datanucleus + datanucleus-core + 3.1.3 + + + org.datanucleus + datanucleus-rdbms + 3.1.3 + + + org.datanucleus + datanucleus-api-jpa + 3.1.3 + + + org.datanucleus + datanucleus-api-jdo + 3.1.3 + + + javax.jdo + jdo-api + 3.0 + + + + + org.datanucleus + maven-datanucleus-plugin + 3.1.3 + + + org.datanucleus + datanucleus-core + 3.1.3 + + + org.datanucleus + datanucleus-api-jpa + 3.1.3 + + + + false + ${basedir}/src/main/resources/log4j.properties + **/*.class + true + ASM + JPA + + + + compile + + enhance + + + + + + + + + + + + org.hibernate + hibernate-validator + 4.3.2.Final + + + javax.validation + validation-api + 1.0.0.GA + + + + + + + + javax.transaction + jta + 1.1 + + + org.springframework + spring-jdbc + ${spring.version} + + + org.springframework + spring-orm + ${spring.version} + + + commons-pool + commons-pool + 1.5.6 + + + commons-dbcp + commons-dbcp + 1.4 + + + commons-logging + commons-logging + + + xml-apis + xml-apis + + + + + + \ No newline at end of file diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/database-dot-com-template.properties b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/database-dot-com-template.properties new file mode 100644 index 000000000..c84c0cb50 --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/database-dot-com-template.properties @@ -0,0 +1 @@ +url=TO_BE_CHANGED_BY_ADDON \ No newline at end of file diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/database-template.properties b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/database-template.properties new file mode 100644 index 000000000..a164302eb --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/database-template.properties @@ -0,0 +1,4 @@ +database.driverClassName=TO_BE_CHANGED_BY_ADDON +database.url=TO_BE_CHANGED_BY_ADDON +database.username=TO_BE_CHANGED_BY_ADDON +database.password=TO_BE_CHANGED_BY_USER diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/jndi-template.properties b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/jndi-template.properties new file mode 100644 index 000000000..0cffa56ae --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/jndi-template.properties @@ -0,0 +1,7 @@ +java.naming.factory.initial=TO_BE_CHANGED_BY_USER +java.naming.factory.object=TO_BE_CHANGED_BY_USER +java.naming.factory.state=TO_BE_CHANGED_BY_USER +java.naming.factory.control=TO_BE_CHANGED_BY_USER +java.naming.factory.url.pkgs=TO_BE_CHANGED_BY_USER +java.naming.provider.url=TO_BE_CHANGED_BY_USER +java.naming.dns.url=TO_BE_CHANGED_BY_USER diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/jpa-dialects.properties b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/jpa-dialects.properties new file mode 100644 index 000000000..b2968f229 --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/jpa-dialects.properties @@ -0,0 +1,41 @@ +OPENJPA.H2_IN_MEMORY=org.apache.openjpa.jdbc.sql.H2Dictionary +OPENJPA.HYPERSONIC_IN_MEMORY=org.apache.openjpa.jdbc.sql.HSQLDictionary +OPENJPA.HYPERSONIC_PERSISTENT=org.apache.openjpa.jdbc.sql.HSQLDictionary +OPENJPA.DERBY_EMBEDDED=org.apache.openjpa.jdbc.sql.DerbyDictionary +OPENJPA.DERBY_CLIENT=org.apache.openjpa.jdbc.sql.DerbyDictionary +OPENJPA.POSTGRES=org.apache.openjpa.jdbc.sql.PostgresDictionary +OPENJPA.MYSQL=org.apache.openjpa.jdbc.sql.MySQLDictionary +OPENJPA.MSSQL=org.apache.openjpa.jdbc.sql.SQLServerDictionary +OPENJPA.ORACLE=org.apache.openjpa.jdbc.sql.OracleDictionary +OPENJPA.SYBASE=org.apache.openjpa.jdbc.sql.SybaseDictionary +OPENJPA.DB2_EXPRESS_C=org.apache.openjpa.jdbc.sql.DB2Dictionary +OPENJPA.DB2_400=org.apache.openjpa.jdbc.sql.DB2Dictionary +OPENJPA.FIREBIRD=org.apache.openjpa.jdbc.sql.FirebirdDictionary + +HIBERNATE.H2_IN_MEMORY=org.hibernate.dialect.H2Dialect +HIBERNATE.HYPERSONIC_IN_MEMORY=org.hibernate.dialect.HSQLDialect +HIBERNATE.HYPERSONIC_PERSISTENT=org.hibernate.dialect.HSQLDialect +HIBERNATE.DERBY_EMBEDDED=org.hibernate.dialect.DerbyDialect +HIBERNATE.DERBY_CLIENT=org.hibernate.dialect.DerbyDialect +HIBERNATE.POSTGRES=org.hibernate.dialect.PostgreSQLDialect +HIBERNATE.MYSQL=org.hibernate.dialect.MySQL5InnoDBDialect +HIBERNATE.MSSQL=org.hibernate.dialect.SQLServerDialect +HIBERNATE.ORACLE=org.hibernate.dialect.OracleDialect +HIBERNATE.SYBASE=org.hibernate.dialect.SybaseDialect +HIBERNATE.DB2_EXPRESS_C=org.hibernate.dialect.DB2Dialect +HIBERNATE.DB2_400=org.hibernate.dialect.DB2400Dialect +HIBERNATE.FIREBIRD=org.hibernate.dialect.FirebirdDialect + +ECLIPSELINK.H2_IN_MEMORY=org.eclipse.persistence.platform.database.H2Platform +ECLIPSELINK.HYPERSONIC_IN_MEMORY=org.eclipse.persistence.platform.database.HSQLPlatform +ECLIPSELINK.HYPERSONIC_PERSISTENT=org.eclipse.persistence.platform.database.HSQLPlatform +ECLIPSELINK.DERBY_EMBEDDED=org.eclipse.persistence.platform.database.DerbyPlatform +ECLIPSELINK.DERBY_CLIENT=org.eclipse.persistence.platform.database.DerbyPlatform +ECLIPSELINK.POSTGRES=org.eclipse.persistence.platform.database.PostgreSQLPlatform +ECLIPSELINK.MYSQL=org.eclipse.persistence.platform.database.MySQLPlatform +ECLIPSELINK.MSSQL=org.eclipse.persistence.platform.database.SQLServerPlatform +ECLIPSELINK.ORACLE=org.eclipse.persistence.platform.database.OraclePlatform +ECLIPSELINK.SYBASE=org.eclipse.persistence.platform.database.SybasePlatform +ECLIPSELINK.DB2_EXPRESS_C=org.eclipse.persistence.platform.database.DB2Platform +ECLIPSELINK.DB2_400=org.eclipse.persistence.platform.database.DB2Platform +ECLIPSELINK.FIREBIRD=org.eclipse.persistence.platform.database.FirebirdPlatform diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/logging.properties b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/logging.properties new file mode 100644 index 000000000..4a78b7f35 --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/logging.properties @@ -0,0 +1,28 @@ +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# + +# Set the default logging level for all loggers to WARNING +.level = WARNING + +# Set the default logging level for ORM, specifically, to WARNING +DataNucleus.JDO.level=WARNING +DataNucleus.Persistence.level=WARNING +DataNucleus.Cache.level=WARNING +DataNucleus.MetaData.level=WARNING +DataNucleus.General.level=WARNING +DataNucleus.Utility.level=WARNING +DataNucleus.Transaction.level=WARNING +DataNucleus.Datastore.level=WARNING +DataNucleus.ClassLoading.level=WARNING +DataNucleus.Plugin.level=WARNING +DataNucleus.ValueGeneration.level=WARNING +DataNucleus.Enhancer.level=WARNING +DataNucleus.SchemaTool.level=WARNING diff --git a/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/persistence-template.xml b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/persistence-template.xml new file mode 100644 index 000000000..359ae446c --- /dev/null +++ b/addon-jpa/src/main/resources/org/springframework/roo/addon/jpa/persistence-template.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/JpaOperationsImplTest.java b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/JpaOperationsImplTest.java new file mode 100644 index 000000000..09f59ed64 --- /dev/null +++ b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/JpaOperationsImplTest.java @@ -0,0 +1,257 @@ +package org.springframework.roo.addon.jpa; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.roo.addon.jpa.JdbcDatabase.H2_IN_MEMORY; +import static org.springframework.roo.addon.jpa.JpaOperationsImpl.JPA_DIALECTS_FILE; +import static org.springframework.roo.addon.jpa.JpaOperationsImpl.PERSISTENCE_XML; +import static org.springframework.roo.addon.jpa.OrmProvider.HIBERNATE; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; + +/** + * Unit test of {@link JpaOperationsImpl} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JpaOperationsImplTest { + + private static final String APP_CONTEXT = "" + + "" + ""; + private static final String APPLICATION_CONTEXT_PATH = "/path/to/the/app/context"; + private static final String DB_DIALECT = "dbDialect"; + private static final String DB_HOST_NAME = "myDbHost"; + private static final String DB_JNDI_NAME = "myDataSource"; + private static final String DB_NAME = "myDbName"; + private static final String DB_PASSWORD = "myDbPassword"; + private static final String DB_USER_NAME = "myDbUserName"; + private static final String EXPECTED_APPLICATION_CONTEXT = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + "\n"; + private static final String EXPECTED_JNDI_APPLICATION_CONTEXT = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + "\n"; + private static final String EXPECTED_PERSISTENCE_XML_FOR_H2_IN_MEMORY_AND_HIBERNATE = "\n" + + "\n" + + "\n" + + " org.hibernate.jpa.HibernatePersistenceProvider\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + private static final String PERSISTENCE_PATH = "/path/to/persistence"; + private static final String PERSISTENCE_UNIT = "myPersistenceUnit"; + private static final String POM = "" + + "" + + " " + + " " + + " " + + " org.apache.maven.plugins" + + " maven-eclipse-plugin" + + " 2.7" + + " " + + " " + + " " + + " " + + " " + + " " + " " + ""; + private static final String POM_PATH = "/path/to/the/pom"; + private static final String TRANSACTION_MANAGER = "myTransactionManager"; + private Properties dialects; + + // Fixture + private JpaOperationsImpl jpaOperations; + + @Mock private FileManager mockFileManager; + + @Mock private PathResolver mockPathResolver; + + @Mock private ProjectOperations mockProjectOperations; + + @Mock private PropFileOperations mockPropFileOperations; + + /** + * Creates a new {@link InputStream} each time we need to read from the + * Spring application context + * + * @param pom the application context XML as a String (required) + * @return a fresh stream + */ + private InputStream getAppContextInputStream(final String appContext) { + return new ByteArrayInputStream(appContext.getBytes()); + } + + /** + * Creates a new {@link InputStream} each time we need to read from the POM + * + * @param pom the POM XML as a String (required) + * @return a fresh stream + */ + private ByteArrayInputStream getPomInputStream(final String pom) { + return new ByteArrayInputStream(pom.getBytes()); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + // Mocks + when(mockProjectOperations.getPathResolver()).thenReturn( + mockPathResolver); + when( + mockPathResolver.getFocusedIdentifier(Path.ROOT, + JpaOperationsImpl.POM_XML)).thenReturn(POM_PATH); + when( + mockPathResolver.getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + JpaOperationsImpl.APPLICATION_CONTEXT_XML)).thenReturn( + APPLICATION_CONTEXT_PATH); + + // Object under test + jpaOperations = new JpaOperationsImpl(); + jpaOperations.pathResolver = mockPathResolver; + jpaOperations.fileManager = mockFileManager; + jpaOperations.projectOperations = mockProjectOperations; + jpaOperations.propFileOperations = mockPropFileOperations; + + // Things that are too hard or ugly to mock + dialects = new Properties(); + } + + @Test + public void testConfigureJpaForH2InMemoryAndHibernateAndJndiForNewProject() { + // Set up + when(mockFileManager.getInputStream(POM_PATH)).thenReturn( + getPomInputStream(POM), getPomInputStream(POM)); + when(mockFileManager.getInputStream(APPLICATION_CONTEXT_PATH)) + .thenReturn(getAppContextInputStream(APP_CONTEXT)); + when( + mockPathResolver.getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, + PERSISTENCE_XML)).thenReturn(PERSISTENCE_PATH); + // i.e. no existing persistence.xml + when(mockFileManager.exists(PERSISTENCE_PATH)).thenReturn(false); + when( + mockPropFileOperations.loadProperties(JPA_DIALECTS_FILE, + JpaOperationsImpl.class)).thenReturn(dialects); + + final OrmProvider ormProvider = HIBERNATE; + final JdbcDatabase jdbcDatabase = H2_IN_MEMORY; + dialects.put(ormProvider.name() + "." + jdbcDatabase.name(), DB_DIALECT); + + // Invoke + jpaOperations.configureJpa(ormProvider, jdbcDatabase, DB_JNDI_NAME, + null, DB_HOST_NAME, DB_NAME, DB_USER_NAME, DB_PASSWORD, + TRANSACTION_MANAGER, PERSISTENCE_UNIT, ""); + + // Check + verifyFileUpdate(EXPECTED_JNDI_APPLICATION_CONTEXT, + APPLICATION_CONTEXT_PATH); + verifyFileUpdate( + EXPECTED_PERSISTENCE_XML_FOR_H2_IN_MEMORY_AND_HIBERNATE, + PERSISTENCE_PATH); + } + + @Test + public void testConfigureJpaForH2InMemoryAndHibernateForNewProject() { + // Set up + when(mockFileManager.getInputStream(POM_PATH)).thenReturn( + getPomInputStream(POM), getPomInputStream(POM)); + when(mockFileManager.getInputStream(APPLICATION_CONTEXT_PATH)) + .thenReturn(getAppContextInputStream(APP_CONTEXT)); + when( + mockPathResolver.getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, + PERSISTENCE_XML)).thenReturn(PERSISTENCE_PATH); + // i.e. no existing persistence.xml + when(mockFileManager.exists(PERSISTENCE_PATH)).thenReturn(false); + when( + mockPropFileOperations.loadProperties(JPA_DIALECTS_FILE, + JpaOperationsImpl.class)).thenReturn(dialects); + + final OrmProvider ormProvider = HIBERNATE; + final JdbcDatabase jdbcDatabase = H2_IN_MEMORY; + dialects.put(ormProvider.name() + "." + jdbcDatabase.name(), DB_DIALECT); + + // Invoke + jpaOperations.configureJpa(ormProvider, jdbcDatabase, null, null, + DB_HOST_NAME, DB_NAME, DB_USER_NAME, DB_PASSWORD, + TRANSACTION_MANAGER, PERSISTENCE_UNIT, ""); + + // Check + verifyFileUpdate(EXPECTED_APPLICATION_CONTEXT, APPLICATION_CONTEXT_PATH); + verifyFileUpdate( + EXPECTED_PERSISTENCE_XML_FOR_H2_IN_MEMORY_AND_HIBERNATE, + PERSISTENCE_PATH); + } + + /** + * Verifies that the mock {@link FileManager} was asked to write the given + * contents to the given file + * + * @param expectedContents the contents we expect to be written + * @param filename the file we expect to be written to + */ + private void verifyFileUpdate(final String expectedContents, + final String filename) { + final ArgumentCaptor textCaptor = ArgumentCaptor + .forClass(String.class); + verify(mockFileManager).createOrUpdateTextFileIfRequired(eq(filename), + textCaptor.capture(), eq(false)); + // Replace the dummy line terminator with the platform-specific one that + // will be applied by XmlUtils.nodeToString. + final String normalisedContents = expectedContents.replace("\n", + IOUtils.LINE_SEPARATOR); + assertEquals(normalisedContents, textCaptor.getValue()); + } +} diff --git a/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerMethodTest.java b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerMethodTest.java new file mode 100644 index 000000000..09b4e1dcf --- /dev/null +++ b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerMethodTest.java @@ -0,0 +1,161 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of the {@link EntityLayerMethod} enum + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class EntityLayerMethodTest { + + private static final List NO_TYPES = Collections + . emptyList(); + + private static final String PLURAL = "People"; + + @Mock private JpaCrudAnnotationValues mockAnnotationValues; + @Mock private JavaType mockIdType; + @Mock private JavaSymbolName mockParameterName; + // Fixture + @Mock private JavaType mockTargetEntity; + + private void assertMethodCall(final String expectedMethodCall, + final EntityLayerMethod method, final String... parameterNames) { + final List parameters = new ArrayList(); + for (final String parameterName : parameterNames) { + final JavaSymbolName mockSymbol = mock(JavaSymbolName.class); + when(mockSymbol.getSymbolName()).thenReturn(parameterName); + // We can use any parameter type here, as it's ignored in production + parameters.add(new MethodParameter(JavaType.OBJECT, mockSymbol)); + } + + // Invoke and check + assertEquals(expectedMethodCall, method.getCall(mockAnnotationValues, + mockTargetEntity, PLURAL, parameters)); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockParameterName.getSymbolName()).thenReturn("person"); + when(mockTargetEntity.getFullyQualifiedTypeName()).thenReturn( + "com.example.Person"); + when(mockTargetEntity.getSimpleTypeName()).thenReturn("Person"); + when(mockIdType.getFullyQualifiedTypeName()).thenReturn( + Long.class.getName()); + } + + @Test + public void testCallClearMethod() { + // Set up + when(mockAnnotationValues.getClearMethod()).thenReturn("erase"); + + // Invoke and check + assertMethodCall("Person.erase()", EntityLayerMethod.CLEAR); + } + + @Test + public void testCallCountAllMethod() { + // Set up + when(mockAnnotationValues.getCountMethod()).thenReturn("total"); + + // Invoke and check + assertMethodCall("Person.totalPeople()", EntityLayerMethod.COUNT_ALL); + } + + @Test + public void testCallFindAllMethod() { + // Set up + when(mockAnnotationValues.getFindAllMethod()).thenReturn("seekAll"); + + // Invoke and check + assertMethodCall("Person.seekAllPeople()", EntityLayerMethod.FIND_ALL); + } + + @Test + public void testCallFindEntriesMethod() { + // Set up + when(mockAnnotationValues.getFindEntriesMethod()).thenReturn("lookFor"); + + // Invoke and check + assertMethodCall("Person.lookForPersonEntries(x, y)", + EntityLayerMethod.FIND_ENTRIES, "x", "y"); + } + + @Test + public void testCallFlushMethod() { + // Set up + when(mockAnnotationValues.getFlushMethod()).thenReturn("bloosh"); + + // Invoke and check + assertMethodCall("person.bloosh()", EntityLayerMethod.FLUSH, "person"); + } + + @Test + public void testCallMergeMethod() { + // Set up + when(mockAnnotationValues.getMergeMethod()).thenReturn("blend"); + + // Invoke and check + assertMethodCall("person.blend()", EntityLayerMethod.MERGE, "person"); + } + + @Test + public void testCallPersistMethod() { + // Set up + when(mockAnnotationValues.getPersistMethod()).thenReturn("store"); + + // Invoke and check + assertMethodCall("person.store()", EntityLayerMethod.PERSIST, "person"); + } + + @Test + public void testCallRemoveMethod() { + // Set up + when(mockAnnotationValues.getRemoveMethod()).thenReturn("trash"); + + // Invoke and check + assertMethodCall("person.trash()", EntityLayerMethod.REMOVE, "person"); + } + + @Test + public void testParameterTypes() { + for (final EntityLayerMethod method : EntityLayerMethod.values()) { + final List parameterTypes = method.getParameterTypes( + mockTargetEntity, mockIdType); + if (method.isStatic()) { + // All we can check is that it's not null + assertNotNull(method + " method has null parameter types", + parameterTypes); + } + else { + assertEquals(Arrays.asList(mockTargetEntity), parameterTypes); + } + } + } + + @Test + public void testValueOfBogusMethodId() { + assertNull(EntityLayerMethod.valueOf("foo", NO_TYPES, mockTargetEntity, + mockIdType)); + } +} diff --git a/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerProviderTest.java b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerProviderTest.java new file mode 100644 index 000000000..9b2131bf6 --- /dev/null +++ b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/EntityLayerProviderTest.java @@ -0,0 +1,198 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.CLEAR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FLUSH_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Path; + +/** + * Unit test of {@link EntityLayerProvider} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class EntityLayerProviderTest { + + private static final String CALLER_MID = "MID:caller#com.example.MyService"; + + // Maps the supported entity methods to their test parameter names + private static final Map> METHODS = new HashMap>(); + + static { + METHODS.put(CLEAR_METHOD, Collections. emptyList()); + METHODS.put(COUNT_ALL_METHOD, Collections. emptyList()); + METHODS.put(FIND_ALL_METHOD, Collections. emptyList()); + METHODS.put(FIND_ENTRIES_METHOD, Arrays.asList("x", "y")); + METHODS.put(FIND_ALL_SORTED_METHOD, Arrays.asList("x", "y")); + METHODS.put(FIND_ENTRIES_SORTED_METHOD, Arrays.asList("w", "x", "y", "z")); + METHODS.put(FIND_METHOD, Arrays.asList("id")); + METHODS.put(FLUSH_METHOD, Collections. emptyList()); + METHODS.put(MERGE_METHOD, Collections. emptyList()); + METHODS.put(PERSIST_METHOD, Collections. emptyList()); + METHODS.put(REMOVE_METHOD, Collections. emptyList()); + } + + // Fixture + private EntityLayerProvider layerProvider; + @Mock private JpaCrudAnnotationValues mockAnnotationValues; + + @Mock private JavaType mockIdType; + @Mock private JpaActiveRecordMetadataProvider mockJpaActiveRecordMetadataProvider; + @Mock private MetadataService mockMetadataService; + @Mock private PluralMetadata mockPluralMetadata; + @Mock private JavaType mockTargetEntity; + @Mock private TypeLocationService mockTypeLocationService; + private String pluralId; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockTargetEntity.getFullyQualifiedTypeName()).thenReturn( + "com.example.Pizza"); + when(mockIdType.getFullyQualifiedTypeName()).thenReturn( + Long.class.getName()); + when(mockTypeLocationService.getTypePath(mockTargetEntity)).thenReturn( + Path.SRC_MAIN_JAVA.getModulePathId("")); + + pluralId = PluralMetadata.createIdentifier(mockTargetEntity, + Path.SRC_MAIN_JAVA.getModulePathId("")); + layerProvider = new EntityLayerProvider(); + layerProvider.typeLocationService = mockTypeLocationService; + layerProvider + .setJpaActiveRecordMetadataProvider(mockJpaActiveRecordMetadataProvider); + layerProvider.setMetadataService(mockMetadataService); + } + + private void setUpMockAnnotationValues() { + when( + mockJpaActiveRecordMetadataProvider + .getAnnotationValues(mockTargetEntity)).thenReturn( + mockAnnotationValues); + } + + private void setUpPlural(final String plural) { + when(mockMetadataService.get(pluralId)).thenReturn(mockPluralMetadata); + when(mockPluralMetadata.getPlural()).thenReturn(plural); + } + + @Test + public void testGetAdditionsForBogusMethod() { + // Set up + setUpMockAnnotationValues(); + setUpPlural("anything"); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, "bogus", mockTargetEntity, + mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsForMethodAnnotatedWithEmptyName() { + // Set up + setUpMockAnnotationValues(); + when(mockAnnotationValues.getFindAllMethod()).thenReturn(""); + setUpPlural("anything"); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsForMethodAnnotatedWithNonEmptyName() { + // Set up + setUpMockAnnotationValues(); + when(mockAnnotationValues.getFindAllMethod()).thenReturn("getAll"); + setUpPlural("Pizzas"); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertEquals("getAllPizzas", additions.getMethodName()); + } + + @Test + public void testGetAdditionsWhenEntityAnnotationValuesNotAvailable() { + // Set up + when( + mockJpaActiveRecordMetadataProvider + .getAnnotationValues(mockTargetEntity)) + .thenReturn(null); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsWhenGovernorPluralIsEmpty() { + // Set up + setUpMockAnnotationValues(); + setUpPlural(""); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsWhenGovernorPluralMetadataIsNull() { + setUpMockAnnotationValues(); + when(mockMetadataService.get(pluralId)).thenReturn(null); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertNull(additions); + } +} diff --git a/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/RooJpaActiveRecordTest.java b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/RooJpaActiveRecordTest.java new file mode 100644 index 000000000..3944c271e --- /dev/null +++ b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/activerecord/RooJpaActiveRecordTest.java @@ -0,0 +1,60 @@ +package org.springframework.roo.addon.jpa.activerecord; + +import static org.junit.Assert.fail; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.ObjectUtils; +import org.junit.Test; +import org.springframework.roo.addon.jpa.entity.RooJpaEntity; + +/** + * Unit test of the {@link RooJpaActiveRecord} annotation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class RooJpaActiveRecordTest { + + /** + * Asserts that a method with the same name, return type, and default value + * as the given target method exists within the given candidate methods. + * This is less strict than calling {@link Method#equals(Object)}. + * + * @param targetMethod + * @param candidateMethods + */ + private void assertMethodExists(final Method targetMethod, + final Iterable candidateMethods) { + for (final Method candidateMethod : candidateMethods) { + if (candidateMethod.getReturnType().equals( + targetMethod.getReturnType()) + && candidateMethod.getName().equals(targetMethod.getName()) + && ObjectUtils.equals(candidateMethod.getDefaultValue(), + targetMethod.getDefaultValue())) { + return; // Found a match + } + } + fail("No " + RooJpaActiveRecord.class.getSimpleName() + + " method has the signature \"" + + targetMethod.getReturnType().getSimpleName() + " " + + targetMethod.getName() + "() default " + + targetMethod.getDefaultValue() + "\""); + } + + /* + * Since annotations can't share code with each other (e.g. by inheritance), + * we have manually redeclared all of the RooJpaEntity methods in the + * RooEntity annotation. This test ensures that we haven't missed any out. + */ + @Test + public void testAllRooJpaEntityMethodsExistInRooEntity() { + final List rooEntityMethods = Arrays + .asList(RooJpaActiveRecord.class.getMethods()); + for (final Method jpaEntityMethod : RooJpaEntity.class.getMethods()) { + assertMethodExists(jpaEntityMethod, rooEntityMethods); + } + } +} diff --git a/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/entity/JpaEntityAnnotationValuesTest.java b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/entity/JpaEntityAnnotationValuesTest.java new file mode 100644 index 000000000..b67c6640e --- /dev/null +++ b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/entity/JpaEntityAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.jpa.entity; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link JpaEntityAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JpaEntityAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooJpaEntity.class; + } + + @Override + protected Class getValuesClass() { + return JpaEntityAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/identifier/IdentifierAnnotationValuesTest.java b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/identifier/IdentifierAnnotationValuesTest.java new file mode 100644 index 000000000..e7de50b2d --- /dev/null +++ b/addon-jpa/src/test/java/org/springframework/roo/addon/jpa/identifier/IdentifierAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.jpa.identifier; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link IdentifierAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class IdentifierAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooIdentifier.class; + } + + @Override + protected Class getValuesClass() { + return IdentifierAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-jsf/pom.xml b/addon-jsf/pom.xml new file mode 100644 index 000000000..46e49ba75 --- /dev/null +++ b/addon-jsf/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.jsf + bundle + Spring Roo - Addon - JSF/PrimeFaces + Support for UI scaffolding using JSF/PrimeFaces + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.finder + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.addon.json + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-jsf/src/main/assembly/assembly.xml b/addon-jsf/src/main/assembly/assembly.xml new file mode 100644 index 000000000..8b85e1ae0 --- /dev/null +++ b/addon-jsf/src/main/assembly/assembly.xml @@ -0,0 +1,74 @@ + + + + zip + + true + + + + / + + unix + true + + readme.txt + + + + /legal + legal + unix + true + + *.txt + *.TXT + + + + /dist + target + true + + *.jar + + + *-tests.jar + *-sources.jar + + + + /src + target + true + + *-tests.jar + *-sources.jar + + + + /src + + true + + pom.xml + + + + + + + lib + false + runtime + false + true + false + true + + + + diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfCommands.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfCommands.java new file mode 100644 index 000000000..8ef0f9f30 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfCommands.java @@ -0,0 +1,73 @@ +package org.springframework.roo.addon.jsf; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; +import static org.springframework.roo.shell.OptionContexts.UPDATE; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the JSF add-on to be used by the ROO shell. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class JsfCommands implements CommandMarker { + + @Reference private JsfOperations jsfOperations; + + @CliAvailabilityIndicator({ "web jsf all", "web jsf scaffold", + "web jsf media" }) + public boolean isJsfInstalled() { + return jsfOperations.isScaffoldOrMediaAdditionAvailable(); + } + + @CliAvailabilityIndicator({ "web jsf setup" }) + public boolean isJsfSetupAvailable() { + return jsfOperations.isJsfInstallationPossible(); + } + + @CliCommand(value = "web jsf all", help = "Create JSF managed beans for all entities") + public void webJsfAll( + @CliOption(key = "package", mandatory = true, optionContext = UPDATE, help = "The package in which new JSF managed beans will be placed") final JavaPackage destinationPackage) { + + jsfOperations.generateAll(destinationPackage); + } + + @CliCommand(value = "web jsf media", help = "Add a cross-browser generic player to embed multimedia content") + public void webJsfMedia( + @CliOption(key = "url", mandatory = true, help = "The url of the media source") final String url, + @CliOption(key = "player", mandatory = false, help = "The name of the media player") final MediaPlayer mediaPlayer) { + + jsfOperations.addMediaSuurce(url, mediaPlayer); + } + + @CliCommand(value = "web jsf scaffold", help = "Create JSF managed bean for an entity") + public void webJsfScaffold( + @CliOption(key = { "class", "" }, mandatory = true, help = "The path and name of the JSF managed bean to be created") final JavaType managedBean, + @CliOption(key = "entity", mandatory = false, unspecifiedDefaultValue = "*", optionContext = PROJECT, help = "The entity which this JSF managed bean class will create and modify as required") final JavaType entity, + @CliOption(key = "beanName", mandatory = false, help = "The name of the managed bean to use in the 'name' attribute of the @ManagedBean annotation") final String beanName, + @CliOption(key = "includeOnMenu", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "true", help = "Include this entity on the generated JSF menu") final boolean includeOnMenu) { + + jsfOperations.createManagedBean(managedBean, entity, beanName, + includeOnMenu); + } + + @CliCommand(value = "web jsf setup", help = "Set up JSF environment") + public void webJsfSetup( + @CliOption(key = "implementation", mandatory = false, help = "The JSF implementation to use") final JsfImplementation jsfImplementation, + @CliOption(key = "library", mandatory = false, help = "The JSF component library to use") final JsfLibrary jsfLibrary, + @CliOption(key = "theme", mandatory = false, help = "The name of the theme") final Theme theme) { + + jsfOperations.setup(jsfImplementation, jsfLibrary, theme); + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfImplementation.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfImplementation.java new file mode 100644 index 000000000..c40de6e15 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfImplementation.java @@ -0,0 +1,16 @@ +package org.springframework.roo.addon.jsf; + +/** + * The JSF implementation. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public enum JsfImplementation { + APACHE_MYFACES, ORACLE_MOJARRA; + + public String getConfigPrefix() { + return "/configuration/jsf-implementations/jsf-implementation[@id='" + + name() + "']"; + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfJavaType.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfJavaType.java new file mode 100644 index 000000000..e019b4c2f --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfJavaType.java @@ -0,0 +1,124 @@ +package org.springframework.roo.addon.jsf; + +import org.springframework.roo.model.JavaType; + +/** + * Constants for JSF/PrimeFaces-specific {@link JavaType}s. Use them in + * preference to creating new instances of these types. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JsfJavaType { + + // javax.faces + public static final JavaType APPLICATION = new JavaType( + "javax.faces.application.Application"); + public static final JavaType APPLICATION_SCOPED = new JavaType( + "javax.faces.bean.ApplicationScoped"); + + public static final JavaType CONVERTER = new JavaType( + "javax.faces.convert.Converter"); + public static final JavaType DATE_TIME_CONVERTER = new JavaType( + "javax.faces.convert.DateTimeConverter"); + + // General + public static final String DISPLAY_CREATE_DIALOG = "displayCreateDialog"; + public static final String DISPLAY_LIST = "displayList"; + public static final JavaType DOUBLE_RANGE_VALIDATOR = new JavaType( + "javax.faces.validator.DoubleRangeValidator"); + // javax.el + public static final JavaType EL_CONTEXT = new JavaType("javax.el.ELContext"); + public static final JavaType ENUM_CONVERTER = new JavaType( + "javax.faces.convert.EnumConverter"); + public static final JavaType EXPRESSION_FACTORY = new JavaType( + "javax.el.ExpressionFactory"); + public static final JavaType FACES_CONTEXT = new JavaType( + "javax.faces.context.FacesContext"); + public static final JavaType FACES_CONVERTER = new JavaType( + "javax.faces.convert.FacesConverter"); + public static final JavaType FACES_MESSAGE = new JavaType( + "javax.faces.application.FacesMessage"); + public static final JavaType HTML_OUTPUT_TEXT = new JavaType( + "javax.faces.component.html.HtmlOutputText"); + public static final JavaType HTML_PANEL_GRID = new JavaType( + "javax.faces.component.html.HtmlPanelGrid"); + public static final JavaType LENGTH_VALIDATOR = new JavaType( + "javax.faces.validator.LengthValidator"); + public static final JavaType LONG_RANGE_VALIDATOR = new JavaType( + "javax.faces.validator.LongRangeValidator"); + public static final JavaType MANAGED_BEAN = new JavaType( + "javax.faces.bean.ManagedBean"); + // org.primefaces + public static final JavaType PRIMEFACES_AUTO_COMPLETE = new JavaType( + "org.primefaces.component.autocomplete.AutoComplete"); + public static final JavaType PRIMEFACES_CALENDAR = new JavaType( + "org.primefaces.component.calendar.Calendar"); + public static final JavaType PRIMEFACES_CLOSE_EVENT = new JavaType( + "org.primefaces.event.CloseEvent"); + public static final JavaType PRIMEFACES_COMMAND_BUTTON = new JavaType( + "org.primefaces.component.commandbutton.CommandButton"); + public static final JavaType PRIMEFACES_DEFAULT_MENU_MODEL = new JavaType( + "org.primefaces.model.DefaultMenuModel"); + public static final JavaType PRIMEFACES_DEFAULT_STREAMED_CONTENT = new JavaType( + "org.primefaces.model.DefaultStreamedContent"); + public static final JavaType PRIMEFACES_FILE_DOWNLOAD_ACTION_LISTENER = new JavaType( + "org.primefaces.component.filedownload.FileDownloadActionListener"); + + public static final JavaType PRIMEFACES_FILE_UPLOAD = new JavaType( + "org.primefaces.component.fileupload.FileUpload"); + public static final JavaType PRIMEFACES_FILE_UPLOAD_EVENT = new JavaType( + "org.primefaces.event.FileUploadEvent"); + public static final JavaType PRIMEFACES_INPUT_TEXT = new JavaType( + "org.primefaces.component.inputtext.InputText"); + public static final JavaType PRIMEFACES_INPUT_TEXTAREA = new JavaType( + "org.primefaces.component.inputtextarea.InputTextarea"); + public static final JavaType PRIMEFACES_KEYBOARD = new JavaType( + "org.primefaces.component.keyboard.Keyboard"); + public static final JavaType PRIMEFACES_MENU_ITEM = new JavaType( + "org.primefaces.component.menuitem.MenuItem"); + public static final JavaType PRIMEFACES_MENU_MODEL = new JavaType( + "org.primefaces.model.MenuModel"); + public static final JavaType PRIMEFACES_MESSAGE = new JavaType( + "org.primefaces.component.message.Message"); + public static final JavaType PRIMEFACES_OUTPUT_LABEL = new JavaType( + "org.primefaces.component.outputlabel.OutputLabel"); + public static final JavaType PRIMEFACES_REQUEST_CONTEXT = new JavaType( + "org.primefaces.context.RequestContext"); + public static final JavaType PRIMEFACES_SELECT_BOOLEAN_CHECKBOX = new JavaType( + "org.primefaces.component.selectbooleancheckbox.SelectBooleanCheckbox"); + public static final JavaType PRIMEFACES_SELECT_MANY_MENU = new JavaType( + "org.primefaces.component.selectmanymenu.SelectManyMenu"); + public static final JavaType PRIMEFACES_SELECT_ONE_LISTBOX = new JavaType( + "org.primefaces.component.selectonelistbox.SelectOneListbox"); + public static final JavaType PRIMEFACES_SLIDER = new JavaType( + "org.primefaces.component.slider.Slider"); + public static final JavaType PRIMEFACES_SPINNER = new JavaType( + "org.primefaces.component.spinner.Spinner"); + public static final JavaType PRIMEFACES_STREAMED_CONTENT = new JavaType( + "org.primefaces.model.StreamedContent"); + public static final JavaType PRIMEFACES_SUB_MENU = new JavaType( + "org.primefaces.component.submenu.Submenu"); + public static final JavaType PRIMEFACES_UPLOADED_FILE = new JavaType( + "org.primefaces.model.UploadedFile"); + public static final JavaType REGEX_VALIDATOR = new JavaType( + "javax.faces.validator.RegexValidator"); + public static final JavaType REQUEST_SCOPED = new JavaType( + "javax.faces.bean.RequestScoped"); + public static final JavaType SESSION_SCOPED = new JavaType( + "javax.faces.bean.SessionScoped"); + public static final JavaType UI_COMPONENT = new JavaType( + "javax.faces.component.UIComponent"); + public static final JavaType UI_SELECT_ITEM = new JavaType( + "javax.faces.component.UISelectItem"); + public static final JavaType UI_SELECT_ITEMS = new JavaType( + "javax.faces.component.UISelectItems"); + public static final JavaType VIEW_SCOPED = new JavaType( + "javax.faces.bean.ViewScoped"); + + /** + * Constructor is private to prevent instantiation + */ + private JsfJavaType() { + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfLibrary.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfLibrary.java new file mode 100644 index 000000000..68166b8bf --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfLibrary.java @@ -0,0 +1,15 @@ +package org.springframework.roo.addon.jsf; + +/** + * The JSF component library. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public enum JsfLibrary { + PRIMEFACES; + + public String getConfigPrefix() { + return "/configuration/jsf-libraries/jsf-library[@id='" + name() + "']"; + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfOperations.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfOperations.java new file mode 100644 index 000000000..fc3809e64 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfOperations.java @@ -0,0 +1,28 @@ +package org.springframework.roo.addon.jsf; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Feature; + +/** + * Provides JSF managed-bean operations. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface JsfOperations extends Feature { + + void addMediaSuurce(String url, MediaPlayer mediaPlayer); + + void createManagedBean(JavaType managedBean, JavaType entity, + String beanName, boolean includeOnMenu); + + void generateAll(JavaPackage destinationPackage); + + boolean isJsfInstallationPossible(); + + boolean isScaffoldOrMediaAdditionAvailable(); + + void setup(JsfImplementation jsfImplementation, JsfLibrary jsfLibrary, + Theme theme); +} \ No newline at end of file diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfOperationsImpl.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfOperationsImpl.java new file mode 100644 index 000000000..1ed957cb3 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/JsfOperationsImpl.java @@ -0,0 +1,979 @@ +package org.springframework.roo.addon.jsf; + +import static org.springframework.roo.model.RooJavaType.ROO_JSF_CONVERTER; +import static org.springframework.roo.model.RooJavaType.ROO_JSF_MANAGED_BEAN; +import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.operations.AbstractOperations; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.ProjectType; +import org.springframework.roo.project.Repository; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.WebXmlUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link JsfOperations}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class JsfOperationsImpl extends AbstractOperations implements + JsfOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JsfOperationsImpl.class); + + private static final String DEPENDENCY_XPATH = "/dependencies/dependency"; + private static final String JSF_IMPLEMENTATION_XPATH = "/configuration/jsf-implementations/jsf-implementation"; + private static final String JSF_LIBRARY_XPATH = "/configuration/jsf-libraries/jsf-library"; + private static final String MYFACES_LISTENER = "org.apache.myfaces.webapp.StartupServletContextListener"; + private static final String MOJARRA_LISTENER = "com.sun.faces.config.ConfigureListener"; + private static final String PRIMEFACES_THEMES_VERSION = "1.0.10"; + private static final String REPOSITORY_XPATH = "/repositories/repository"; + + private MetadataDependencyRegistry metadataDependencyRegistry; + private MetadataService metadataService; + private PathResolver pathResolver; + private ProjectOperations projectOperations; + private Shell shell; + private TypeLocationService typeLocationService; + private TypeManagementService typeManagementService; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + public void addMediaSuurce(final String url, MediaPlayer mediaPlayer) { + + Validate.notBlank(url, "Media source url required"); + + final String mainPage = getProjectOperations().getPathResolver() + .getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, "pages/main.xhtml"); + final Document document = XmlUtils.readXml(fileManager + .getInputStream(mainPage)); + final Element root = document.getDocumentElement(); + final Element element = DomUtils + .findFirstElementByName("p:panel", root); + if (element == null) { + return; + } + + if (mediaPlayer == null) { + mp: for (final MediaPlayer mp : MediaPlayer.values()) { + for (final String mediaType : mp.getMediaTypes()) { + if (StringUtils.lowerCase(url).contains(mediaType)) { + mediaPlayer = mp; + break mp; + } + } + } + } + + if (url.contains("youtube")) { + mediaPlayer = MediaPlayer.FLASH; + } + + final Element paraElement = new XmlElementBuilder("p", document) + .build(); + Element mediaElement; + if (mediaPlayer == null) { + mediaElement = new XmlElementBuilder("p:media", document) + .addAttribute("value", url).build(); + } + else { + mediaElement = new XmlElementBuilder("p:media", document) + .addAttribute("value", url) + .addAttribute("player", + StringUtils.lowerCase(mediaPlayer.name())).build(); + } + paraElement.appendChild(mediaElement); + element.appendChild(paraElement); + + fileManager.createOrUpdateTextFileIfRequired(mainPage, + XmlUtils.nodeToString(document), false); + } + + private void addOrRemoveListener(final JsfImplementation jsfImplementation, + final Document document) { + Validate.notNull(jsfImplementation, "JSF implementation required"); + Validate.notNull(document, "web.xml document required"); + + final Element root = document.getDocumentElement(); + final Element webAppElement = XmlUtils.findFirstElement("/web-app", + root); + Element listenerElement; + switch (jsfImplementation) { + case ORACLE_MOJARRA: + listenerElement = XmlUtils.findFirstElement( + "listener[listener-class = '" + MYFACES_LISTENER + "']", + webAppElement); + if (listenerElement != null) { + webAppElement.removeChild(listenerElement); + DomUtils.removeTextNodes(webAppElement); + } + listenerElement = XmlUtils.findFirstElement( + "listener[listener-class = '" + MOJARRA_LISTENER + "']", + webAppElement); + if (listenerElement == null) { + WebXmlUtils.addListener(MOJARRA_LISTENER, document, ""); + DomUtils.removeTextNodes(webAppElement); + } + break; + case APACHE_MYFACES: + listenerElement = XmlUtils.findFirstElement( + "listener[listener-class = '" + MOJARRA_LISTENER + "']", + webAppElement); + if (listenerElement != null) { + webAppElement.removeChild(listenerElement); + DomUtils.removeTextNodes(webAppElement); + } + listenerElement = XmlUtils.findFirstElement( + "listener[listener-class = '" + MYFACES_LISTENER + "']", + webAppElement); + if (listenerElement == null) { + WebXmlUtils.addListener(MYFACES_LISTENER, document, ""); + DomUtils.removeTextNodes(webAppElement); + } + break; + } + } + + private void changePrimeFacesTheme(final Theme theme, + final Document document) { + + Validate.notNull(theme, "Theme required"); + Validate.notNull(document, "web.xml document required"); + + // Add theme to the pom if not already there + final String themeName = StringUtils.lowerCase(theme.name().replace( + "_", "-")); + getProjectOperations().addDependency( + getProjectOperations().getFocusedModuleName(), + "org.primefaces.themes", themeName, PRIMEFACES_THEMES_VERSION); + + // Update the web.xml primefaces.THEME content-param + final Element root = document.getDocumentElement(); + + final Element contextParamElement = XmlUtils + .findFirstElement( + "/web-app/context-param[param-name = 'primefaces.THEME']", + root); + Validate.notNull(contextParamElement, + "The web.xml primefaces.THEME context param element required"); + final Element paramValueElement = XmlUtils.findFirstElement( + "param-value", contextParamElement); + Validate.notNull(paramValueElement, + "primefaces.THEME param-value element required"); + paramValueElement.setTextContent(themeName); + } + + private void copyEntityTypePage(final JavaType entity, + final String beanName, final String plural) { + + final String domainTypeFile = getProjectOperations().getPathResolver() + .getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, + "pages/" + + JavaSymbolName + .getReservedWordSafeName(entity) + + ".xhtml"); + InputStream inputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "pages/content-template.xhtml"); + String input = IOUtils.toString(inputStream); + input = input.replace("__BEAN_NAME__", beanName); + input = input + .replace("__DOMAIN_TYPE__", entity.getSimpleTypeName()); + input = input.replace("__LC_DOMAIN_TYPE__", JavaSymbolName + .getReservedWordSafeName(entity).getSymbolName()); + input = input.replace("__DOMAIN_TYPE_PLURAL__", plural); + + fileManager.createOrUpdateTextFileIfRequired(domainTypeFile, input, + false); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to create '" + + domainTypeFile + "'", e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + private void createConverter(final JavaPackage javaPackage, + final JavaType entity) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(shell == null){ + shell = getShell(); + } + Validate.notNull(shell, "Shell is required"); + + if(typeManagementService == null){ + typeManagementService = getTypeManagementService(); + } + Validate.notNull(typeManagementService, "TypeManagementService is required"); + + // Create type annotation for new converter class + final JavaType converterType = new JavaType( + javaPackage.getFullyQualifiedPackageName() + "." + + entity.getSimpleTypeName() + "Converter"); + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + ROO_JSF_CONVERTER); + annotationBuilder.addClassAttribute("entity", entity); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(converterType, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, converterType, + PhysicalTypeCategory.CLASS); + cidBuilder.addAnnotation(annotationBuilder); + cidBuilder.addImplementsType(JsfJavaType.CONVERTER); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + + shell.flash(Level.FINE, + "Created " + converterType.getFullyQualifiedTypeName(), + JsfOperationsImpl.class.getName()); + shell.flash(Level.FINE, "", JsfOperationsImpl.class.getName()); + } + + public void createManagedBean(final JavaType managedBean, + final JavaType entity, String beanName, final boolean includeOnMenu) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + Validate.notNull(metadataService, "MetadataService is required"); + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(shell == null){ + shell = getShell(); + } + Validate.notNull(shell, "Shell is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + if(typeManagementService == null){ + typeManagementService = getTypeManagementService(); + } + Validate.notNull(typeManagementService, "TypeManagementService is required"); + + final JavaPackage managedBeanPackage = managedBean.getPackage(); + installFacesConfig(managedBeanPackage); + installI18n(managedBeanPackage); + installBean("ApplicationBean-template.java", managedBeanPackage); + + final String managedBeanTypeName = managedBeanPackage + .getFullyQualifiedPackageName(); + final JavaPackage utilPackage = new JavaPackage(managedBeanTypeName + + ".util"); + installBean("LocaleBean-template.java", utilPackage); + installBean( + "ViewExpiredExceptionExceptionHandlerFactory-template.java", + utilPackage); + installBean("ViewExpiredExceptionExceptionHandler-template.java", + utilPackage); + installBean("MessageFactory-template.java", utilPackage); + + if (fileManager.exists(typeLocationService + .getPhysicalTypeCanonicalPath(managedBean, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)))) { + // Type exists already - nothing to do + return; + } + + final ClassOrInterfaceTypeDetails entityTypeDetails = typeLocationService + .getTypeDetails(entity); + Validate.notNull(entityTypeDetails, + "The type '%s' could not be resolved", entity); + + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(PluralMetadata.createIdentifier(entity, + PhysicalTypeIdentifier.getPath(entityTypeDetails + .getDeclaredByMetadataId()))); + Validate.notNull(pluralMetadata, + "The plural for type '%s' could not be resolved", entity); + + // Create type annotation for new managed bean + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + ROO_JSF_MANAGED_BEAN); + annotationBuilder.addClassAttribute("entity", entity); + + if (StringUtils.isBlank(beanName)) { + beanName = StringUtils + .uncapitalize(managedBean.getSimpleTypeName()); + } + annotationBuilder.addStringAttribute("beanName", beanName); + + if (!includeOnMenu) { + annotationBuilder.addBooleanAttribute("includeOnMenu", + includeOnMenu); + } + + final LogicalPath managedBeanPath = pathResolver + .getFocusedPath(Path.SRC_MAIN_JAVA); + final String resourceIdentifier = typeLocationService + .getPhysicalTypeCanonicalPath(managedBean, managedBeanPath); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(managedBean, + pathResolver.getPath(resourceIdentifier)); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, managedBean, + PhysicalTypeCategory.CLASS); + cidBuilder + .addAnnotation(new AnnotationMetadataBuilder(ROO_SERIALIZABLE)); + cidBuilder.addAnnotation(annotationBuilder); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + + shell.flash(Level.FINE, + "Created " + managedBean.getFullyQualifiedTypeName(), + JsfOperationsImpl.class.getName()); + shell.flash(Level.FINE, "", JsfOperationsImpl.class.getName()); + + copyEntityTypePage(entity, beanName, pluralMetadata.getPlural()); + + // Create a javax.faces.convert.Converter class for the entity + createConverter(new JavaPackage(managedBeanTypeName + ".converter"), + entity); + } + + private void createOrUpdateWebXml( + final JsfImplementation jsfImplementation, final Theme theme) { + + final String webXmlPath = getWebXmlFile(); + + final Document document; + if (fileManager.exists(webXmlPath)) { + document = XmlUtils.readXml(fileManager.getInputStream(webXmlPath)); + } + else { + document = getDocumentTemplate("WEB-INF/web-template.xml"); + final String projectName = getProjectOperations().getFocusedModule() + .getDisplayName(); + WebXmlUtils.setDisplayName(projectName, document, null); + WebXmlUtils.setDescription("Roo generated " + projectName + + " application", document, null); + } + + if (jsfImplementation != null) { + addOrRemoveListener(jsfImplementation, document); + } + if (theme != null) { + changePrimeFacesTheme(theme, document); + } + + fileManager.createOrUpdateTextFileIfRequired(webXmlPath, + XmlUtils.nodeToString(document), false); + } + + public void generateAll(final JavaPackage destinationPackage) { + Validate.notNull(destinationPackage, "Destination package required"); + + // Create JSF managed bean for each entity + generateManagedBeans(destinationPackage); + } + + private void generateManagedBeans(final JavaPackage destinationPackage) { + + if(metadataDependencyRegistry == null){ + metadataDependencyRegistry = getMetadataDependencyRegistry(); + } + Validate.notNull(metadataDependencyRegistry, "MetadataDependencyRegistry is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + for (final ClassOrInterfaceTypeDetails cid : typeLocationService + .findClassesOrInterfaceDetailsWithTag(CustomDataKeys.PERSISTENT_TYPE)) { + if (Modifier.isAbstract(cid.getModifier())) { + continue; + } + + final JavaType entity = cid.getName(); + final LogicalPath path = PhysicalTypeIdentifier.getPath(cid + .getDeclaredByMetadataId()); + + // Check to see if this persistent type has a JSF metadata listening + // to it + final String downstreamJsfMetadataId = JsfManagedBeanMetadata + .createIdentifier(entity, path); + if (metadataDependencyRegistry.getDownstream( + cid.getDeclaredByMetadataId()).contains( + downstreamJsfMetadataId)) { + // There is already a JSF managed bean for this entity + continue; + } + + // To get here, there is no listening managed bean, so add one + final JavaType managedBean = new JavaType( + destinationPackage.getFullyQualifiedPackageName() + "." + + entity.getSimpleTypeName() + "Bean"); + final String beanName = StringUtils.uncapitalize(managedBean + .getSimpleTypeName()); + createManagedBean(managedBean, entity, beanName, true); + } + } + + private List getDependencies(final String xPathExpression, + final Element configuration) { + final List dependencies = new ArrayList(); + for (final Element dependencyElement : XmlUtils.findElements( + xPathExpression + DEPENDENCY_XPATH, configuration)) { + dependencies.add(new Dependency(dependencyElement)); + } + return dependencies; + } + + private JsfImplementation getExistingOrDefaultJsfImplementation( + final Element configuration) { + + final Pom pom = getProjectOperations() + .getPomFromModuleName(getProjectOperations().getFocusedModuleName()); + JsfImplementation existingJsfImplementation = null; + for (final JsfImplementation value : JsfImplementation.values()) { + final Element jsfDependencyElement = XmlUtils.findFirstElement( + JSF_IMPLEMENTATION_XPATH + "[@id = '" + value.name() + "']" + + DEPENDENCY_XPATH, configuration); + if (jsfDependencyElement != null + && pom.isDependencyRegistered(new Dependency( + jsfDependencyElement))) { + existingJsfImplementation = value; + break; + } + } + return existingJsfImplementation == null ? JsfImplementation.ORACLE_MOJARRA + : existingJsfImplementation; + } + + private JsfLibrary getExistingOrDefaultJsfLibrary( + final Element configuration) { + + final Pom pom = getProjectOperations() + .getPomFromModuleName(getProjectOperations().getFocusedModuleName()); + JsfLibrary existingJsfImplementation = null; + for (final JsfLibrary value : JsfLibrary.values()) { + final Element jsfDependencyElement = XmlUtils.findFirstElement( + JSF_LIBRARY_XPATH + "[@id = '" + value.name() + "']" + + DEPENDENCY_XPATH, configuration); + if (jsfDependencyElement != null + && pom.isDependencyRegistered(new Dependency( + jsfDependencyElement))) { + existingJsfImplementation = value; + break; + } + } + return existingJsfImplementation == null ? JsfLibrary.PRIMEFACES + : existingJsfImplementation; + } + + private String getFacesConfigFile() { + return getProjectOperations().getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/faces-config.xml"); + } + + private String getJsfImplementationXPath( + final List jsfImplementations) { + final StringBuilder builder = new StringBuilder( + JSF_IMPLEMENTATION_XPATH).append("["); + for (int i = 0; i < jsfImplementations.size(); i++) { + if (i > 0) { + builder.append(" or "); + } + builder.append("@id = '"); + builder.append(jsfImplementations.get(i).name()); + builder.append("'"); + } + builder.append("]"); + return builder.toString(); + } + + private String getJsfLibraryXPath(final List jsfLibraries) { + final StringBuilder builder = new StringBuilder(JSF_LIBRARY_XPATH) + .append("["); + for (int i = 0; i < jsfLibraries.size(); i++) { + if (i > 0) { + builder.append(" or "); + } + builder.append("@id = '"); + builder.append(jsfLibraries.get(i).name()); + builder.append("'"); + } + builder.append("]"); + return builder.toString(); + } + + public String getName() { + return FeatureNames.JSF; + } + + private List getUnwantedJsfImplementations( + final JsfImplementation jsfImplementation) { + final List unwantedJsfImplementations = new ArrayList( + Arrays.asList(JsfImplementation.values())); + unwantedJsfImplementations.remove(jsfImplementation); + return unwantedJsfImplementations; + } + + private List getUnwantedJsfLibraries(final JsfLibrary jsfLibrary) { + final List unwantedJsfLibraries = new ArrayList( + Arrays.asList(JsfLibrary.values())); + unwantedJsfLibraries.remove(jsfLibrary); + return unwantedJsfLibraries; + } + + private String getWebXmlFile() { + return getProjectOperations().getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/web.xml"); + } + + private boolean hasFacesConfig() { + return fileManager.exists(getFacesConfigFile()); + } + + private void installBean(final String templateName, + final JavaPackage destinationPackage) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + if(shell == null){ + shell = getShell(); + } + Validate.notNull(shell, "Shell is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + final String beanName = templateName.substring(0, + templateName.indexOf("-template")); + final JavaType javaType = new JavaType( + destinationPackage.getFullyQualifiedPackageName() + "." + + beanName); + final String physicalTypeIdentifier = PhysicalTypeIdentifier + .createIdentifier(javaType, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + final String physicalPath = typeLocationService + .getPhysicalTypeCanonicalPath(physicalTypeIdentifier); + if (fileManager.exists(physicalPath)) { + return; + } + + InputStream inputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), templateName); + String input = IOUtils.toString(inputStream); + input = input.replace("__PACKAGE__", + destinationPackage.getFullyQualifiedPackageName()); + fileManager.createOrUpdateTextFileIfRequired(physicalPath, input, + false); + + shell.flash(Level.FINE, + "Created " + javaType.getFullyQualifiedTypeName(), + JsfOperationsImpl.class.getName()); + shell.flash(Level.FINE, "", JsfOperationsImpl.class.getName()); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to create '" + physicalPath + + "'", e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + private void installFacesConfig(final JavaPackage destinationPackage) { + Validate.isTrue(getProjectOperations().isFocusedProjectAvailable(), + "Project metadata required"); + if (hasFacesConfig()) { + return; + } + + InputStream inputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "WEB-INF/faces-config-template.xml"); + String input = IOUtils.toString(inputStream); + input = input.replace("__PACKAGE__", + destinationPackage.getFullyQualifiedPackageName()); + fileManager.createOrUpdateTextFileIfRequired(getFacesConfigFile(), + input, false); + } + catch (final IOException e) { + throw new IllegalStateException( + "Unable to create 'faces.config.xml'", e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + private void installI18n(final JavaPackage destinationPackage) { + final String packagePath = destinationPackage + .getFullyQualifiedPackageName() + .replace('.', File.separatorChar); + final String i18nDirectory = getProjectOperations().getPathResolver() + .getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, + packagePath + "/i18n"); + copyDirectoryContents("i18n/*.properties", i18nDirectory, false); + } + + public boolean isInstalledInModule(final String moduleName) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + final LogicalPath webAppPath = LogicalPath.getInstance( + Path.SRC_MAIN_WEBAPP, moduleName); + return fileManager.exists(pathResolver.getIdentifier(webAppPath, + "WEB-INF/faces-config.xml")) + || fileManager.exists(pathResolver.getIdentifier(webAppPath, + "templates/layout.xhtml")); + } + + public boolean isJsfInstallationPossible() { + return getProjectOperations().isFocusedProjectAvailable() + && !getProjectOperations() + .isFeatureInstalledInFocusedModule(FeatureNames.MVC); + } + + public boolean isScaffoldOrMediaAdditionAvailable() { + return isInstalledInModule(getProjectOperations().getFocusedModuleName()) + && fileManager.exists(getWebXmlFile()); + } + + public void setup(JsfImplementation jsfImplementation, + final JsfLibrary jsfLibrary, final Theme theme) { + + if(pathResolver == null){ + pathResolver = getPathResolver(); + } + Validate.notNull(pathResolver, "PathResolver is required"); + + jsfImplementation = updateConfiguration(jsfImplementation, jsfLibrary); + createOrUpdateWebXml(jsfImplementation, theme); + + final LogicalPath webappPath = Path.SRC_MAIN_WEBAPP + .getModulePathId(getProjectOperations().getFocusedModuleName()); + copyDirectoryContents("index.html", + pathResolver.getIdentifier(webappPath, ""), false); + copyDirectoryContents("viewExpired.xhtml", + pathResolver.getIdentifier(webappPath, ""), false); + copyDirectoryContents("resources/images/*.*", + pathResolver.getIdentifier(webappPath, "resources/images"), + false); + copyDirectoryContents("resources/css/*.css", + pathResolver.getIdentifier(webappPath, "resources/css"), false); + copyDirectoryContents("resources/js/*.js", + pathResolver.getIdentifier(webappPath, "resources/js"), false); + copyDirectoryContents("templates/*.xhtml", + pathResolver.getIdentifier(webappPath, "templates"), false); + copyDirectoryContents("pages/main.xhtml", + pathResolver.getIdentifier(webappPath, "pages"), false); + + getProjectOperations().updateProjectType( + getProjectOperations().getFocusedModuleName(), ProjectType.WAR); + + fileManager.scan(); + } + + private JsfImplementation updateConfiguration( + JsfImplementation jsfImplementation, JsfLibrary jsfLibrary) { + // Update pom.xml with JSF/Primefaces dependencies and repositories + final Element configuration = XmlUtils.getConfiguration(getClass()); + + if (jsfImplementation == null) { + // JSF implementation was not specified by user so first query POM + // to determine if there is an existing JSF dependency and use it, + // otherwise default to Oracle Mojarra + jsfImplementation = getExistingOrDefaultJsfImplementation(configuration); + } + + if (jsfLibrary == null) { + // JSF component libraru was not specified by user so first query + // POM to determine if there is an existing JSF dependency and use + // it, otherwise default to PrimeFaces + jsfLibrary = getExistingOrDefaultJsfLibrary(configuration); + } + + updateDependencies(configuration, jsfImplementation, jsfLibrary); + updateRepositories(configuration, jsfImplementation, jsfLibrary); + return jsfImplementation; + } + + private void updateDependencies(final Element configuration, + final JsfImplementation jsfImplementation, + final JsfLibrary jsfLibrary) { + + final List requiredDependencyElements = new ArrayList(); + + final List jsfImplementationDependencyElements = XmlUtils + .findElements(jsfImplementation.getConfigPrefix() + + DEPENDENCY_XPATH, configuration); + for (final Element dependencyElement : jsfImplementationDependencyElements) { + requiredDependencyElements.add(new Dependency(dependencyElement)); + } + + final List jsfLibraryDependencyElements = XmlUtils + .findElements(jsfLibrary.getConfigPrefix() + DEPENDENCY_XPATH, + configuration); + for (final Element dependencyElement : jsfLibraryDependencyElements) { + requiredDependencyElements.add(new Dependency(dependencyElement)); + } + + final List jsfDependencyElements = XmlUtils.findElements( + "/configuration/jsf" + DEPENDENCY_XPATH, configuration); + for (final Element dependencyElement : jsfDependencyElements) { + requiredDependencyElements.add(new Dependency(dependencyElement)); + } + + // Remove redundant dependencies + final List redundantDependencyElements = new ArrayList(); + + final List unwantedJsfImplementations = getUnwantedJsfImplementations(jsfImplementation); + if (!unwantedJsfImplementations.isEmpty()) { + redundantDependencyElements.addAll(getDependencies( + getJsfImplementationXPath(unwantedJsfImplementations), + configuration)); + } + + final List unwantedJsfLibraries = getUnwantedJsfLibraries(jsfLibrary); + if (!unwantedJsfLibraries.isEmpty()) { + redundantDependencyElements.addAll(getDependencies( + getJsfLibraryXPath(unwantedJsfLibraries), configuration)); + } + + // Don't remove any we actually need + redundantDependencyElements.removeAll(requiredDependencyElements); + + // Update the POM + getProjectOperations().addDependencies( + getProjectOperations().getFocusedModuleName(), + requiredDependencyElements); + getProjectOperations().removeDependencies( + getProjectOperations().getFocusedModuleName(), + redundantDependencyElements); + } + + private void updateRepositories(final Element configuration, + final JsfImplementation jsfImplementation, + final JsfLibrary jsfLibrary) { + + final List repositories = new ArrayList(); + + final List jsfRepositoryElements = XmlUtils.findElements( + jsfImplementation.getConfigPrefix() + REPOSITORY_XPATH, + configuration); + for (final Element repositoryElement : jsfRepositoryElements) { + repositories.add(new Repository(repositoryElement)); + } + + final List jsfLibraryRepositoryElements = XmlUtils + .findElements(jsfLibrary.getConfigPrefix() + REPOSITORY_XPATH, + configuration); + for (final Element repositoryElement : jsfLibraryRepositoryElements) { + repositories.add(new Repository(repositoryElement)); + } + + getProjectOperations().addRepositories( + getProjectOperations().getFocusedModuleName(), repositories); + } + + public MetadataDependencyRegistry getMetadataDependencyRegistry(){ + // Get all Services implement MetadataDependencyRegistry interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataDependencyRegistry.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataDependencyRegistry) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataDependencyRegistry on JsfOperationsImpl."); + return null; + } + } + + public MetadataService getMetadataService(){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on JsfOperationsImpl."); + return null; + } + } + + public PathResolver getPathResolver(){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on JsfOperationsImpl."); + return null; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on JsfOperationsImpl."); + return null; + } + }else{ + return projectOperations; + } + + } + + public Shell getShell(){ + // Get all Services implement Shell interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(Shell.class.getName(), null); + + for(ServiceReference ref : references){ + return (Shell) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load Shell on JsfOperationsImpl."); + return null; + } + } + + public TypeLocationService getTypeLocationService(){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on JsfOperationsImpl."); + return null; + } + } + + public TypeManagementService getTypeManagementService(){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on JsfOperationsImpl."); + return null; + } + } + +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/MediaPlayer.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/MediaPlayer.java new file mode 100644 index 000000000..b55f4d762 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/MediaPlayer.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.jsf; + +/** + * Enum representing PrimeFaces media players. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public enum MediaPlayer { + FLASH("flv", "mp3", "swf"), QUICKTIME("aif", "aiff", "aac", "au", "bmp", + "gsm", "mov", "mid", "midi", "mpg", "mpeg", "mp4", "m4a", "psd", + "qt", "qtif", "qif", "qti", "snd", "tif", "tiff", "wav", "3g2", + "3pg"), REAL("ra", "ram", "rm", "rpm", "rv", "smi", "smil"), WINDOWS( + "asx", "asf", "avi", "wma", "wmv"); + + private String[] mediaTypes; + + private MediaPlayer(final String... mediaTypes) { + this.mediaTypes = mediaTypes; + } + + public String[] getMediaTypes() { + return mediaTypes; + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/Theme.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/Theme.java new file mode 100644 index 000000000..f1328839b --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/Theme.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.jsf; + +/** + * Enum to represent PrimeFaces themes. PrimeFaces is integrated with the ThemeRoller CSS framework. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public enum Theme { + ARISTO, BLACK_TIE, BLITZER, BLUESKY, CASABLANCA, CUPERTINO, DARK_HIVE, DOT_LUV, EGGPLANT, EXCITE_BIKE, FLICK, GLASS_X, HOT_SNEAKS, HUMANITY, LE_FROG, MIDNIGHT, MINT_CHOC, OVERCAST, PEPPER_GRINDER, REDMOND, ROCKET, SMOOTHNESS, SOUTH_STREET, START, SUNNY, SWANKY_PURSE, TRONTASTIC, UI_DARKNESS, UI_LIGHTNESS, VADER; +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadata.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadata.java new file mode 100644 index 000000000..ae0a93b7b --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadata.java @@ -0,0 +1,282 @@ +package org.springframework.roo.addon.jsf.application; + +import static org.springframework.roo.addon.jsf.JsfJavaType.APPLICATION; +import static org.springframework.roo.addon.jsf.JsfJavaType.APPLICATION_SCOPED; +import static org.springframework.roo.addon.jsf.JsfJavaType.DISPLAY_CREATE_DIALOG; +import static org.springframework.roo.addon.jsf.JsfJavaType.DISPLAY_LIST; +import static org.springframework.roo.addon.jsf.JsfJavaType.EL_CONTEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.EXPRESSION_FACTORY; +import static org.springframework.roo.addon.jsf.JsfJavaType.FACES_CONTEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.MANAGED_BEAN; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_DEFAULT_MENU_MODEL; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_MENU_ITEM; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_MENU_MODEL; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_SUB_MENU; +import static org.springframework.roo.addon.jsf.JsfJavaType.REQUEST_SCOPED; +import static org.springframework.roo.addon.jsf.JsfJavaType.SESSION_SCOPED; +import static org.springframework.roo.addon.jsf.JsfJavaType.VIEW_SCOPED; +import static org.springframework.roo.model.JdkJavaType.POST_CONSTRUCT; +import static org.springframework.roo.model.RooJavaType.ROO_JSF_MANAGED_BEAN; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooJsfApplicationBean}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JsfApplicationBeanMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String CREATE_ICON = "ui-icon ui-icon-document"; + private static final String LIST_ICON = "ui-icon ui-icon-folder-open"; + private static final JavaSymbolName MENU_MODEL = new JavaSymbolName( + "menuModel"); + private static final String PROVIDES_TYPE_STRING = JsfApplicationBeanMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private Set managedBeans; + + public JsfApplicationBeanMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final Set managedBeans, + final String projectName) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(managedBeans, "Managed beans required"); + Validate.notBlank(projectName, "Project name required"); + + if (!isValid()) { + return; + } + + if (managedBeans.isEmpty()) { + valid = false; + return; + } + + this.managedBeans = managedBeans; + + // Add @ManagedBean annotation if required + builder.addAnnotation(getManagedBeanAnnotation()); + + // Add @SessionScoped annotation if required + builder.addAnnotation(getScopeAnnotation()); + + // Add menu model field + builder.addField(getField(MENU_MODEL, PRIMEFACES_MENU_MODEL)); + + // Add init() method + builder.addMethod(getInitMethod()); + + // Add model field accessor method + builder.addMethod(getAccessorMethod(MENU_MODEL, PRIMEFACES_MENU_MODEL)); + + // Add application name accessor method + builder.addMethod(getMethod( + Modifier.PUBLIC, + new JavaSymbolName("getAppName"), + JavaType.STRING, + null, + null, + InvocableMemberBodyBuilder.getInstance().appendFormalLine( + "return \"" + StringUtils.capitalize(projectName) + + "\";"))); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private MethodMetadataBuilder getInitMethod() { + final JavaSymbolName methodName = new JavaSymbolName("init"); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(EL_CONTEXT, + APPLICATION, EXPRESSION_FACTORY, FACES_CONTEXT, + PRIMEFACES_MENU_ITEM, PRIMEFACES_SUB_MENU, + PRIMEFACES_DEFAULT_MENU_MODEL); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + bodyBuilder + .appendFormalLine("FacesContext facesContext = FacesContext.getCurrentInstance();"); + bodyBuilder + .appendFormalLine("Application application = facesContext.getApplication();"); + bodyBuilder + .appendFormalLine("ExpressionFactory expressionFactory = application.getExpressionFactory();"); + bodyBuilder + .appendFormalLine("ELContext elContext = facesContext.getELContext();"); + bodyBuilder.appendFormalLine(""); + + bodyBuilder.appendFormalLine("menuModel = new DefaultMenuModel();"); + bodyBuilder.appendFormalLine("Submenu submenu;"); + bodyBuilder.appendFormalLine("MenuItem item;"); + + for (final ClassOrInterfaceTypeDetails managedBean : managedBeans) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(managedBean.getAnnotations(), + ROO_JSF_MANAGED_BEAN); + if (annotation == null) { + continue; + } + + final AnnotationAttributeValue includeOnMenuAttributeValue = annotation + .getAttribute(new JavaSymbolName("includeOnMenu")); + if (includeOnMenuAttributeValue != null + && !((Boolean) includeOnMenuAttributeValue.getValue()) + .booleanValue()) { + continue; + } + + final AnnotationAttributeValue entityAttributeValue = annotation + .getAttribute(new JavaSymbolName("entity")); + final JavaType entity = (JavaType) entityAttributeValue.getValue(); + final String entityLabel = entity.getSimpleTypeName().length() > 26 ? entity + .getSimpleTypeName().substring(0, 23) + "..." + : entity.getSimpleTypeName(); + + final AnnotationAttributeValue beanNameAttributeValue = annotation + .getAttribute(new JavaSymbolName("beanName")); + final String beanName = (String) beanNameAttributeValue.getValue(); + + bodyBuilder.appendFormalLine(""); + bodyBuilder.appendFormalLine("submenu = new Submenu();"); + bodyBuilder.appendFormalLine("submenu.setId(\"" + + StringUtils.uncapitalize(entity.getSimpleTypeName()) + + "Submenu\");"); + bodyBuilder.appendFormalLine("submenu.setLabel(\"" + entityLabel + + "\");"); + + bodyBuilder.appendFormalLine("item = new MenuItem();"); + bodyBuilder.appendFormalLine("item.setId(\"create" + + entity.getSimpleTypeName() + "MenuItem\");"); + bodyBuilder + .appendFormalLine("item.setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{messages.label_create}\", String.class));"); + bodyBuilder + .appendFormalLine("item.setActionExpression(expressionFactory.createMethodExpression(elContext, \"#{" + + beanName + + "." + + DISPLAY_CREATE_DIALOG + + "}\", String.class, new Class[0]));"); + bodyBuilder.appendFormalLine("item.setIcon(\"" + CREATE_ICON + + "\");"); + bodyBuilder.appendFormalLine("item.setAjax(false);"); + bodyBuilder.appendFormalLine("item.setAsync(false);"); + bodyBuilder.appendFormalLine("item.setUpdate(\":dataForm:data\");"); + bodyBuilder.appendFormalLine("submenu.getChildren().add(item);"); + + bodyBuilder.appendFormalLine("item = new MenuItem();"); + bodyBuilder.appendFormalLine("item.setId(\"list" + + entity.getSimpleTypeName() + "MenuItem\");"); + bodyBuilder + .appendFormalLine("item.setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{messages.label_list}\", String.class));"); + bodyBuilder + .appendFormalLine("item.setActionExpression(expressionFactory.createMethodExpression(elContext, \"#{" + + beanName + + "." + + DISPLAY_LIST + + "}\", String.class, new Class[0]));"); + bodyBuilder + .appendFormalLine("item.setIcon(\"" + LIST_ICON + "\");"); + bodyBuilder.appendFormalLine("item.setAjax(false);"); + bodyBuilder.appendFormalLine("item.setAsync(false);"); + bodyBuilder.appendFormalLine("item.setUpdate(\":dataForm:data\");"); + bodyBuilder.appendFormalLine("submenu.getChildren().add(item);"); + + bodyBuilder.appendFormalLine("menuModel.addSubmenu(submenu);"); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + new ArrayList(), + new ArrayList(), bodyBuilder); + methodBuilder.addAnnotation(new AnnotationMetadataBuilder( + POST_CONSTRUCT)); + return methodBuilder; + } + + private AnnotationMetadata getManagedBeanAnnotation() { + return getTypeAnnotation(MANAGED_BEAN); + } + + private AnnotationMetadata getScopeAnnotation() { + if (hasScopeAnnotation()) { + return null; + } + return new AnnotationMetadataBuilder(REQUEST_SCOPED).build(); + } + + private boolean hasScopeAnnotation() { + return governorTypeDetails.getAnnotation(SESSION_SCOPED) != null + || governorTypeDetails.getAnnotation(VIEW_SCOPED) != null + || governorTypeDetails.getAnnotation(REQUEST_SCOPED) != null + || governorTypeDetails.getAnnotation(APPLICATION_SCOPED) != null; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadataProvider.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadataProvider.java new file mode 100644 index 000000000..10550f2d6 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.jsf.application; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link JsfApplicationBeanMetadata}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface JsfApplicationBeanMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadataProviderImpl.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadataProviderImpl.java new file mode 100644 index 000000000..1d6ad183f --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/JsfApplicationBeanMetadataProviderImpl.java @@ -0,0 +1,194 @@ +package org.springframework.roo.addon.jsf.application; + +import static org.springframework.roo.model.RooJavaType.ROO_JSF_APPLICATION_BEAN; +import static org.springframework.roo.model.RooJavaType.ROO_JSF_MANAGED_BEAN; + +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link JsfApplicationBeanMetadataProvider}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class JsfApplicationBeanMetadataProviderImpl extends + AbstractItdMetadataProvider implements + JsfApplicationBeanMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JsfApplicationBeanMetadataProviderImpl.class); + + private ConfigurableMetadataProvider configurableMetadataProvider; + private ProjectOperations projectOperations; + + // Stores the MID (as accepted by this JsfApplicationBeanMetadataProvider) + // for the one (and only one) application-wide menu bean + private String applicationBeanMid; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getMetadataDependencyRegistry().registerDependency( + JsfManagedBeanMetadata.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_JSF_APPLICATION_BEAN); + getConfigurableMetadataProvider() + .addMetadataTrigger(ROO_JSF_APPLICATION_BEAN); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return JsfApplicationBeanMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getMetadataDependencyRegistry().deregisterDependency( + JsfManagedBeanMetadata.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_JSF_APPLICATION_BEAN); + getConfigurableMetadataProvider() + .removeMetadataTrigger(ROO_JSF_APPLICATION_BEAN); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = JsfApplicationBeanMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JsfApplicationBeanMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "ApplicationBean"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + applicationBeanMid = metadataIdentificationString; + + // To get here we know the governor is the MenuBean so let's go ahead + // and create its ITD + final Set managedBeans = getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(ROO_JSF_MANAGED_BEAN); + for (final ClassOrInterfaceTypeDetails managedBean : managedBeans) { + getMetadataDependencyRegistry().registerDependency( + managedBean.getDeclaredByMetadataId(), + metadataIdentificationString); + } + + final ProjectMetadata projectMetadata = projectOperations + .getFocusedProjectMetadata(); + Validate.notNull(projectMetadata, "Project metadata required"); + + return new JsfApplicationBeanMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, managedBeans, + projectMetadata.getPom().getDisplayName()); + } + + public String getProvidesType() { + return JsfApplicationBeanMetadata.getMetadataIdentiferType(); + } + + @Override + protected String resolveDownstreamDependencyIdentifier( + final String upstreamDependency) { + if (MetadataIdentificationUtils.getMetadataClass(upstreamDependency) + .equals(MetadataIdentificationUtils + .getMetadataClass(JsfManagedBeanMetadata + .getMetadataIdentiferType()))) { + // A JsfManagedBeanMetadata upstream MID has changed or become + // available for the first time + // It's OK to return null if we don't yet know the MID because its + // JavaType has never been found + return applicationBeanMid; + } + + // It wasn't a JsfManagedBeanMetadata, so we can let the superclass + // handle it + // (it's expected it would be a PhysicalTypeIdentifier notification, as + // that's the only other thing we registered to receive) + return super.resolveDownstreamDependencyIdentifier(upstreamDependency); + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on JsfApplicationBeanMetadataProviderImpl."); + return null; + } + }else{ + return configurableMetadataProvider; + } + + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on JsfApplicationBeanMetadataProviderImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/RooJsfApplicationBean.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/RooJsfApplicationBean.java new file mode 100644 index 000000000..f9d6aadff --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/application/RooJsfApplicationBean.java @@ -0,0 +1,17 @@ +package org.springframework.roo.addon.jsf.application; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires ROO JSF application bean support. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJsfApplicationBean { +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterAnnotationValues.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterAnnotationValues.java new file mode 100644 index 000000000..02525b4af --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterAnnotationValues.java @@ -0,0 +1,30 @@ +package org.springframework.roo.addon.jsf.converter; + +import static org.springframework.roo.model.RooJavaType.ROO_JSF_CONVERTER; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; + +/** + * Represents a parsed {@link RooJsfConverter} annotation. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JsfConverterAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private JavaType entity; + + public JsfConverterAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, ROO_JSF_CONVERTER); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public JavaType getEntity() { + return entity; + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadata.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadata.java new file mode 100644 index 000000000..0f77f1fb4 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadata.java @@ -0,0 +1,241 @@ +package org.springframework.roo.addon.jsf.converter; + +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.addon.jsf.JsfJavaType.CONVERTER; +import static org.springframework.roo.addon.jsf.JsfJavaType.FACES_CONTEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.FACES_CONVERTER; +import static org.springframework.roo.addon.jsf.JsfJavaType.UI_COMPONENT; +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.JavaType.STRING; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooJsfConverter}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JsfConverterMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + static final String ID_FIELD_NAME = "id"; + private static final String PROVIDES_TYPE_STRING = JsfConverterMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public JsfConverterMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final JsfConverterAnnotationValues annotationValues, + final MemberTypeAdditions findMethod, + final MethodMetadata identifierAccessor) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), + "Metadata identification string '%s' is invalid", identifier); + Validate.notNull(annotationValues, "Annotation values required"); + + if (!isValid()) { + return; + } + + if (findMethod == null || identifierAccessor == null) { + valid = false; + return; + } + + if (!isConverterInterfaceIntroduced()) { + builder.getImportRegistrationResolver().addImport(CONVERTER); + builder.addImplementsType(CONVERTER); + } + + builder.addAnnotation(getFacesConverterAnnotation()); + builder.addMethod(getGetAsObjectMethod(findMethod, identifierAccessor)); + builder.addMethod(getGetAsStringMethod(annotationValues.getEntity(), + identifierAccessor)); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private AnnotationMetadata getFacesConverterAnnotation() { + final AnnotationMetadata annotation = getTypeAnnotation(FACES_CONVERTER); + if (annotation == null) { + return null; + } + + final AnnotationMetadataBuilder annotationBuulder = new AnnotationMetadataBuilder( + annotation); + // annotationBuulder.addClassAttribute("forClass", entity); // TODO The + // forClass attribute causes issues + annotationBuulder.addStringAttribute("value", + destination.getFullyQualifiedTypeName()); + return annotationBuulder.build(); + } + + private MethodMetadataBuilder getGetAsObjectMethod( + final MemberTypeAdditions findMethod, + final MethodMetadata identifierAccessor) { + final JavaSymbolName methodName = new JavaSymbolName("getAsObject"); + final JavaType[] parameterTypes = { FACES_CONTEXT, UI_COMPONENT, STRING }; + if (governorHasMethod(methodName, parameterTypes)) { + return null; + } + + findMethod.copyAdditionsTo(builder, governorTypeDetails); + final JavaType returnType = identifierAccessor.getReturnType(); + + builder.getImportRegistrationResolver().addImports(returnType, + FACES_CONTEXT, UI_COMPONENT); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("if (value == null || value.length() == 0) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return null;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(returnType.getSimpleTypeName() + " " + + ID_FIELD_NAME + " = " + + getJavaTypeConversionString(returnType) + ";"); + bodyBuilder.appendFormalLine("return " + findMethod.getMethodCall() + + ";"); + + // Create getAsObject method + final List parameterNames = Arrays.asList( + new JavaSymbolName("context"), new JavaSymbolName("component"), + new JavaSymbolName("value")); + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, OBJECT, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getGetAsStringMethod(final JavaType entity, + final MethodMetadata identifierAccessor) { + final JavaSymbolName methodName = new JavaSymbolName("getAsString"); + final JavaType[] parameterTypes = { FACES_CONTEXT, UI_COMPONENT, OBJECT }; + if (governorHasMethod(methodName, parameterTypes)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(entity, + FACES_CONTEXT, UI_COMPONENT); + + final String simpleTypeName = entity.getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return value instanceof " + + simpleTypeName + " ? ((" + simpleTypeName + ") value)." + + identifierAccessor.getMethodName().getSymbolName() + + "().toString() : \"\";"); + + // Create getAsString method + final List parameterNames = Arrays.asList( + new JavaSymbolName("context"), new JavaSymbolName("component"), + new JavaSymbolName("value")); + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + JavaType.STRING, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private String getJavaTypeConversionString(final JavaType javaType) { + if (javaType.equals(JavaType.LONG_OBJECT) + || javaType.equals(JavaType.LONG_PRIMITIVE)) { + return "Long.parseLong(value)"; + } + else if (javaType.equals(JavaType.INT_OBJECT) + || javaType.equals(JavaType.INT_PRIMITIVE)) { + return "Integer.parseInt(value)"; + } + else if (javaType.equals(JavaType.DOUBLE_OBJECT) + || javaType.equals(JavaType.DOUBLE_PRIMITIVE)) { + return "Double.parseDouble(value)"; + } + else if (javaType.equals(JavaType.FLOAT_OBJECT) + || javaType.equals(JavaType.FLOAT_PRIMITIVE)) { + return "Float.parseFloat(value)"; + } + else if (javaType.equals(JavaType.SHORT_OBJECT) + || javaType.equals(JavaType.SHORT_PRIMITIVE)) { + return "Short.parseShort(value)"; + } + else if (javaType.equals(JavaType.BYTE_OBJECT) + || javaType.equals(JavaType.BYTE_PRIMITIVE)) { + return "Byte.parseByte(value)"; + } + else if (javaType.equals(JdkJavaType.BIG_DECIMAL)) { + return "new BigDecimal(value)"; + } + else if (javaType.equals(JdkJavaType.BIG_INTEGER)) { + return "new BigInteger(value)"; + } + else if (javaType.equals(STRING)) { + return "value"; + } + else { + return "value.toString()"; + } + } + + private boolean isConverterInterfaceIntroduced() { + return isImplementing(governorTypeDetails, CONVERTER); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadataProvider.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadataProvider.java new file mode 100644 index 000000000..f6a7003e9 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.jsf.converter; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link JsfConverterMetadata}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface JsfConverterMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadataProviderImpl.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadataProviderImpl.java new file mode 100644 index 000000000..17a04d39a --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/JsfConverterMetadataProviderImpl.java @@ -0,0 +1,231 @@ +package org.springframework.roo.addon.jsf.converter; + +import static org.springframework.roo.addon.jsf.converter.JsfConverterMetadata.ID_FIELD_NAME; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.model.RooJavaType.ROO_JSF_CONVERTER; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link JsfConverterMetadataProvider}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class JsfConverterMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + JsfConverterMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JsfConverterMetadataProviderImpl.class); + + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + private ConfigurableMetadataProvider configurableMetadataProvider; + private LayerService layerService; + private final Map converterMidToEntityMap = new LinkedHashMap(); + private final Map entityToConverterMidMap = new LinkedHashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_JSF_CONVERTER); + getConfigurableMetadataProvider().addMetadataTrigger(ROO_JSF_CONVERTER); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return JsfConverterMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_JSF_CONVERTER); + getConfigurableMetadataProvider().removeMetadataTrigger(ROO_JSF_CONVERTER); + } + + private MemberTypeAdditions getFindMethod(final JavaType entity, + final String metadataIdentificationString) { + + if(layerService == null){ + layerService = getLayerService(); + } + Validate.notNull(layerService, "LayerService is required"); + + getMetadataDependencyRegistry().registerDependency( + getTypeLocationService().getPhysicalTypeIdentifier(entity), + metadataIdentificationString); + final List idFields = getPersistenceMemberLocator() + .getIdentifierFields(entity); + if (idFields.isEmpty()) { + return null; + } + final FieldMetadata idField = idFields.get(0); + final JavaType idType = getPersistenceMemberLocator() + .getIdentifierType(entity); + if (idType == null) { + return null; + } + getMetadataDependencyRegistry() + .registerDependency(idField.getDeclaredByMetadataId(), + metadataIdentificationString); + + final MethodParameter idParameter = new MethodParameter(idType, + ID_FIELD_NAME); + return layerService.getMemberTypeAdditions( + metadataIdentificationString, FIND_METHOD.name(), entity, + idType, LAYER_POSITION, idParameter); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = JsfConverterMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JsfConverterMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Converter"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = entityToConverterMidMap.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(itdTypeDetails.getGovernor().getName()); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = entityToConverterMidMap.get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // We need to parse the annotation, which we expect to be present + final JsfConverterAnnotationValues annotationValues = new JsfConverterAnnotationValues( + governorPhysicalTypeMetadata); + final JavaType entity = annotationValues.getEntity(); + if (!annotationValues.isAnnotationFound() || entity == null) { + return null; + } + + // Remember that this entity JavaType matches up with this metadata + // identification string + // Start by clearing any previous association + final JavaType oldEntity = converterMidToEntityMap + .get(metadataIdentificationString); + if (oldEntity != null) { + entityToConverterMidMap.remove(oldEntity); + } + entityToConverterMidMap.put(entity, metadataIdentificationString); + converterMidToEntityMap.put(metadataIdentificationString, entity); + + final MemberTypeAdditions findMethod = getFindMethod(entity, + metadataIdentificationString); + final MethodMetadata identifierAccessor = getPersistenceMemberLocator() + .getIdentifierAccessor(entity); + + return new JsfConverterMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, annotationValues, + findMethod, identifierAccessor); + } + + public String getProvidesType() { + return JsfConverterMetadata.getMetadataIdentiferType(); + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on JsfConverterMetadataProviderImpl."); + return null; + } + }else{ + return configurableMetadataProvider; + } + } + + public LayerService getLayerService(){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on JsfConverterMetadataProviderImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/RooJsfConverter.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/RooJsfConverter.java new file mode 100644 index 000000000..08b5efdd9 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/converter/RooJsfConverter.java @@ -0,0 +1,19 @@ +package org.springframework.roo.addon.jsf.converter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires a ROO JSF converter. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJsfConverter { + + Class entity(); +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanAnnotationValues.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanAnnotationValues.java new file mode 100644 index 000000000..ffa2f16c9 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanAnnotationValues.java @@ -0,0 +1,48 @@ +package org.springframework.roo.addon.jsf.managedbean; + +import static org.springframework.roo.model.RooJavaType.ROO_JSF_MANAGED_BEAN; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooJsfManagedBean} annotation. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JsfManagedBeanAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String beanName; + @AutoPopulate private JavaType entity; + @AutoPopulate private boolean includeOnMenu = true; + + public JsfManagedBeanAnnotationValues( + final ClassOrInterfaceTypeDetails governorPhysicalTypeDetails) { + super(governorPhysicalTypeDetails, RooJavaType.ROO_JSF_MANAGED_BEAN); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public JsfManagedBeanAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, ROO_JSF_MANAGED_BEAN); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getBeanName() { + return beanName; + } + + public JavaType getEntity() { + return entity; + } + + public boolean isIncludeOnMenu() { + return includeOnMenu; + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadata.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadata.java new file mode 100644 index 000000000..6b91cd52e --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadata.java @@ -0,0 +1,1851 @@ +package org.springframework.roo.addon.jsf.managedbean; + +import static java.lang.reflect.Modifier.PRIVATE; +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.addon.jsf.JsfJavaType.APPLICATION; +import static org.springframework.roo.addon.jsf.JsfJavaType.APPLICATION_SCOPED; +import static org.springframework.roo.addon.jsf.JsfJavaType.DATE_TIME_CONVERTER; +import static org.springframework.roo.addon.jsf.JsfJavaType.DISPLAY_CREATE_DIALOG; +import static org.springframework.roo.addon.jsf.JsfJavaType.DISPLAY_LIST; +import static org.springframework.roo.addon.jsf.JsfJavaType.DOUBLE_RANGE_VALIDATOR; +import static org.springframework.roo.addon.jsf.JsfJavaType.EL_CONTEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.ENUM_CONVERTER; +import static org.springframework.roo.addon.jsf.JsfJavaType.EXPRESSION_FACTORY; +import static org.springframework.roo.addon.jsf.JsfJavaType.FACES_CONTEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.FACES_MESSAGE; +import static org.springframework.roo.addon.jsf.JsfJavaType.HTML_OUTPUT_TEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.HTML_PANEL_GRID; +import static org.springframework.roo.addon.jsf.JsfJavaType.LENGTH_VALIDATOR; +import static org.springframework.roo.addon.jsf.JsfJavaType.LONG_RANGE_VALIDATOR; +import static org.springframework.roo.addon.jsf.JsfJavaType.MANAGED_BEAN; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_AUTO_COMPLETE; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_CALENDAR; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_CLOSE_EVENT; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_COMMAND_BUTTON; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_DEFAULT_STREAMED_CONTENT; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_FILE_DOWNLOAD_ACTION_LISTENER; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_FILE_UPLOAD; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_FILE_UPLOAD_EVENT; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_INPUT_TEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_INPUT_TEXTAREA; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_MESSAGE; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_OUTPUT_LABEL; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_REQUEST_CONTEXT; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_SELECT_BOOLEAN_CHECKBOX; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_SELECT_MANY_MENU; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_SPINNER; +import static org.springframework.roo.addon.jsf.JsfJavaType.PRIMEFACES_STREAMED_CONTENT; +import static org.springframework.roo.addon.jsf.JsfJavaType.REGEX_VALIDATOR; +import static org.springframework.roo.addon.jsf.JsfJavaType.REQUEST_SCOPED; +import static org.springframework.roo.addon.jsf.JsfJavaType.SESSION_SCOPED; +import static org.springframework.roo.addon.jsf.JsfJavaType.UI_COMPONENT; +import static org.springframework.roo.addon.jsf.JsfJavaType.UI_SELECT_ITEM; +import static org.springframework.roo.addon.jsf.JsfJavaType.UI_SELECT_ITEMS; +import static org.springframework.roo.addon.jsf.JsfJavaType.VIEW_SCOPED; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.JavaType.BOOLEAN_OBJECT; +import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JavaType.VOID_PRIMITIVE; +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; +import static org.springframework.roo.model.JdkJavaType.BYTE_ARRAY_INPUT_STREAM; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.JdkJavaType.HASH_SET; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.POST_CONSTRUCT; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MAX; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MIN; +import static org.springframework.roo.model.Jsr303JavaType.FUTURE; +import static org.springframework.roo.model.Jsr303JavaType.MAX; +import static org.springframework.roo.model.Jsr303JavaType.MIN; +import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; +import static org.springframework.roo.model.Jsr303JavaType.PAST; +import static org.springframework.roo.model.Jsr303JavaType.PATTERN; +import static org.springframework.roo.model.Jsr303JavaType.SIZE; +import static org.springframework.roo.model.RooJavaType.ROO_UPLOADED_FILE; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.operations.jsr303.UploadedFileContentType; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooJsfManagedBean}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JsfManagedBeanMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private enum Action { + CREATE, EDIT, VIEW; + } + + static final String APPLICATION_TYPE_FIELDS_KEY = "applicationTypeFieldsKey"; + static final String APPLICATION_TYPE_KEY = "applicationTypeKey"; + private static final JavaSymbolName COLUMNS = new JavaSymbolName("columns"); + private static final JavaSymbolName CREATE_DIALOG_VISIBLE = new JavaSymbolName( + "createDialogVisible"); + static final String CRUD_ADDITIONS_KEY = "crudAdditionsKey"; + private static final JavaSymbolName DATA_VISIBLE = new JavaSymbolName( + "dataVisible"); + static final String ENUMERATED_KEY = "enumeratedKey"; + + private static final String HTML_PANEL_GRID_ID = "htmlPanelGrid"; + static final String LIST_VIEW_FIELD_KEY = "listViewFieldKey"; + private static final JavaSymbolName NAME = new JavaSymbolName("name"); + static final String PARAMETER_TYPE_KEY = "parameterTypeKey"; + static final String PARAMETER_TYPE_MANAGED_BEAN_NAME_KEY = "parameterTypeManagedBeanNameKey"; + static final String PARAMETER_TYPE_PLURAL_KEY = "parameterTypePluralKey"; + private static final String PROVIDES_TYPE_STRING = JsfManagedBeanMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private String beanName; + private final List builderFields = new ArrayList();; + private final List builderMethods = new ArrayList(); + private JavaType entity; + private JavaSymbolName entityName; + private Set locatedFields; + private String plural; + private JavaType messageFactory; + + public JsfManagedBeanMetadata( + final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final JsfManagedBeanAnnotationValues annotationValues, + final String plural, + final Map crudAdditions, + final Set locatedFields, + final MethodMetadata identifierAccessor) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), + "Metadata identification string '%s' is invalid", identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notBlank(plural, "Plural required"); + Validate.notNull(crudAdditions, "Crud additions map required"); + Validate.notNull(locatedFields, "Located fields required"); + + if (!isValid()) { + return; + } + + entity = annotationValues.getEntity(); + + final MemberTypeAdditions findAllMethod = crudAdditions + .get(FIND_ALL_METHOD); + final MemberTypeAdditions mergeMethod = crudAdditions.get(MERGE_METHOD); + final MemberTypeAdditions persistMethod = crudAdditions + .get(PERSIST_METHOD); + final MemberTypeAdditions removeMethod = crudAdditions + .get(REMOVE_METHOD); + if (identifierAccessor == null || findAllMethod == null + || mergeMethod == null || persistMethod == null + || removeMethod == null || entity == null) { + valid = false; + return; + } + + this.locatedFields = locatedFields; + beanName = annotationValues.getBeanName(); + this.plural = plural; + entityName = JavaSymbolName.getReservedWordSafeName(entity); + messageFactory = new JavaType(destination.getPackage() + .getFullyQualifiedPackageName() + ".util.MessageFactory"); + + final JavaSymbolName allEntitiesFieldName = new JavaSymbolName("all" + + plural); + final JavaType entityListType = getListType(entity); + + // Add @ManagedBean annotation if required + builder.addAnnotation(getManagedBeanAnnotation(annotationValues + .getBeanName())); + + // Add @SessionScoped annotation if required + builder.addAnnotation(getScopeAnnotation()); + + // Add builderFields + builderFields + .add(getField(PRIVATE, NAME, STRING, "\"" + plural + "\"")); + builderFields.add(getField(entityName, entity)); + builderFields.add(getField(allEntitiesFieldName, entityListType)); + builderFields.add(getField(PRIVATE, DATA_VISIBLE, BOOLEAN_PRIMITIVE, + Boolean.FALSE.toString())); + builderFields.add(getField(COLUMNS, getListType(STRING))); + builderFields.add(getPanelGridField(Action.CREATE)); + builderFields.add(getPanelGridField(Action.EDIT)); + builderFields.add(getPanelGridField(Action.VIEW)); + builderFields.add(getField(PRIVATE, CREATE_DIALOG_VISIBLE, + BOOLEAN_PRIMITIVE, Boolean.FALSE.toString())); + + // Add builderMethods + builderMethods.add(getInitMethod(identifierAccessor)); + builderMethods.add(getAccessorMethod(NAME, STRING)); + builderMethods.add(getAccessorMethod(COLUMNS, getListType(STRING))); + builderMethods.add(getAccessorMethod(allEntitiesFieldName, + entityListType)); + builderMethods.add(getMutatorMethod(allEntitiesFieldName, + entityListType)); + builderMethods.add(getFindAllEntitiesMethod(allEntitiesFieldName, + findAllMethod)); + builderMethods.add(getAccessorMethod(DATA_VISIBLE, BOOLEAN_PRIMITIVE)); + builderMethods.add(getMutatorMethod(DATA_VISIBLE, BOOLEAN_PRIMITIVE)); + builderMethods.add(getPanelGridAccessorMethod(Action.CREATE)); + builderMethods.add(getPanelGridMutatorMethod(Action.CREATE)); + builderMethods.add(getPanelGridAccessorMethod(Action.EDIT)); + builderMethods.add(getPanelGridMutatorMethod(Action.EDIT)); + builderMethods.add(getPanelGridAccessorMethod(Action.VIEW)); + builderMethods.add(getPanelGridMutatorMethod(Action.VIEW)); + builderMethods.add(getPopulatePanelMethod(Action.CREATE)); + builderMethods.add(getPopulatePanelMethod(Action.EDIT)); + builderMethods.add(getPopulatePanelMethod(Action.VIEW)); + + builderMethods.add(getEntityAccessorMethod()); + builderMethods.add(getMutatorMethod(entityName, entity)); + + addOtherFieldsAndMethods(); + + builderMethods.add(getOnEditMethod()); + builderMethods.add(getAccessorMethod(CREATE_DIALOG_VISIBLE, + BOOLEAN_PRIMITIVE)); + builderMethods.add(getMutatorMethod(CREATE_DIALOG_VISIBLE, + BOOLEAN_PRIMITIVE)); + builderMethods.add(getDisplayListMethod()); + builderMethods.add(getDisplayCreateDialogMethod()); + builderMethods.add(getPersistMethod(mergeMethod, persistMethod, + identifierAccessor)); + builderMethods.add(getDeleteMethod(removeMethod)); + builderMethods.add(getResetMethod()); + builderMethods.add(getHandleDialogCloseMethod()); + + // Add builderFields first to builder followed by builderMethods + for (final FieldMetadataBuilder fieldBuilder : builderFields) { + builder.addField(fieldBuilder); + } + for (final MethodMetadataBuilder method : builderMethods) { + builder.addMethod(method); + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private void addOtherFieldsAndMethods() { + for (final FieldMetadata field : locatedFields) { + final CustomData customData = field.getCustomData(); + + if (customData.keySet().contains(APPLICATION_TYPE_KEY)) { + builderMethods.add(getAutoCompleteApplicationTypeMethod(field)); + } + else if (customData.keySet().contains(ENUMERATED_KEY)) { + builderMethods.add(getAutoCompleteEnumMethod(field)); + } + else if (field.getCustomData().keySet() + .contains(PARAMETER_TYPE_KEY)) { + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType parameterType = (JavaType) field.getCustomData() + .get(PARAMETER_TYPE_KEY); + final JavaSymbolName selectedFieldName = new JavaSymbolName( + getSelectedFieldName(fieldName)); + final JavaType listType = getListType(parameterType); + + builderFields.add(getField(selectedFieldName, listType)); + builderMethods.add(getAccessorMethod(selectedFieldName, + listType)); + + JavaType realListType = HASH_SET; + if (listType.equals(LIST)) { + realListType = ARRAY_LIST; + } + builder.getImportRegistrationResolver().addImport(realListType); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (" + + selectedFieldName.getSymbolName() + " != null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(entityName.getSymbolName() + + ".set" + StringUtils.capitalize(fieldName) + + "(new " + realListType.getSimpleTypeName() + "<" + parameterType.getSimpleTypeName() + + ">(" + selectedFieldName + "));"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("this." + + selectedFieldName.getSymbolName() + " = " + + selectedFieldName.getSymbolName() + ";"); + builderMethods.add(getMutatorMethod(selectedFieldName, + listType, bodyBuilder)); + } + else if (field.getAnnotation(ROO_UPLOADED_FILE) != null) { + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_STREAMED_CONTENT, + PRIMEFACES_DEFAULT_STREAMED_CONTENT, + BYTE_ARRAY_INPUT_STREAM); + + final String fieldName = field.getFieldName().getSymbolName(); + final JavaSymbolName streamedContentFieldName = new JavaSymbolName( + fieldName + "StreamedContent"); + + builderMethods.add(getFileUploadListenerMethod(field)); + + final AnnotationMetadata annotation = field + .getAnnotation(ROO_UPLOADED_FILE); + final String contentType = (String) annotation.getAttribute( + "contentType").getValue(); + final String fileExtension = StringUtils + .lowerCase(UploadedFileContentType.getFileExtension( + contentType).name()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (" + + entityName.getSymbolName() + " != null && " + + entityName.getSymbolName() + ".get" + + StringUtils.capitalize(fieldName) + "() != null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("return new DefaultStreamedContent(new ByteArrayInputStream(" + + entityName.getSymbolName() + + ".get" + + StringUtils.capitalize(fieldName) + + "()), \"" + + contentType + + "\", \"" + + fieldName + + "." + + fileExtension + "\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("return new DefaultStreamedContent(new ByteArrayInputStream(\"\".getBytes()));"); + builderMethods.add(getAccessorMethod(streamedContentFieldName, + PRIMEFACES_STREAMED_CONTENT, bodyBuilder)); + } + } + } + + private String getAddChildToComponent(final String componentId, + final String childComponentId) { + return componentId + ".getChildren().add(" + childComponentId + ");"; + } + + private String getAddToPanelText(final String componentId) { + return getAddChildToComponent(HTML_PANEL_GRID_ID, componentId); + } + + private String getAllowTypeRegex(final String allowedType) { + final StringBuilder builder = new StringBuilder(); + final char[] value = allowedType.toCharArray(); + for (final char element : value) { + builder.append("[").append(Character.toLowerCase(element)) + .append(Character.toUpperCase(element)).append("]"); + } + if (allowedType.equals(UploadedFileContentType.JPG.name())) { + builder.append("|[jJ][pP][eE][gG]"); + } + return builder.toString(); + } + + private String getAutoCcompleteItemLabelValue(final FieldMetadata field, + final String fieldName) { + final StringBuilder sb = new StringBuilder(); + @SuppressWarnings("unchecked") + final List applicationTypeFields = (List) field + .getCustomData().get(APPLICATION_TYPE_FIELDS_KEY); + for (final FieldMetadata applicationTypeField : applicationTypeFields) { + sb.append("#{") + .append(fieldName) + .append(".") + .append(applicationTypeField.getFieldName().getSymbolName()) + .append("} "); + } + return sb.length() > 0 ? sb.toString().trim() : fieldName; + } + + private MethodMetadataBuilder getAutoCompleteApplicationTypeMethod( + final FieldMetadata field) { + final JavaSymbolName methodName = new JavaSymbolName("complete" + + StringUtils.capitalize(field.getFieldName().getSymbolName())); + final JavaType parameterType = STRING; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(LIST, ARRAY_LIST); + + final List parameterNames = Arrays + .asList(new JavaSymbolName("query")); + + @SuppressWarnings("unchecked") + final Map crudAdditions = (Map) field + .getCustomData().get(CRUD_ADDITIONS_KEY); + final MemberTypeAdditions findAllMethod = crudAdditions + .get(FIND_ALL_METHOD); + findAllMethod.copyAdditionsTo(builder, governorTypeDetails); + final String simpleTypeName = field.getFieldType().getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("List<" + simpleTypeName + + "> suggestions = new ArrayList<" + simpleTypeName + ">();"); + bodyBuilder.appendFormalLine("for (" + simpleTypeName + " " + + StringUtils.uncapitalize(simpleTypeName) + " : " + + findAllMethod.getMethodCall() + ") {"); + bodyBuilder.indent(); + + final StringBuilder sb = new StringBuilder(); + @SuppressWarnings("unchecked") + final List applicationTypeFields = (List) field + .getCustomData().get(APPLICATION_TYPE_FIELDS_KEY); + for (int i = 0; i < applicationTypeFields.size(); i++) { + final JavaSymbolName accessorMethodName = BeanInfoUtils + .getAccessorMethodName(applicationTypeFields.get(i)); + if (i > 0) { + sb.append(" + ").append(" \" \" ").append(" + "); + } + sb.append(StringUtils.uncapitalize(simpleTypeName)).append(".") + .append(accessorMethodName).append("()"); + } + bodyBuilder.appendFormalLine("String " + + StringUtils.uncapitalize(simpleTypeName) + + "Str = String.valueOf(" + sb.toString().trim() + ");"); + + bodyBuilder.appendFormalLine("if (" + + StringUtils.uncapitalize(simpleTypeName) + + "Str.toLowerCase().startsWith(query.toLowerCase())) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("suggestions.add(" + + StringUtils.uncapitalize(simpleTypeName) + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return suggestions;"); + + final JavaType returnType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(field.getFieldType())); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getAutoCompleteEnumMethod( + final FieldMetadata autoCompleteField) { + final JavaSymbolName methodName = new JavaSymbolName("complete" + + StringUtils.capitalize(autoCompleteField.getFieldName() + .getSymbolName())); + final JavaType parameterType = STRING; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(LIST, ARRAY_LIST); + + final List parameterNames = Arrays + .asList(new JavaSymbolName("query")); + final JavaType returnType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(autoCompleteField.getFieldType())); + + final String simpleTypeName = autoCompleteField.getFieldType() + .getSimpleTypeName(); + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("List<" + simpleTypeName + + "> suggestions = new ArrayList<" + simpleTypeName + ">();"); + bodyBuilder.appendFormalLine("for (" + simpleTypeName + " " + + StringUtils.uncapitalize(simpleTypeName) + " : " + + simpleTypeName + ".values()) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("if (" + + StringUtils.uncapitalize(simpleTypeName) + + ".name().toLowerCase().startsWith(query.toLowerCase())) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("suggestions.add(" + + StringUtils.uncapitalize(simpleTypeName) + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return suggestions;"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private Integer getColumnLength(final FieldMetadata field) { + @SuppressWarnings("unchecked") + final Map values = (Map) field + .getCustomData().get(CustomDataKeys.COLUMN_FIELD); + if (values != null && values.containsKey("length")) { + return (Integer) values.get("length"); + } + return null; + } + + private String getComponentCreation(final String componentName) { + return new StringBuilder().append("(").append(componentName) + .append(") application.createComponent(").append(componentName) + .append(".COMPONENT_TYPE);").toString(); + } + + private MethodMetadataBuilder getDeleteMethod( + final MemberTypeAdditions removeMethod) { + final JavaSymbolName methodName = new JavaSymbolName("delete"); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(FACES_MESSAGE, + FACES_CONTEXT, messageFactory); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(removeMethod.getMethodCall() + ";"); + removeMethod.copyAdditionsTo(builder, governorTypeDetails); + bodyBuilder + .appendFormalLine("FacesMessage facesMessage = MessageFactory.getMessage(\"message_successfully_deleted\", \"" + + entity.getSimpleTypeName() + "\");"); + bodyBuilder + .appendFormalLine("FacesContext.getCurrentInstance().addMessage(null, facesMessage);"); + bodyBuilder.appendFormalLine("reset();"); + bodyBuilder.appendFormalLine("return findAll" + plural + "();"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, STRING, + new ArrayList(), + new ArrayList(), bodyBuilder); + } + + private MethodMetadataBuilder getDisplayCreateDialogMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + DISPLAY_CREATE_DIALOG); + if (governorHasMethod(methodName)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName.getSymbolName() + " = new " + + entity.getSimpleTypeName() + "();"); + bodyBuilder.appendFormalLine(CREATE_DIALOG_VISIBLE + " = true;"); + bodyBuilder.appendFormalLine("return \"" + entityName.getSymbolName() + + "\";"); + return getMethod(PUBLIC, methodName, STRING, null, null, bodyBuilder); + } + + private MethodMetadataBuilder getDisplayListMethod() { + final JavaSymbolName methodName = new JavaSymbolName(DISPLAY_LIST); + if (governorHasMethod(methodName)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(CREATE_DIALOG_VISIBLE + " = false;"); + bodyBuilder.appendFormalLine("findAll" + plural + "();"); + bodyBuilder.appendFormalLine("return \"" + entityName.getSymbolName() + + "\";"); + return getMethod(PUBLIC, methodName, STRING, null, null, bodyBuilder); + } + + public String getDoubleRangeValdatorString(final String fieldValueId, + final BigDecimal minValue, final BigDecimal maxValue) { + builder.getImportRegistrationResolver().addImport( + DOUBLE_RANGE_VALIDATOR); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("DoubleRangeValidator " + fieldValueId + + "Validator = new DoubleRangeValidator();"); + if (minValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + "Validator.setMinimum(" + + minValue.doubleValue() + ");"); + } + if (maxValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + "Validator.setMaximum(" + + maxValue.doubleValue() + ");"); + } + bodyBuilder.appendFormalLine(fieldValueId + ".addValidator(" + + fieldValueId + "Validator);"); + return bodyBuilder.getOutput(); + } + + private MethodMetadataBuilder getEntityAccessorMethod() { + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (" + entityName.getSymbolName() + + " == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(entityName.getSymbolName() + " = new " + + entity.getSimpleTypeName() + "();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return " + entityName.getSymbolName() + + ";"); + return getAccessorMethod(entityName, entity, bodyBuilder); + } + + private MethodMetadataBuilder getFileUploadListenerMethod( + final FieldMetadata field) { + final String fieldName = field.getFieldName().getSymbolName(); + final JavaSymbolName methodName = getFileUploadMethodName(fieldName); + final JavaType parameterType = PRIMEFACES_FILE_UPLOAD_EVENT; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(FACES_CONTEXT, + FACES_MESSAGE, PRIMEFACES_FILE_UPLOAD_EVENT, messageFactory); + + final List parameterNames = Arrays + .asList(new JavaSymbolName("event")); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName + ".set" + + StringUtils.capitalize(fieldName) + + "(event.getFile().getContents());"); + bodyBuilder + .appendFormalLine("FacesMessage facesMessage = MessageFactory.getMessage(\"message_successfully_uploaded\", event.getFile().getFileName());"); + bodyBuilder + .appendFormalLine("FacesContext.getCurrentInstance().addMessage(null, facesMessage);"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private JavaSymbolName getFileUploadMethodName(final String fieldName) { + return new JavaSymbolName("handleFileUploadFor" + + StringUtils.capitalize(fieldName)); + } + + private MethodMetadataBuilder getFindAllEntitiesMethod( + final JavaSymbolName allEntitiesFieldName, + final MemberTypeAdditions findAllMethod) { + final JavaSymbolName methodName = new JavaSymbolName("findAll" + plural); + if (governorHasMethod(methodName)) { + return null; + } + + findAllMethod.copyAdditionsTo(builder, governorTypeDetails); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(allEntitiesFieldName.getSymbolName() + + " = " + findAllMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine(DATA_VISIBLE + " = !" + + allEntitiesFieldName.getSymbolName() + ".isEmpty();"); + bodyBuilder.appendFormalLine("return null;"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + JavaType.STRING, new ArrayList(), + new ArrayList(), bodyBuilder); + } + + private MethodMetadataBuilder getHandleDialogCloseMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + "handleDialogClose"); + final JavaType parameterType = PRIMEFACES_CLOSE_EVENT; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_CLOSE_EVENT); + + final List parameterNames = Arrays + .asList(new JavaSymbolName("event")); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("reset();"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getInitMethod( + final MethodMetadata identifierAccessor) { + final JavaSymbolName methodName = new JavaSymbolName("init"); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImport(ARRAY_LIST); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("columns = new ArrayList();"); + for (final FieldMetadata field : locatedFields) { + if (field.getCustomData().keySet().contains(LIST_VIEW_FIELD_KEY)) { + bodyBuilder.appendFormalLine("columns.add(\"" + + field.getFieldName().getSymbolName() + "\");"); + } + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + new ArrayList(), + new ArrayList(), bodyBuilder); + methodBuilder.addAnnotation(new AnnotationMetadataBuilder( + POST_CONSTRUCT)); + return methodBuilder; + } + + public String getLengthValdatorString(final String fieldValueId, + final Number minValue, final Number maxValue) { + builder.getImportRegistrationResolver().addImport(LENGTH_VALIDATOR); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("LengthValidator " + fieldValueId + + "Validator = new LengthValidator();"); + if (minValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + "Validator.setMinimum(" + + minValue.intValue() + ");"); + } + if (maxValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + "Validator.setMaximum(" + + maxValue.intValue() + ");"); + } + bodyBuilder.appendFormalLine(fieldValueId + ".addValidator(" + + fieldValueId + "Validator);"); + return bodyBuilder.getOutput(); + } + + private JavaType getListType(final JavaType parameterType) { + return new JavaType(LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(parameterType)); + } + + public String getLongRangeValdatorString(final String fieldValueId, + final BigDecimal minValue, final BigDecimal maxValue) { + builder.getImportRegistrationResolver().addImport(LONG_RANGE_VALIDATOR); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("LongRangeValidator " + fieldValueId + + "Validator = new LongRangeValidator();"); + if (minValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + "Validator.setMinimum(" + + minValue.longValue() + ");"); + } + if (maxValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + "Validator.setMaximum(" + + maxValue.longValue() + ");"); + } + bodyBuilder.appendFormalLine(fieldValueId + ".addValidator(" + + fieldValueId + "Validator);"); + return bodyBuilder.getOutput(); + } + + private AnnotationMetadata getManagedBeanAnnotation(final String beanName) { + final AnnotationMetadata annotation = getTypeAnnotation(MANAGED_BEAN); + if (annotation == null) { + return null; + } + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + annotation); + annotationBuilder.addStringAttribute("name", beanName); + return annotationBuilder.build(); + } + + private BigDecimal getMinOrMaxValue(final FieldMetadata field, + final JavaType annotationType) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), annotationType); + if (annotation != null + && annotation.getAttribute(new JavaSymbolName("value")) != null) { + return new BigDecimal(String.valueOf(annotation.getAttribute( + new JavaSymbolName("value")).getValue())); + } + return null; + } + + private MethodMetadataBuilder getOnEditMethod() { + final JavaSymbolName methodName = new JavaSymbolName("onEdit"); + if (governorHasMethod(methodName)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + for (final FieldMetadata field : locatedFields) { + final CustomData customData = field.getCustomData(); + if (!customData.keySet().contains(PARAMETER_TYPE_KEY)) { + continue; + } + + builder.getImportRegistrationResolver().addImport(ARRAY_LIST); + + final String fieldName = field.getFieldName().getSymbolName(); + final JavaType parameterType = (JavaType) customData + .get(PARAMETER_TYPE_KEY); + final String entityAccessorMethodCall = entityName.getSymbolName() + + ".get" + StringUtils.capitalize(fieldName) + "()"; + + bodyBuilder + .appendFormalLine("if (" + entityName.getSymbolName() + + " != null && " + entityAccessorMethodCall + + " != null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(getSelectedFieldName(fieldName) + + " = new ArrayList<" + parameterType.getSimpleTypeName() + + ">(" + entityAccessorMethodCall + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + bodyBuilder.appendFormalLine("return null;"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + JavaType.STRING, new ArrayList(), + new ArrayList(), bodyBuilder); + } + + private MethodMetadataBuilder getPanelGridAccessorMethod(final Action action) { + final String fieldName = StringUtils.lowerCase(action.name()) + + "PanelGrid"; + final JavaSymbolName methodName = BeanInfoUtils.getAccessorMethodName( + new JavaSymbolName(fieldName), HTML_PANEL_GRID); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImport(HTML_PANEL_GRID); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + switch (action) { + case CREATE: + bodyBuilder.appendFormalLine("if (" + fieldName + " == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + + " = populateCreatePanel();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return " + fieldName + ";"); + break; + case EDIT: + bodyBuilder.appendFormalLine("if (" + fieldName + " == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldName + " = populateEditPanel();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return " + fieldName + ";"); + break; + default: + bodyBuilder.appendFormalLine("return populateViewPanel();"); + break; + } + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + HTML_PANEL_GRID, new ArrayList(), + new ArrayList(), bodyBuilder); + } + + private FieldMetadataBuilder getPanelGridField(final Action panelType) { + return getField( + new JavaSymbolName(StringUtils.lowerCase(panelType.name()) + + "PanelGrid"), HTML_PANEL_GRID); + } + + private MethodMetadataBuilder getPanelGridMutatorMethod(final Action action) { + return getMutatorMethod( + new JavaSymbolName(StringUtils.lowerCase(action.name()) + + "PanelGrid"), HTML_PANEL_GRID); + } + + private MethodMetadataBuilder getPersistMethod( + final MemberTypeAdditions mergeMethod, + final MemberTypeAdditions persistMethod, + final MethodMetadata identifierAccessor) { + final JavaSymbolName methodName = new JavaSymbolName("persist"); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(FACES_MESSAGE, + PRIMEFACES_REQUEST_CONTEXT, FACES_CONTEXT, messageFactory); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("String message = \"\";"); + bodyBuilder.appendFormalLine("if (" + entityName.getSymbolName() + "." + + identifierAccessor.getMethodName().getSymbolName() + + "() != null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(mergeMethod.getMethodCall() + ";"); + mergeMethod.copyAdditionsTo(builder, governorTypeDetails); + bodyBuilder + .appendFormalLine("message = \"message_successfully_updated\";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";"); + persistMethod.copyAdditionsTo(builder, governorTypeDetails); + bodyBuilder + .appendFormalLine("message = \"message_successfully_created\";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("RequestContext context = RequestContext.getCurrentInstance();"); + bodyBuilder + .appendFormalLine("context.execute(\"createDialogWidget.hide()\");"); + bodyBuilder + .appendFormalLine("context.execute(\"editDialogWidget.hide()\");"); + bodyBuilder.appendFormalLine(""); + bodyBuilder + .appendFormalLine("FacesMessage facesMessage = MessageFactory.getMessage(message, \"" + + entity.getSimpleTypeName() + "\");"); + bodyBuilder + .appendFormalLine("FacesContext.getCurrentInstance().addMessage(null, facesMessage);"); + bodyBuilder.appendFormalLine("reset();"); + bodyBuilder.appendFormalLine("return findAll" + plural + "();"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, STRING, + new ArrayList(), + new ArrayList(), bodyBuilder); + } + + private MethodMetadataBuilder getPopulatePanelMethod(final Action action) { + JavaSymbolName methodName; + String suffix1; + String suffix2; + switch (action) { + case CREATE: + suffix1 = "CreateOutput"; + suffix2 = "CreateInput"; + methodName = new JavaSymbolName("populateCreatePanel"); + break; + case EDIT: + suffix1 = "EditOutput"; + suffix2 = "EditInput"; + methodName = new JavaSymbolName("populateEditPanel"); + break; + default: + suffix1 = "Label"; + suffix2 = "Value"; + methodName = new JavaSymbolName("populateViewPanel"); + break; + } + + if (governorHasMethod(methodName)) { + return null; + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId()); + methodBuilder.setModifier(PUBLIC); + methodBuilder.setMethodName(methodName); + methodBuilder.setReturnType(HTML_PANEL_GRID); + methodBuilder.setParameterTypes(new ArrayList()); + methodBuilder.setParameterNames(new ArrayList()); + + builder.getImportRegistrationResolver().addImports(FACES_CONTEXT, + HTML_PANEL_GRID); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("FacesContext facesContext = FacesContext.getCurrentInstance();"); + bodyBuilder.appendFormalLine(APPLICATION.getFullyQualifiedTypeName() + + " application = facesContext.getApplication();"); + + if (locatedFields.isEmpty()) { + bodyBuilder.appendFormalLine("return " + + getComponentCreation("HtmlPanelGrid")); + methodBuilder.setBodyBuilder(bodyBuilder); + return methodBuilder; + } + + builder.getImportRegistrationResolver().addImports(EL_CONTEXT, + EXPRESSION_FACTORY, HTML_OUTPUT_TEXT, PRIMEFACES_OUTPUT_LABEL); + + bodyBuilder + .appendFormalLine("ExpressionFactory expressionFactory = application.getExpressionFactory();"); + bodyBuilder + .appendFormalLine("ELContext elContext = facesContext.getELContext();"); + bodyBuilder.appendFormalLine(""); + bodyBuilder.appendFormalLine("HtmlPanelGrid " + HTML_PANEL_GRID_ID + + " = " + getComponentCreation("HtmlPanelGrid")); + bodyBuilder.appendFormalLine(""); + + for (final FieldMetadata field : locatedFields) { + final CustomData customData = field.getCustomData(); + final JavaType fieldType = field.getFieldType(); + final String simpleTypeName = fieldType.getSimpleTypeName(); + final String fieldName = field.getFieldName().getSymbolName(); + final String fieldLabelId = fieldName + suffix1; + final String fieldValueId = fieldName + suffix2; + + final BigDecimal minValue = ObjectUtils.max( + getMinOrMaxValue(field, MIN), + getMinOrMaxValue(field, DECIMAL_MIN)); + + final BigDecimal maxValue = ObjectUtils.min( + getMinOrMaxValue(field, MAX), + getMinOrMaxValue(field, DECIMAL_MAX)); + + final Integer sizeMinValue = getSizeMinOrMax(field, "min"); + + final Integer min = ObjectUtils.min(getSizeMinOrMax(field, "max"), + getColumnLength(field)); + final BigDecimal sizeMaxValue = min != null ? new BigDecimal(min) + : null; + + final boolean required = action != Action.VIEW + && (!isNullable(field) || minValue != null + || maxValue != null || sizeMinValue != null || sizeMaxValue != null); + final boolean isTextarea = sizeMinValue != null + && sizeMinValue.intValue() > 30 || sizeMaxValue != null + && sizeMaxValue.intValue() > 30 + || customData.keySet().contains(CustomDataKeys.LOB_FIELD); + + final boolean isUIComponent = isUIComponent(field, fieldType, + customData); + + // Field label + if (action.equals(Action.VIEW) || !isUIComponent) { + bodyBuilder.appendFormalLine("HtmlOutputText " + fieldLabelId + + " = " + getComponentCreation("HtmlOutputText")); + } + else { + bodyBuilder.appendFormalLine("OutputLabel " + fieldLabelId + + " = " + getComponentCreation("OutputLabel")); + bodyBuilder.appendFormalLine(fieldLabelId + ".setFor(\"" + + fieldValueId + "\");"); + } + bodyBuilder.appendFormalLine(fieldLabelId + ".setId(\"" + + fieldLabelId + "\");"); + bodyBuilder.appendFormalLine(fieldLabelId + ".setValue(\"" + + field.getFieldName().getReadableSymbolName() + ":\");"); + bodyBuilder.appendFormalLine(getAddToPanelText(fieldLabelId)); + bodyBuilder.appendFormalLine(""); + + // Field value + final String converterName = fieldValueId + "Converter"; + final String htmlOutputTextStr = "HtmlOutputText " + fieldValueId + + " = " + getComponentCreation("HtmlOutputText"); + final String inputTextStr = "InputText " + fieldValueId + " = " + + getComponentCreation("InputText"); + final String componentIdStr = fieldValueId + ".setId(\"" + + fieldValueId + "\");"; + final String requiredStr = fieldValueId + ".setRequired(" + + required + ");"; + + if (field.getAnnotation(ROO_UPLOADED_FILE) != null) { + final AnnotationMetadata annotation = field + .getAnnotation(ROO_UPLOADED_FILE); + final String contentType = (String) annotation.getAttribute( + "contentType").getValue(); + final String allowedType = UploadedFileContentType + .getFileExtension(contentType).name(); + if (action == Action.VIEW) { + builder.getImportRegistrationResolver().addImports( + UI_COMPONENT, + PRIMEFACES_FILE_DOWNLOAD_ACTION_LISTENER, + PRIMEFACES_COMMAND_BUTTON, + PRIMEFACES_STREAMED_CONTENT); + + // bodyBuilder.appendFormalLine("CommandButton " + + // fieldValueId + " = " + + // getComponentCreation("CommandButton")); + // bodyBuilder.appendFormalLine(fieldValueId + + // ".addActionListener(new FileDownloadActionListener(expressionFactory.createValueExpression(elContext, \"#{" + // + beanName + "." + + // fieldName + + // "StreamedContent}\", StreamedContent.class), null));"); + // bodyBuilder.appendFormalLine(fieldValueId + + // ".setValue(\"Download\");"); + // bodyBuilder.appendFormalLine(fieldValueId + + // ".setAjax(false);"); + + // TODO Make following code work as currently the view panel + // is not refreshed and the download field is always seen as + // null + bodyBuilder.appendFormalLine("UIComponent " + fieldValueId + + ";"); + bodyBuilder.appendFormalLine("if (" + entityName + + " != null && " + entityName + ".get" + + StringUtils.capitalize(fieldName) + + "() != null && " + entityName + ".get" + + StringUtils.capitalize(fieldName) + + "().length > 0) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldValueId + " = " + + getComponentCreation("CommandButton")); + bodyBuilder + .appendFormalLine("((CommandButton) " + + fieldValueId + + ").addActionListener(new FileDownloadActionListener(expressionFactory.createValueExpression(elContext, \"#{" + + beanName + + "." + + fieldName + + "StreamedContent}\", StreamedContent.class), null));"); + bodyBuilder.appendFormalLine("((CommandButton) " + + fieldValueId + ").setValue(\"Download\");"); + bodyBuilder.appendFormalLine("((CommandButton) " + + fieldValueId + ").setAjax(false);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(fieldValueId + " = " + + getComponentCreation("HtmlOutputText")); + bodyBuilder.appendFormalLine("((HtmlOutputText) " + + fieldValueId + ").setValue(\"\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + else { + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_FILE_UPLOAD, + PRIMEFACES_FILE_UPLOAD_EVENT); + + bodyBuilder.appendFormalLine("FileUpload " + fieldValueId + + " = " + getComponentCreation("FileUpload")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setFileUploadListener(expressionFactory.createMethodExpression(elContext, \"#{" + + beanName + + "." + + getFileUploadMethodName(fieldName) + + "}\", void.class, new Class[] { FileUploadEvent.class }));"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setMode(\"advanced\");"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setAllowTypes(\"/(\\\\.|\\\\/)(" + + getAllowTypeRegex(allowedType) + ")$/\");"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setUpdate(\":growlForm:growl\");"); + + final AnnotationAttributeValue autoUploadAttr = annotation + .getAttribute("autoUpload"); + if (autoUploadAttr != null + && (Boolean) autoUploadAttr.getValue()) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setAuto(true);"); + } + bodyBuilder.appendFormalLine(requiredStr); + } + } + else if (fieldType.equals(BOOLEAN_OBJECT) + || fieldType.equals(BOOLEAN_PRIMITIVE)) { + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName)); + } + else { + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_SELECT_BOOLEAN_CHECKBOX); + bodyBuilder.appendFormalLine("SelectBooleanCheckbox " + + fieldValueId + " = " + + getComponentCreation("SelectBooleanCheckbox")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(requiredStr); + } + } + else if (customData.keySet().contains(ENUMERATED_KEY)) { + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName)); + } + else { + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_AUTO_COMPLETE, fieldType); + + bodyBuilder.appendFormalLine("AutoComplete " + fieldValueId + + " = " + getComponentCreation("AutoComplete")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(getSetCompleteMethod( + fieldValueId, fieldName)); + bodyBuilder.appendFormalLine(fieldValueId + + ".setDropdown(true);"); + bodyBuilder.appendFormalLine(requiredStr); + } + } + else if (JdkJavaType.isDateField(fieldType)) { + if (action == Action.VIEW) { + builder.getImportRegistrationResolver().addImport( + DATE_TIME_CONVERTER); + + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder + .appendFormalLine("DateTimeConverter " + + converterName + + " = (DateTimeConverter) application.createConverter(DateTimeConverter.CONVERTER_ID);"); + // TODO Get working: + // bodyBuilder.appendFormalLine(converterName + + // ".setPattern(((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT)).toPattern());"); + bodyBuilder.appendFormalLine(converterName + + ".setPattern(\"dd/MM/yyyy\");"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setConverter(" + converterName + ");"); + } + else { + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_CALENDAR, DATE); + // builder.getImportRegistrationResolver().addImports(DATE_FORMAT, + // SIMPLE_DATE_FORMAT); + + bodyBuilder.appendFormalLine("Calendar " + fieldValueId + + " = " + getComponentCreation("Calendar")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, "Date")); + bodyBuilder.appendFormalLine(fieldValueId + + ".setNavigator(true);"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setEffect(\"slideDown\");"); + // TODO Get working: + // bodyBuilder.appendFormalLine(fieldValueId + + // ".setPattern(((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT)).toPattern());"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setPattern(\"dd/MM/yyyy\");"); + bodyBuilder.appendFormalLine(requiredStr); + if (MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), PAST) != null) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setMaxdate(new Date());"); + } + if (MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), FUTURE) != null) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setMindate(new Date());"); + } + } + } + else if (JdkJavaType.isIntegerType(fieldType)) { + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName)); + } + else { + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_INPUT_TEXT, PRIMEFACES_SPINNER); + if (fieldType.equals(JdkJavaType.BIG_INTEGER)) { + builder.getImportRegistrationResolver().addImport( + fieldType); + } + + bodyBuilder.appendFormalLine("Spinner " + fieldValueId + + " = " + getComponentCreation("Spinner")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(requiredStr); + if (minValue != null || maxValue != null) { + if (minValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setMin(" + minValue.doubleValue() + + ");"); + } + if (maxValue != null) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setMax(" + maxValue.doubleValue() + + ");"); + } + bodyBuilder.append(getLongRangeValdatorString( + fieldValueId, minValue, maxValue)); + } + bodyBuilder.appendFormalLine(""); + } + } + else if (JdkJavaType.isDecimalType(fieldType)) { + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName)); + } + else { + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_INPUT_TEXT); + if (fieldType.equals(JdkJavaType.BIG_DECIMAL)) { + builder.getImportRegistrationResolver().addImport( + fieldType); + } + + bodyBuilder.appendFormalLine(inputTextStr); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(requiredStr); + if (minValue != null || maxValue != null) { + bodyBuilder.append(getDoubleRangeValdatorString( + fieldValueId, minValue, maxValue)); + } + } + } + else if (fieldType.equals(STRING)) { + if (isTextarea) { + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_INPUT_TEXTAREA); + bodyBuilder.appendFormalLine("InputTextarea " + + fieldValueId + " = " + + getComponentCreation("InputTextarea")); + } + else { + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + } + else { + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_INPUT_TEXT); + bodyBuilder.appendFormalLine(inputTextStr); + } + } + + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName)); + if (action == Action.VIEW) { + if (isTextarea) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setReadonly(true);"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setDisabled(true);"); + } + } + else { + if (sizeMinValue != null || sizeMaxValue != null) { + bodyBuilder.append(getLengthValdatorString( + fieldValueId, sizeMinValue, sizeMaxValue)); + } + setRegexPatternValidationString(field, fieldValueId, + bodyBuilder); + bodyBuilder.appendFormalLine(requiredStr); + } + } + else if (customData.keySet().contains(PARAMETER_TYPE_KEY)) { + final JavaType parameterType = (JavaType) customData + .get(PARAMETER_TYPE_KEY); + final String parameterTypeSimpleTypeName = parameterType + .getSimpleTypeName(); + final String parameterTypeFieldName = StringUtils + .uncapitalize(parameterTypeSimpleTypeName); + final String parameterTypeManagedBeanName = (String) customData + .get(PARAMETER_TYPE_MANAGED_BEAN_NAME_KEY); + final String parameterTypePlural = (String) customData + .get(PARAMETER_TYPE_PLURAL_KEY); + + if (StringUtils.isNotBlank(parameterTypeManagedBeanName)) { + if (customData.keySet().contains(ONE_TO_MANY_FIELD) + || customData.keySet().contains(MANY_TO_MANY_FIELD) + && isInverseSideOfRelationship(field, ONE_TO_MANY, + MANY_TO_MANY)) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValue(\"This relationship is managed from the " + + parameterTypeSimpleTypeName + + " side\");"); + } + else { + final JavaType converterType = new JavaType(destination + .getPackage().getFullyQualifiedPackageName() + + ".converter." + + parameterTypeSimpleTypeName + + "Converter"); + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_SELECT_MANY_MENU, UI_SELECT_ITEMS, + fieldType, converterType); + + bodyBuilder.appendFormalLine("SelectManyMenu " + + fieldValueId + " = " + + getComponentCreation("SelectManyMenu")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(fieldValueId + + ".setConverter(new " + + converterType.getSimpleTypeName() + "());"); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + beanName + "." + + getSelectedFieldName(fieldName) + + "}\", List.class));"); + bodyBuilder + .appendFormalLine("UISelectItems " + + fieldValueId + + "Items = (UISelectItems) application.createComponent(UISelectItems.COMPONENT_TYPE);"); + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setReadonly(true);"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setDisabled(true);"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + beanName + "." + + entityName.getSymbolName() + "." + + fieldName + "}\", " + + simpleTypeName + ".class));"); + } + else { + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + parameterTypeManagedBeanName + + ".all" + + StringUtils + .capitalize(parameterTypePlural) + + "}\", List.class));"); + bodyBuilder.appendFormalLine(requiredStr); + } + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"var\", expressionFactory.createValueExpression(elContext, \"" + + parameterTypeFieldName + + "\", String.class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"itemLabel\", expressionFactory.createValueExpression(elContext, \"#{" + + parameterTypeFieldName + + "}\", String.class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"itemValue\", expressionFactory.createValueExpression(elContext, \"#{" + + parameterTypeFieldName + "}\", " + + parameterTypeSimpleTypeName + + ".class));"); + bodyBuilder.appendFormalLine(getAddChildToComponent( + fieldValueId, fieldValueId + "Items")); + } + } + else { + // Parameter type is an enum + bodyBuilder.appendFormalLine("SelectManyMenu " + + fieldValueId + " = " + + getComponentCreation("SelectManyMenu")); + bodyBuilder.appendFormalLine(componentIdStr); + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(fieldValueId + + ".setReadonly(true);"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setDisabled(true);"); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + beanName + "." + + getSelectedFieldName(fieldName) + + "}\", List.class));"); + bodyBuilder + .appendFormalLine("UISelectItems " + + fieldValueId + + "Items = (UISelectItems) application.createComponent(UISelectItems.COMPONENT_TYPE);"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + beanName + "." + + entityName.getSymbolName() + "." + + fieldName + "}\", " + simpleTypeName + + ".class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"var\", expressionFactory.createValueExpression(elContext, \"" + + parameterTypeFieldName + + "\", String.class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"itemLabel\", expressionFactory.createValueExpression(elContext, \"#{" + + parameterTypeFieldName + + "}\", String.class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Items.setValueExpression(\"itemValue\", expressionFactory.createValueExpression(elContext, \"#{" + + parameterTypeFieldName + "}\", " + + parameterTypeSimpleTypeName + + ".class));"); + bodyBuilder.appendFormalLine(getAddChildToComponent( + fieldValueId, fieldValueId + "Items")); + } + else { + builder.getImportRegistrationResolver().addImports( + UI_SELECT_ITEM, ENUM_CONVERTER); + + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + beanName + "." + + getSelectedFieldName(fieldName) + + "}\", List.class));"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setConverter(new EnumConverter(" + + parameterTypeSimpleTypeName + ".class));"); + bodyBuilder.appendFormalLine(requiredStr); + bodyBuilder.appendFormalLine("UISelectItem " + + fieldValueId + "Item;"); + bodyBuilder + .appendFormalLine("for (" + + parameterTypeSimpleTypeName + + " " + + StringUtils + .uncapitalize(parameterTypeSimpleTypeName) + + " : " + parameterTypeSimpleTypeName + + ".values()) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine(fieldValueId + + "Item = (UISelectItem) application.createComponent(UISelectItem.COMPONENT_TYPE);"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Item.setItemLabel(" + + StringUtils + .uncapitalize(parameterTypeSimpleTypeName) + + ".name());"); + bodyBuilder + .appendFormalLine(fieldValueId + + "Item.setItemValue(" + + StringUtils + .uncapitalize(parameterTypeSimpleTypeName) + + ");"); + bodyBuilder.appendFormalLine(getAddChildToComponent( + fieldValueId, fieldValueId + "Item")); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + } + } + else if (customData.keySet().contains(APPLICATION_TYPE_KEY)) { + if (customData.keySet().contains(ONE_TO_ONE_FIELD) + && isInverseSideOfRelationship(field, ONE_TO_ONE)) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValue(\"This relationship is managed from the " + + simpleTypeName + " side\");"); + } + else { + final JavaType converterType = new JavaType(destination + .getPackage().getFullyQualifiedPackageName() + + ".converter." + simpleTypeName + "Converter"); + builder.getImportRegistrationResolver().addImport( + converterType); + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(fieldValueId + + ".setConverter(new " + + converterType.getSimpleTypeName() + "());"); + } + else { + builder.getImportRegistrationResolver().addImports( + PRIMEFACES_AUTO_COMPLETE, fieldType); + + bodyBuilder.appendFormalLine("AutoComplete " + + fieldValueId + " = " + + getComponentCreation("AutoComplete")); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(getSetCompleteMethod( + fieldValueId, fieldName)); + bodyBuilder.appendFormalLine(fieldValueId + + ".setDropdown(true);"); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValueExpression(\"var\", expressionFactory.createValueExpression(elContext, \"" + + fieldName + "\", String.class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValueExpression(\"itemLabel\", expressionFactory.createValueExpression(elContext, \"" + + getAutoCcompleteItemLabelValue(field, + fieldName) + + "\", String.class));"); + bodyBuilder + .appendFormalLine(fieldValueId + + ".setValueExpression(\"itemValue\", expressionFactory.createValueExpression(elContext, \"#{" + + fieldName + "}\", " + simpleTypeName + + ".class));"); + bodyBuilder.appendFormalLine(fieldValueId + + ".setConverter(new " + + converterType.getSimpleTypeName() + "());"); + bodyBuilder.appendFormalLine(requiredStr); + } + } + } + else { + if (action == Action.VIEW) { + bodyBuilder.appendFormalLine(htmlOutputTextStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName)); + } + else { + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_INPUT_TEXT); + + bodyBuilder.appendFormalLine(inputTextStr); + bodyBuilder.appendFormalLine(componentIdStr); + bodyBuilder.appendFormalLine(getSetValueExpression( + fieldValueId, fieldName, simpleTypeName)); + bodyBuilder.appendFormalLine(requiredStr); + } + } + + if (action != Action.VIEW) { + bodyBuilder.appendFormalLine(getAddToPanelText(fieldValueId)); + // Add message for input field + builder.getImportRegistrationResolver().addImport( + PRIMEFACES_MESSAGE); + + bodyBuilder.appendFormalLine(""); + bodyBuilder.appendFormalLine("Message " + fieldValueId + + "Message = " + getComponentCreation("Message")); + bodyBuilder.appendFormalLine(fieldValueId + "Message.setId(\"" + + fieldValueId + "Message\");"); + bodyBuilder.appendFormalLine(fieldValueId + "Message.setFor(\"" + + fieldValueId + "\");"); + bodyBuilder.appendFormalLine(fieldValueId + + "Message.setDisplay(\"icon\");"); + bodyBuilder.appendFormalLine(getAddToPanelText(fieldValueId + + "Message")); + } + else { + bodyBuilder.appendFormalLine(getAddToPanelText(fieldValueId)); + } + + bodyBuilder.appendFormalLine(""); + } + bodyBuilder.appendFormalLine("return " + HTML_PANEL_GRID_ID + ";"); + + return new MethodMetadataBuilder(getId(), PUBLIC, methodName, + HTML_PANEL_GRID, new ArrayList(), + new ArrayList(), bodyBuilder); + } + + private MethodMetadataBuilder getResetMethod() { + final JavaSymbolName methodName = new JavaSymbolName("reset"); + if (governorHasMethod(methodName)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName.getSymbolName() + " = null;"); + for (final FieldMetadata field : locatedFields) { + final CustomData customData = field.getCustomData(); + if (!customData.keySet().contains(PARAMETER_TYPE_KEY)) { + continue; + } + + bodyBuilder.appendFormalLine(getSelectedFieldName(field + .getFieldName().getSymbolName()) + " = null;"); + } + bodyBuilder.appendFormalLine(CREATE_DIALOG_VISIBLE + " = false;"); + return getMethod(PUBLIC, methodName, VOID_PRIMITIVE, null, null, + bodyBuilder); + } + + private AnnotationMetadata getScopeAnnotation() { + if (hasScopeAnnotation()) { + return null; + } + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + SESSION_SCOPED); + return annotationBuilder.build(); + } + + private String getSelectedFieldName(final String fieldName) { + return "selected" + StringUtils.capitalize(fieldName); + } + + private String getSetCompleteMethod(final String fieldValueId, + final String fieldName) { + return fieldValueId + + ".setCompleteMethod(expressionFactory.createMethodExpression(elContext, \"#{" + + beanName + ".complete" + StringUtils.capitalize(fieldName) + + "}\", List.class, new Class[] { String.class }));"; + } + + private String getSetValueExpression(final String fieldValueId, + final String fieldName) { + return getSetValueExpression(fieldValueId, fieldName, "String"); + } + + private String getSetValueExpression(final String inputFieldVar, + final String fieldName, final String className) { + return inputFieldVar + + ".setValueExpression(\"value\", expressionFactory.createValueExpression(elContext, \"#{" + + beanName + "." + entityName.getSymbolName() + "." + fieldName + + "}\", " + className + ".class));"; + } + + private Integer getSizeMinOrMax(final FieldMetadata field, + final String attrName) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), SIZE); + if (annotation != null + && annotation.getAttribute(new JavaSymbolName(attrName)) != null) { + return (Integer) annotation.getAttribute( + new JavaSymbolName(attrName)).getValue(); + } + return null; + } + + private boolean hasScopeAnnotation() { + return governorTypeDetails.getAnnotation(SESSION_SCOPED) != null + || governorTypeDetails.getAnnotation(VIEW_SCOPED) != null + || governorTypeDetails.getAnnotation(REQUEST_SCOPED) != null + || governorTypeDetails.getAnnotation(APPLICATION_SCOPED) != null; + } + + private boolean isInverseSideOfRelationship(final FieldMetadata field, + final JavaType... annotationTypes) { + for (final JavaType annotationType : annotationTypes) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), annotationType); + if (annotation != null + && annotation.getAttribute(new JavaSymbolName("mappedBy")) != null) { + return true; + } + } + return false; + } + + private boolean isNullable(final FieldMetadata field) { + return MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), + NOT_NULL) == null; + } + + private void setRegexPatternValidationString(final FieldMetadata field, + final String fieldValueId, + final InvocableMemberBodyBuilder bodyBuilder) { + final AnnotationMetadata patternAnnotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), PATTERN); + if (patternAnnotation != null) { + builder.getImportRegistrationResolver().addImport(REGEX_VALIDATOR); + + final AnnotationAttributeValue regexpAttr = patternAnnotation + .getAttribute(new JavaSymbolName("regexp")); + bodyBuilder.appendFormalLine("RegexValidator " + fieldValueId + + "RegexValidator = new RegexValidator();"); + bodyBuilder.appendFormalLine(fieldValueId + + "RegexValidator.setPattern(\"" + regexpAttr.getValue() + + "\");"); + bodyBuilder.appendFormalLine(fieldValueId + ".addValidator(" + + fieldValueId + "RegexValidator);"); + } + } + + private boolean isUIComponent(FieldMetadata field, JavaType fieldType, + CustomData customData) { + + if (field.getAnnotation(ROO_UPLOADED_FILE) != null + || fieldType.equals(BOOLEAN_OBJECT) + || fieldType.equals(BOOLEAN_PRIMITIVE) + || customData.keySet().contains(ENUMERATED_KEY) + || JdkJavaType.isDateField(fieldType) + || JdkJavaType.isIntegerType(fieldType) + || JdkJavaType.isDecimalType(fieldType) + || fieldType.equals(STRING)) { + + return true; + + } + else if (customData.keySet().contains(PARAMETER_TYPE_KEY)) { + if (StringUtils.isNotBlank((String) customData + .get(PARAMETER_TYPE_MANAGED_BEAN_NAME_KEY))) { + if (customData.keySet().contains(ONE_TO_MANY_FIELD) + || customData.keySet().contains(MANY_TO_MANY_FIELD) + && isInverseSideOfRelationship(field, ONE_TO_MANY, + MANY_TO_MANY)) { + return false; + } + } + + return true; + + } + else if (customData.keySet().contains(APPLICATION_TYPE_KEY)) { + if (customData.keySet().contains(ONE_TO_ONE_FIELD) + && isInverseSideOfRelationship(field, ONE_TO_ONE)) { + return false; + } + return true; + } + else { + return true; + } + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadataProvider.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadataProvider.java new file mode 100644 index 000000000..d7ea23ac9 --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.jsf.managedbean; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link JsfManagedBeanMetadata}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface JsfManagedBeanMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadataProviderImpl.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadataProviderImpl.java new file mode 100644 index 000000000..ae9b4c0ec --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/JsfManagedBeanMetadataProviderImpl.java @@ -0,0 +1,527 @@ +package org.springframework.roo.addon.jsf.managedbean; + +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.APPLICATION_TYPE_FIELDS_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.APPLICATION_TYPE_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.CRUD_ADDITIONS_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.ENUMERATED_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.LIST_VIEW_FIELD_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.PARAMETER_TYPE_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.PARAMETER_TYPE_MANAGED_BEAN_NAME_KEY; +import static org.springframework.roo.addon.jsf.managedbean.JsfManagedBeanMetadata.PARAMETER_TYPE_PLURAL_KEY; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.JavaType.BOOLEAN_OBJECT; +import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; +import static org.springframework.roo.model.JavaType.BYTE_ARRAY_PRIMITIVE; +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.RooJavaType.ROO_JSF_MANAGED_BEAN; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.CustomDataBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link JsfManagedBeanMetadataProvider}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component +@Service +public class JsfManagedBeanMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + JsfManagedBeanMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JsfManagedBeanMetadataProviderImpl.class); + + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + // -- The maximum number of fields to form a String to show in a drop down + // field. + private static final int MAX_DROP_DOWN_FIELDS = 4; + // -- The maximum number of entity fields to show in a list view. + private static final int MAX_LIST_VIEW_FIELDS = 5; + + private ConfigurableMetadataProvider configurableMetadataProvider; + private LayerService layerService; + private final Map entityToManagedBeanMidMap = new LinkedHashMap(); + private final Map managedBeanMidToEntityMap = new LinkedHashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_JSF_MANAGED_BEAN); + getConfigurableMetadataProvider().addMetadataTrigger(ROO_JSF_MANAGED_BEAN); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return JsfManagedBeanMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_JSF_MANAGED_BEAN); + getConfigurableMetadataProvider() + .removeMetadataTrigger(ROO_JSF_MANAGED_BEAN); + } + + /** + * Returns the additions to make to the generated ITD in order to invoke the + * various CRUD methods of the given entity + * + * @param entity the target entity type (required) + * @param metadataIdentificationString the ID of the metadata that's being + * created (required) + * @return a non-null map (may be empty if the CRUD methods are + * indeterminable) + */ + private Map getCrudAdditions( + final JavaType entity, final String metadataIdentificationString) { + + if(layerService == null){ + layerService = getLayerService(); + } + Validate.notNull(layerService, "LayerService is required"); + + getMetadataDependencyRegistry().registerDependency( + getTypeLocationService().getPhysicalTypeIdentifier(entity), + metadataIdentificationString); + final List idFields = getPersistenceMemberLocator() + .getIdentifierFields(entity); + if (idFields.isEmpty()) { + return Collections.emptyMap(); + } + final FieldMetadata identifierField = idFields.get(0); + final JavaType identifierType = getPersistenceMemberLocator() + .getIdentifierType(entity); + if (identifierType == null) { + return Collections.emptyMap(); + } + getMetadataDependencyRegistry().registerDependency( + identifierField.getDeclaredByMetadataId(), + metadataIdentificationString); + + final JavaSymbolName entityName = JavaSymbolName + .getReservedWordSafeName(entity); + final MethodParameter entityParameter = new MethodParameter(entity, + entityName); + final MethodParameter idParameter = new MethodParameter(identifierType, + "id"); + final MethodParameter firstResultParameter = new MethodParameter( + INT_PRIMITIVE, "firstResult"); + final MethodParameter maxResultsParameter = new MethodParameter( + INT_PRIMITIVE, "sizeNo"); + + final Map additions = new HashMap(); + additions.put(COUNT_ALL_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, COUNT_ALL_METHOD.name(), entity, + identifierType, LAYER_POSITION)); + additions.put(FIND_ALL_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, FIND_ALL_METHOD.name(), entity, + identifierType, LAYER_POSITION)); + additions.put(FIND_ENTRIES_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, FIND_ENTRIES_METHOD.name(), + entity, identifierType, LAYER_POSITION, firstResultParameter, + maxResultsParameter)); + additions.put(FIND_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, FIND_METHOD.name(), entity, + identifierType, LAYER_POSITION, idParameter)); + additions.put(MERGE_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, MERGE_METHOD.name(), entity, + identifierType, LAYER_POSITION, entityParameter)); + additions.put(PERSIST_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, PERSIST_METHOD.name(), entity, + identifierType, LAYER_POSITION, entityParameter)); + additions.put(REMOVE_METHOD, layerService.getMemberTypeAdditions( + metadataIdentificationString, REMOVE_METHOD.name(), entity, + identifierType, LAYER_POSITION, entityParameter)); + return additions; + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = JsfManagedBeanMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JsfManagedBeanMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "ManagedBean"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = entityToManagedBeanMidMap.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = entityToManagedBeanMidMap.get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // We need to parse the annotation, which we expect to be present + final JsfManagedBeanAnnotationValues annotationValues = new JsfManagedBeanAnnotationValues( + governorPhysicalTypeMetadata); + final JavaType entity = annotationValues.getEntity(); + if (!annotationValues.isAnnotationFound() || entity == null) { + return null; + } + + final MemberDetails memberDetails = getMemberDetails(entity); + if (memberDetails == null) { + return null; + } + + final MethodMetadata identifierAccessor = getPersistenceMemberLocator() + .getIdentifierAccessor(entity); + final MethodMetadata versionAccessor = getPersistenceMemberLocator() + .getVersionAccessor(entity); + final Set locatedFields = locateFields(entity, + memberDetails, metadataIdentificationString, + identifierAccessor, versionAccessor); + + // Remember that this entity JavaType matches up with this metadata + // identification string + // Start by clearing any previous association + final JavaType oldEntity = managedBeanMidToEntityMap + .get(metadataIdentificationString); + if (oldEntity != null) { + entityToManagedBeanMidMap.remove(oldEntity); + } + entityToManagedBeanMidMap.put(entity, metadataIdentificationString); + managedBeanMidToEntityMap.put(metadataIdentificationString, entity); + + final String physicalTypeIdentifier = getTypeLocationService() + .getPhysicalTypeIdentifier(entity); + final LogicalPath path = PhysicalTypeIdentifier + .getPath(physicalTypeIdentifier); + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(PluralMetadata.createIdentifier(entity, path)); + Validate.notNull(pluralMetadata, "Could not determine plural for '%s'", + entity.getSimpleTypeName()); + final String plural = pluralMetadata.getPlural(); + + final Map crudAdditions = getCrudAdditions( + entity, metadataIdentificationString); + + return new JsfManagedBeanMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, annotationValues, + plural, crudAdditions, locatedFields, identifierAccessor); + } + + public String getProvidesType() { + return JsfManagedBeanMetadata.getMetadataIdentiferType(); + } + + private boolean isEnum(final ClassOrInterfaceTypeDetails cid) { + return cid != null + && cid.getPhysicalTypeCategory() == PhysicalTypeCategory.ENUMERATION; + } + + private boolean isFieldOfInterest(final FieldMetadata field) { + final JavaType fieldType = field.getFieldType(); + return !fieldType.isCommonCollectionType() + && !fieldType.isArray() // Exclude collections and arrays + && !fieldType.equals(BOOLEAN_PRIMITIVE) + // Boolean values would not be meaningful in this presentation + && !fieldType.equals(BOOLEAN_OBJECT) + && !fieldType.equals(BYTE_ARRAY_PRIMITIVE) + // Not interested in embedded types + && !field.getCustomData().keySet().contains(EMBEDDED_FIELD); + } + + /** + * Returns an iterable collection of the given entity's fields excluding any + * ID or version field; along the way, flags the first + * {@value #MAX_LIST_VIEW_FIELDS} non ID/version fields as being displayable + * in the list view for this entity type. + * + * @param entity the entity for which to find the fields and accessors + * (required) + * @param memberDetails the entity's members (required) + * @param metadataIdentificationString the ID of the metadata being + * generated (required) + * @param versionAccessor + * @param identifierAccessor + * @return a non-null iterable collection + */ + private Set locateFields(final JavaType entity, + final MemberDetails memberDetails, + final String metadataIdentificationString, + final MethodMetadata identifierAccessor, + final MethodMetadata versionAccessor) { + final Set locatedFields = new LinkedHashSet(); + final Set managedBeanTypes = getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(ROO_JSF_MANAGED_BEAN); + + int listViewFields = 0; + for (final MethodMetadata method : memberDetails.getMethods()) { + if (!BeanInfoUtils.isAccessorMethod(method)) { + continue; + } + if (method.hasSameName(identifierAccessor, versionAccessor)) { + continue; + } + final FieldMetadata field = BeanInfoUtils.getFieldForPropertyName( + memberDetails, + BeanInfoUtils.getPropertyNameForJavaBeanMethod(method)); + if (field == null) { + continue; + } + getMetadataDependencyRegistry().registerDependency( + field.getDeclaredByMetadataId(), + metadataIdentificationString); + + final CustomDataBuilder customDataBuilder = new CustomDataBuilder( + field.getCustomData()); + final JavaType fieldType = field.getFieldType(); + if (fieldType.equals(DATE) + && field.getFieldName().getSymbolName().equals("created")) { + continue; + } + + final ClassOrInterfaceTypeDetails fieldTypeCid = getTypeLocationService() + .getTypeDetails(fieldType); + + // Check field is to be displayed in the entity's list view + if (listViewFields < MAX_LIST_VIEW_FIELDS + && isFieldOfInterest(field) && fieldTypeCid == null) { + listViewFields++; + customDataBuilder.put(LIST_VIEW_FIELD_KEY, field); + } + + final boolean enumerated = field.getCustomData().keySet() + .contains(CustomDataKeys.ENUMERATED_FIELD) + || isEnum(fieldTypeCid); + if (enumerated) { + customDataBuilder.put(ENUMERATED_KEY, null); + } + else { + if (fieldType.isCommonCollectionType()) { + parameterTypeLoop: for (final JavaType parameter : fieldType + .getParameters()) { + final ClassOrInterfaceTypeDetails parameterTypeCid = getTypeLocationService() + .getTypeDetails(parameter); + if (parameterTypeCid == null) { + continue; + } + + for (final ClassOrInterfaceTypeDetails managedBeanType : managedBeanTypes) { + final AnnotationMetadata managedBeanAnnotation = managedBeanType + .getAnnotation(ROO_JSF_MANAGED_BEAN); + if (((JavaType) managedBeanAnnotation.getAttribute( + "entity").getValue()).equals(parameter)) { + customDataBuilder.put(PARAMETER_TYPE_KEY, + parameter); + customDataBuilder.put( + PARAMETER_TYPE_MANAGED_BEAN_NAME_KEY, + managedBeanAnnotation.getAttribute( + "beanName").getValue()); + + final LogicalPath logicalPath = PhysicalTypeIdentifier + .getPath(parameterTypeCid + .getDeclaredByMetadataId()); + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(PluralMetadata.createIdentifier( + parameter, logicalPath)); + if (pluralMetadata != null) { + customDataBuilder.put( + PARAMETER_TYPE_PLURAL_KEY, + pluralMetadata.getPlural()); + } + // Only support one generic type parameter + break parameterTypeLoop; + } + // Parameter type is not an entity - test for an + // enum + if (isEnum(parameterTypeCid)) { + customDataBuilder.put(PARAMETER_TYPE_KEY, + parameter); + } + } + } + } + else { + if (fieldTypeCid != null + && !customDataBuilder.keySet().contains( + CustomDataKeys.EMBEDDED_FIELD)) { + customDataBuilder.put(APPLICATION_TYPE_KEY, null); + final MethodMetadata applicationTypeIdentifierAccessor = getPersistenceMemberLocator() + .getIdentifierAccessor(entity); + final MethodMetadata applicationTypeVersionAccessor = getPersistenceMemberLocator() + .getVersionAccessor(entity); + final List applicationTypeFields = new ArrayList(); + + int dropDownFields = 0; + final MemberDetails applicationTypeMemberDetails = getMemberDetails(fieldType); + for (final MethodMetadata applicationTypeMethod : applicationTypeMemberDetails + .getMethods()) { + if (!BeanInfoUtils + .isAccessorMethod(applicationTypeMethod)) { + continue; + } + if (applicationTypeMethod.hasSameName( + applicationTypeIdentifierAccessor, + applicationTypeVersionAccessor)) { + continue; + } + final FieldMetadata applicationTypeField = BeanInfoUtils + .getFieldForJavaBeanMethod( + applicationTypeMemberDetails, + applicationTypeMethod); + if (applicationTypeField == null) { + continue; + } + if (dropDownFields < MAX_DROP_DOWN_FIELDS + && isFieldOfInterest(applicationTypeField) + && !getTypeLocationService() + .isInProject(applicationTypeField + .getFieldType())) { + dropDownFields++; + applicationTypeFields.add(applicationTypeField); + } + } + if (applicationTypeFields.isEmpty()) { + applicationTypeFields.add(BeanInfoUtils + .getFieldForJavaBeanMethod( + applicationTypeMemberDetails, + applicationTypeIdentifierAccessor)); + } + customDataBuilder.put(APPLICATION_TYPE_FIELDS_KEY, + applicationTypeFields); + customDataBuilder.put( + CRUD_ADDITIONS_KEY, + getCrudAdditions(fieldType, + metadataIdentificationString)); + } + } + } + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + field); + fieldBuilder.setCustomData(customDataBuilder); + locatedFields.add(fieldBuilder.build()); + } + + return locatedFields; + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on JsfManagedBeanMetadataProviderImpl."); + return null; + } + }else{ + return configurableMetadataProvider; + } + + } + + public LayerService getLayerService(){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on JsfManagedBeanMetadataProviderImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/RooJsfManagedBean.java b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/RooJsfManagedBean.java new file mode 100644 index 000000000..1b513f69a --- /dev/null +++ b/addon-jsf/src/main/java/org/springframework/roo/addon/jsf/managedbean/RooJsfManagedBean.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.jsf.managedbean; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires ROO JSF managed-bean support. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJsfManagedBean { + + String beanName(); + + Class entity(); + + boolean includeOnMenu() default true; +} diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ApplicationBean-template.java b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ApplicationBean-template.java new file mode 100644 index 000000000..42d9582b9 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ApplicationBean-template.java @@ -0,0 +1,23 @@ +package __PACKAGE__; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.roo.addon.jsf.application.RooJsfApplicationBean; + +@RooJsfApplicationBean +public class ApplicationBean { + + public String getColumnName(String column) { + if (column == null || column.length() == 0) { + return column; + } + final Pattern p = Pattern.compile("[A-Z][^A-Z]*"); + final Matcher m = p.matcher(Character.toUpperCase(column.charAt(0)) + column.substring(1)); + final StringBuilder builder = new StringBuilder(); + while (m.find()) { + builder.append(m.group()).append(" "); + } + return builder.toString().trim(); + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/LocaleBean-template.java b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/LocaleBean-template.java new file mode 100755 index 000000000..683a0f3a3 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/LocaleBean-template.java @@ -0,0 +1,60 @@ +package __PACKAGE__; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import javax.annotation.PostConstruct; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.SessionScoped; +import javax.faces.context.FacesContext; +import javax.faces.model.SelectItem; + +@ManagedBean +@SessionScoped +public class LocaleBean { + private Locale locale; + + @PostConstruct + public void init() { + locale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale(); + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public SelectItem[] getLocales() { + List items = new ArrayList(); + Iterator supportedLocales = FacesContext.getCurrentInstance().getApplication().getSupportedLocales(); + while (supportedLocales.hasNext()) { + Locale locale = supportedLocales.next(); + items.add(new SelectItem(locale.toString(), locale.getDisplayName())); + } + return items.toArray(new SelectItem[] {}); + } + + public String getSelectedLocale() { + return getLocale().toString(); + } + + public void setSelectedLocale() { + setSelectedLocale(FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("locale")); + } + + public void setSelectedLocale(String localeString) { + Iterator supportedLocales = FacesContext.getCurrentInstance().getApplication().getSupportedLocales(); + while (supportedLocales.hasNext()) { + Locale locale = supportedLocales.next(); + if (locale.toString().equals(localeString)) { + this.locale = locale; + break; + } + } + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/MessageFactory-template.java b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/MessageFactory-template.java new file mode 100644 index 000000000..127213c08 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/MessageFactory-template.java @@ -0,0 +1,91 @@ +package __PACKAGE__; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; + +public class MessageFactory { + + private static String DEFAULT_DETAIL_SUFFIX = "_detail"; + + private MessageFactory() { + } + + public static FacesMessage getMessage(final Locale locale, final String messageId, + final FacesMessage.Severity severity, final Object... params) { + final FacesMessage facesMessage = getMessage(locale, messageId, params); + facesMessage.setSeverity(severity); + return facesMessage; + } + + public static FacesMessage getMessage(final Locale locale, final String messageId, + final Object... params) { + String summary = null; + String detail = null; + final FacesContext context = FacesContext.getCurrentInstance(); + final ResourceBundle bundle = context.getApplication() + .getResourceBundle(context, "messages"); + + try { + summary = getFormattedText(locale, bundle.getString(messageId), + params); + } + catch (final MissingResourceException e) { + summary = messageId; + } + + try { + detail = getFormattedText(locale, + bundle.getString(messageId + DEFAULT_DETAIL_SUFFIX), params); + } + catch (final MissingResourceException e) { + // NoOp + } + + return new FacesMessage(summary, detail); + } + + public static FacesMessage getMessage(final String messageId, + final FacesMessage.Severity severity, final Object... params) { + final FacesMessage facesMessage = getMessage(getLocale(), messageId, params); + facesMessage.setSeverity(severity); + return facesMessage; + } + + public static FacesMessage getMessage(final String messageId, final Object... params) { + return getMessage(getLocale(), messageId, params); + } + + private static String getFormattedText(final Locale locale, final String message, + final Object params[]) { + MessageFormat messageFormat = null; + + if (params == null || message == null) { + return message; + } + + messageFormat = locale == null ? new MessageFormat(message) + : new MessageFormat(message, locale); + return messageFormat.format(params); + } + + private static Locale getLocale() { + Locale locale = null; + final FacesContext facesContext = FacesContext.getCurrentInstance(); + if (facesContext != null && facesContext.getViewRoot() != null) { + locale = facesContext.getViewRoot().getLocale(); + if (locale == null) { + locale = Locale.getDefault(); + } + } + else { + locale = Locale.getDefault(); + } + + return locale; + } +} diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ViewExpiredExceptionExceptionHandler-template.java b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ViewExpiredExceptionExceptionHandler-template.java new file mode 100644 index 000000000..f61eae20c --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ViewExpiredExceptionExceptionHandler-template.java @@ -0,0 +1,53 @@ +package __PACKAGE__; + +import java.util.Iterator; +import java.util.Map; + +import javax.faces.FacesException; +import javax.faces.application.NavigationHandler; +import javax.faces.application.ViewExpiredException; +import javax.faces.context.ExceptionHandler; +import javax.faces.context.ExceptionHandlerWrapper; +import javax.faces.context.FacesContext; +import javax.faces.event.ExceptionQueuedEvent; +import javax.faces.event.ExceptionQueuedEventContext; + +public class ViewExpiredExceptionExceptionHandler extends ExceptionHandlerWrapper { + private ExceptionHandler wrapped; + + public ViewExpiredExceptionExceptionHandler(ExceptionHandler wrapped) { + this.wrapped = wrapped; + } + + @Override + public ExceptionHandler getWrapped() { + return this.wrapped; + } + + @Override + public void handle() throws FacesException { + for (Iterator i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) { + ExceptionQueuedEvent event = i.next(); + ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource(); + + Throwable t = context.getException(); + if (t instanceof ViewExpiredException) { + ViewExpiredException vee = (ViewExpiredException) t; + FacesContext facesContext = FacesContext.getCurrentInstance(); + Map requestMap = facesContext.getExternalContext().getRequestMap(); + NavigationHandler navigationHandler = facesContext.getApplication().getNavigationHandler(); + try { + // Push some useful stuff to the request scope for use in the page + requestMap.put("currentViewId", vee.getViewId()); + navigationHandler.handleNavigation(facesContext, null, "/viewExpired"); + facesContext.renderResponse(); + } finally { + i.remove(); + } + } + } + + // At this point, the queue will not contain any ViewExpiredEvents. Therefore, let the parent handle them. + getWrapped().handle(); + } +} diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ViewExpiredExceptionExceptionHandlerFactory-template.java b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ViewExpiredExceptionExceptionHandlerFactory-template.java new file mode 100644 index 000000000..62ab42fc3 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/ViewExpiredExceptionExceptionHandlerFactory-template.java @@ -0,0 +1,17 @@ +package __PACKAGE__; + +import javax.faces.context.ExceptionHandler; +import javax.faces.context.ExceptionHandlerFactory; + +public class ViewExpiredExceptionExceptionHandlerFactory extends ExceptionHandlerFactory { + private ExceptionHandlerFactory parent; + + public ViewExpiredExceptionExceptionHandlerFactory(ExceptionHandlerFactory parent) { + this.parent = parent; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return new ViewExpiredExceptionExceptionHandler(parent.getExceptionHandler()); + } +} \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/WEB-INF/faces-config-template.xml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/WEB-INF/faces-config-template.xml new file mode 100644 index 000000000..a05744cf5 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/WEB-INF/faces-config-template.xml @@ -0,0 +1,22 @@ + + + + org.springframework.web.jsf.el.SpringBeanFacesELResolver + + __PACKAGE__.i18n.messages + messages + + + en + en + de + es + + + + __PACKAGE__.util.ViewExpiredExceptionExceptionHandlerFactory + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/WEB-INF/web-template.xml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/WEB-INF/web-template.xml new file mode 100644 index 000000000..9be050565 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/WEB-INF/web-template.xml @@ -0,0 +1,56 @@ + + + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + + + defaultHtmlEscape + true + + + contextConfigLocation + classpath*:META-INF/spring/applicationContext*.xml + + + primefaces.THEME + south-street + + + PrimeFaces FileUpload Filter + org.primefaces.webapp.filter.FileUploadFilter + + + PrimeFaces FileUpload Filter + Faces Servlet + + + Spring OpenEntityManagerInViewFilter + org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter + + + Spring OpenEntityManagerInViewFilter + /* + + + + org.springframework.web.context.ContextLoaderListener + + + Faces Servlet + javax.faces.webapp.FacesServlet + 1 + + + Faces Servlet + *.jsf + + + 10 + + + index.html + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/configuration.xml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/configuration.xml new file mode 100644 index 000000000..6ce13cc92 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/configuration.xml @@ -0,0 +1,94 @@ + + + + + + + com.sun.faces + jsf-api + 2.1.2 + + + com.sun.faces + jsf-impl + 2.1.2 + + + + + java.net.m2 + http://download.java.net/maven/2 + + + + + + + org.apache.myfaces.core + myfaces-api + 2.1.10 + + + org.apache.myfaces.core + myfaces-impl + 2.1.10 + + + commons-logging + commons-logging + + + + + + + + + + + org.primefaces + primefaces + 3.5 + + + org.primefaces.themes + south-street + 1.0.10 + + + + + prime-repo + PrimeFaces Maven Repository + http://repository.primefaces.org + default + + + + + + + + org.springframework + spring-web + ${spring.version} + + + javax.el + el-api + 2.2 + provided + + + commons-fileupload + commons-fileupload + 1.2.2 + + + commons-io + commons-io + 2.1 + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_de.properties b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_de.properties new file mode 100644 index 000000000..4a1dbe986 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_de.properties @@ -0,0 +1,26 @@ +label_actions=Aktionen +label_close=Schliessen +label_confirm_deletion=Lschen besttigen. +label_create=Erstellen +label_delete=Lschen +label_delete_record=Sind Sie sicher, dass Sie diesen Datensatz wirklich lschen mchten? +label_edit=Bearbeiten +label_find=Finde {0} +label_home=Home +label_language_switch=Sprache nach {0} ndern +label_language=Sprache +label_list=Liste alle +label_no=Nein +label_no_records_found=Kein {0} gefunden +label_save=Speichern +label_session_expired=Ihre session ist abgelaufen. Bitte das Spring Roo logo clicken um zurckzukehren. +label_update=Aktualisieren +label_view=Ansehen +label_welcome_titlepane=Willkommen zu {0} +label_welcome_text=Spring Roo bietet ein interaktives, leichtgewichtiges und vom Anwender anpassbares Tool, welches die schnelle Erstellung von performanten Enterprise Java Applikationen ermglicht. +label_yes=Ja +message_successfully_created={0} erfolgreich erstellt +message_successfully_updated={0} erfolgreich aktualisiert +message_successfully_deleted={0} erfolgreich gelscht +message_successfully_uploaded=Erfolgreich +message_successfully_uploaded_detail={0} ist hochgeladen. diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_en.properties b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_en.properties new file mode 100755 index 000000000..dc8e031bf --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_en.properties @@ -0,0 +1,26 @@ +label_actions=Actions +label_close=Close +label_confirm_deletion=Confirm deletion +label_create=Create +label_delete=Delete +label_delete_record=Are you sure you want to delete this record? +label_edit=Edit +label_find=Find by {0} +label_home=Home +label_language_switch=Switch language to {0} +label_language=Language +label_list=List all +label_no=No +label_no_records_found=No {0} found +label_save=Save +label_session_expired=Your session has expired. Click the Spring Roo logo to return. +label_update=Update +label_view=View +label_welcome_titlepane=Welcome to {0} +label_welcome_text=Spring Roo provides interactive, lightweight and user customizable tooling that enables rapid delivery of high performance enterprise Java applications. +label_yes=Yes +message_successfully_created={0} successfully created +message_successfully_updated={0} successfully updated +message_successfully_deleted={0} successfully deleted +message_successfully_uploaded=Successful +message_successfully_uploaded_detail={0} is uploaded. \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_es.properties b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_es.properties new file mode 100755 index 000000000..01ea9656d --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_es.properties @@ -0,0 +1,26 @@ +label_actions=Actions +label_close=Close +label_confirm_deletion=Confirm deletion +label_create=Create +label_delete=Delete +label_delete_record=Est seguro que desea eliminar este registro? +label_edit=Edit +label_find=Find by {0} +label_home=Home +label_language_switch=Switch language to {0} +label_language=Language +label_list=List all +label_no=No +label_no_records_found=No {0} found +label_save=Save +label_session_expired=Your session has expired. Click the Spring Roo logo to return. +label_update=Update +label_view=View +label_welcome_titlepane=Bienvenido a {0} +label_welcome_text=Spring Roo proporciona herramientas interactivas, ligeras y adaptables al usuario que permiten entregar rpidamente aplicaciones empresariales Java de alto rendimiento. +label_yes=S +message_successfully_created={0} creado con xito +message_successfully_updated={0} actualizado con xito +message_successfully_deleted={0} borrado con xito +message_successfully_uploaded=xito +message_successfully_uploaded_detail={0} se ha subido. \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_fr.properties b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_fr.properties new file mode 100644 index 000000000..a14ad9ed5 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/i18n/messages_fr.properties @@ -0,0 +1,26 @@ +label_actions=Actions +label_close=Fermer +label_confirm_deletion=Confirmer la suppression +label_create=Créer +label_delete=Supprimer +label_delete_record=Êtes-vous sûr de vouloir supprimer cette entrée ? +label_edit=Editer +label_find=Lister par {0} +label_home=Accueil +label_language_switch=Changer la langue pour {0} +label_language=Langue +label_list=Tout lister +label_no=Non +label_no_records_found=Aucun(e) {0} trouvé(e) +label_save=Sauvegarder +label_session_expired=Votre session a expiré. Cliquez sur le logo Spring Roo pour revenir en arrière. +label_update=Mettre à jour +label_view=Voir +label_welcome_titlepane=Bienvenue sur {0} +label_welcome_text=Spring Roo est un outil interactif, léger et customisable permettant la mise en place rapide d'applications Java Enterprise avec de grandes performances. +label_yes=Oui +message_successfully_created={0} a été créé(e) avec succès +message_successfully_updated={0} a été mis(e) à jour avec succès +message_successfully_deleted={0} a été supprimé(e) avec succès +message_successfully_uploaded=Successful +message_successfully_uploaded_detail={0} est uploadé(e). \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/index.html b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/index.html new file mode 100755 index 000000000..9d3e5189f --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/index.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/pages/content-template.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/pages/content-template.xhtml new file mode 100755 index 000000000..145dc2711 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/pages/content-template.xhtml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/pages/main.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/pages/main.xhtml new file mode 100755 index 000000000..58ce1223f --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/pages/main.xhtml @@ -0,0 +1,22 @@ + + + + + + + + + + +

    + +

    +

    + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/css/standard.css b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/css/standard.css new file mode 100755 index 000000000..b16d0a367 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/css/standard.css @@ -0,0 +1,88 @@ +body,div,td { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + word-wrap: break-word; +} + +body { + background-color: #fff; + text-align: left; +} + +a img { + border: 0 none; + vertical-align: middle; +} + +a { + text-decoration: underline; +} + +a:hover { + color: #456314; +} + +a:active { + color: #7db223; +} + +a:visited { + color: #7db223; +} + +.wrapper { + width: 1024px; + min-width: 1024px; + max-width: 1024px; + margin-right: auto; + margin-left: auto; /* fix max-width incompatibility in IE6 */ + width: expression(document.body.clientWidth > 1024 ? "1024px" : "auto"); + overflow: hidden; + display: block; +} + +.menu { + width: 230px; + vertical-align: top; + text-align: center; + margin-right: 5px; + word-wrap: break-word; +} + +.content { + width: 770px; + height: auto; + vertical-align: top; + text-align: left; +} + +.banner { + margin-bottom:5px; +} + +.action-column { + text-align: center; + width: 120px !important; +} + +.ui-selectmanymenu { + width: 155px !important; + height: 100px !important; +} + +.dialog { + width: auto; +} + +.col1 { + vertical-align: top; + font-weight: bold; +} + +.col2 { + vertical-align: top; +} + +.col3 { + vertical-align: top; +} \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/csv.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/csv.png new file mode 100644 index 000000000..68aeb18a4 Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/csv.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/de.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/de.png new file mode 100755 index 000000000..ac4a97736 Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/de.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/en.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/en.png new file mode 100755 index 000000000..ff701e19f Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/en.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/es.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/es.png new file mode 100755 index 000000000..c2de2d711 Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/es.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/excel.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/excel.png new file mode 100644 index 000000000..373755c8b Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/excel.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/favicon.ico b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/favicon.ico new file mode 100755 index 000000000..c2f2b6ccd Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/favicon.ico differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/fr.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/fr.png new file mode 100644 index 000000000..aea81ce54 Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/fr.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/pdf.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/pdf.png new file mode 100644 index 000000000..d2d848a07 Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/pdf.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/roo_logo.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/roo_logo.png new file mode 100644 index 000000000..acda9573c Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/roo_logo.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/springsource-logo.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/springsource-logo.png new file mode 100755 index 000000000..e170f8abf Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/springsource-logo.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/xml.png b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/xml.png new file mode 100644 index 000000000..d6109fc1b Binary files /dev/null and b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/images/xml.png differ diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/js/calendar.js b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/js/calendar.js new file mode 100644 index 000000000..293c453e1 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/resources/js/calendar.js @@ -0,0 +1,54 @@ +PrimeFaces.locales['es'] = { + closeText: 'Cerrar', + prevText: 'Anterior', + nextText: 'Siguiente', + currentText: 'Inicio', + monthNames: ['Enero','Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'], + monthNamesShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun','Jul','Ago','Sep','Oct','Nov','Dic'], + dayNames: ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'], + dayNamesShort: ['Dom','Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'], + dayNamesMin: ['D','L','M','X','J','V','S'], + weekHeader: 'Semana', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '', + timeOnlyTitle: 'Sólo hora', + timeText: 'Tempo', + hourText: 'Hora', + minuteText: 'Minuto', + secondText: 'Segundo', + currentText: 'Fecha actual', + ampm: false, + month: 'Mes', + week: 'Semana', + day: 'Día', + allDayText : 'Todo el día' + }; +PrimeFaces.locales['de'] = { + closeText: 'Schließen', + prevText: 'Zurück', + nextText: 'Weiter', + currentText: 'Start', + monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + monthNamesShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], + dayNames: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + dayNamesShort: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'], + dayNamesMin: ['S', 'M', 'D', 'M ', 'D', 'F ', 'S'], + weekHeader: 'Woche', + FirstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '', + timeOnlyTitle: 'Nur Zeit', + timeText: 'Zeit', + hourText: 'Stunde', + minuteText: 'Minute', + secondText: 'Sekunde', + currentText: 'Aktuelles Datum', + ampm: false, + month: 'Monat', + week: 'Woche', + day: 'Tag', + allDayText: 'Ganzer Tag' + }; \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/content.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/content.xhtml new file mode 100755 index 000000000..02684a1f7 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/content.xhtml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/footer.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/footer.xhtml new file mode 100755 index 000000000..490309f80 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/footer.xhtml @@ -0,0 +1,71 @@ + + + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/header.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/header.xhtml new file mode 100755 index 000000000..7ba32611d --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/header.xhtml @@ -0,0 +1,12 @@ + + + + #{applicationBean.appName} + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/layout.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/layout.xhtml new file mode 100755 index 000000000..8da52179e --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/layout.xhtml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/menu.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/menu.xhtml new file mode 100644 index 000000000..736ceaf1e --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/templates/menu.xhtml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/viewExpired.xhtml b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/viewExpired.xhtml new file mode 100644 index 000000000..ef0fcd092 --- /dev/null +++ b/addon-jsf/src/main/resources/org/springframework/roo/addon/jsf/viewExpired.xhtml @@ -0,0 +1,37 @@ + + + + + + #{applicationBean.appName} + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-json/pom.xml b/addon-json/pom.xml new file mode 100644 index 000000000..05109e8f1 --- /dev/null +++ b/addon-json/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.json + bundle + Spring Roo - Addon - JSON + Support for JSON document (de-)serialization of project domain entities. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.bootstrap + + + org.springframework.roo + org.springframework.roo.classpath + + + \ No newline at end of file diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/CustomDataJsonTags.java b/addon-json/src/main/java/org/springframework/roo/addon/json/CustomDataJsonTags.java new file mode 100644 index 000000000..c420c1b1c --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/CustomDataJsonTags.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.json; + +import org.springframework.roo.model.CustomData; + +/** + * {@link CustomData} tag definitions for json-related functionality. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +public enum CustomDataJsonTags { + FROM_JSON_ARRAY_METHOD, FROM_JSON_METHOD, TO_JSON_ARRAY_METHOD, TO_JSON_METHOD; +} diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/JsonAnnotationValues.java b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonAnnotationValues.java new file mode 100644 index 000000000..25680c9f6 --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonAnnotationValues.java @@ -0,0 +1,63 @@ +package org.springframework.roo.addon.json; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooJson} annotation. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class JsonAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate boolean deepSerialize; + @AutoPopulate boolean iso8601Dates; + @AutoPopulate String fromJsonArrayMethod = "fromJsonArrayTo"; + @AutoPopulate String fromJsonMethod = "fromJsonTo"; + @AutoPopulate String rootName = ""; + @AutoPopulate String toJsonArrayMethod = "toJsonArray"; + @AutoPopulate String toJsonMethod = "toJson"; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public JsonAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_JSON); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getFromJsonArrayMethod() { + return fromJsonArrayMethod; + } + + public String getFromJsonMethod() { + return fromJsonMethod; + } + + public String getRootName() { + return rootName; + } + + public String getToJsonArrayMethod() { + return toJsonArrayMethod; + } + + public String getToJsonMethod() { + return toJsonMethod; + } + + public boolean isDeepSerialize() { + return deepSerialize; + } + + public boolean isIso8601Dates() { + return iso8601Dates; + } +} diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/JsonCommands.java b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonCommands.java new file mode 100644 index 000000000..b45bc64dd --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonCommands.java @@ -0,0 +1,48 @@ +package org.springframework.roo.addon.json; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for addon-json + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class JsonCommands implements CommandMarker { + + @Reference private JsonOperations jsonOperations; + + @CliCommand(value = "json add", help = "Adds @RooJson annotation to target type") + public void add( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The java type to apply this annotation to") final JavaType target, + @CliOption(key = "rootName", mandatory = false, help = "The root name which should be used to wrap the JSON document") final String rootName, + @CliOption(key = "deepSerialize", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Indication if deep serialization should be enabled.") final boolean deep, + @CliOption(key = "iso8601Dates", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Indication if dates should be formatted according to ISO 8601") final boolean iso8601Dates) { + + jsonOperations.annotateType(target, rootName, deep); + } + + @CliCommand(value = "json all", help = "Adds @RooJson annotation to all types annotated with @RooJavaBean") + public void all( + @CliOption(key = "deepSerialize", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Indication if deep serialization should be enabled") final boolean deep, + @CliOption(key = "iso8601Dates", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Indication if dates should be formatted according to ISO 8601") final boolean iso8601Dates) { + + jsonOperations.annotateAll(deep); + } + + @CliAvailabilityIndicator({ "json setup", "json add", "json all" }) + public boolean isPropertyAvailable() { + return jsonOperations.isJsonInstallationPossible(); + } +} \ No newline at end of file diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/JsonMetadata.java b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonMetadata.java new file mode 100644 index 000000000..59063d7b4 --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonMetadata.java @@ -0,0 +1,347 @@ +package org.springframework.roo.addon.json; + +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; +import static org.springframework.roo.model.JdkJavaType.COLLECTION; +import static org.springframework.roo.model.JdkJavaType.LIST; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata to be triggered by {@link RooJson} annotation + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class JsonMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final JavaType JSON_DESERIALIZER = new JavaType( + "flexjson.JSONDeserializer"); + private static final JavaType JSON_SERIALIZER = new JavaType( + "flexjson.JSONSerializer"); + + private static final String PROVIDES_TYPE_STRING = JsonMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private JsonAnnotationValues annotationValues; + + private String typeNamePlural; + + public JsonMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String typeNamePlural, + final JsonAnnotationValues annotationValues) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notBlank(typeNamePlural, "Plural of the target type required"); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + + if (!isValid()) { + return; + } + + this.annotationValues = annotationValues; + this.typeNamePlural = typeNamePlural; + + builder.addMethod(getToJsonMethod(false)); + builder.addMethod(getToJsonMethod(true)); + builder.addMethod(getFromJsonMethod()); + builder.addMethod(getToJsonArrayMethod(false)); + builder.addMethod(getToJsonArrayMethod(true)); + builder.addMethod(getFromJsonArrayMethod()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private MethodMetadataBuilder getFromJsonArrayMethod() { + // Compute the relevant method name + final JavaSymbolName methodName = getFromJsonArrayMethodName(); + if (methodName == null) { + return null; + } + + final JavaType parameterType = JavaType.STRING; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final String list = LIST.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final String arrayList = ARRAY_LIST.getNameIncludingTypeParameters( + false, builder.getImportRegistrationResolver()); + final String bean = destination.getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String deserializer = JSON_DESERIALIZER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + bodyBuilder.appendFormalLine("return new " + deserializer + "<" + list + + "<" + bean + ">>()"); + if (annotationValues.isIso8601Dates()) { + bodyBuilder + .appendFormalLine(".use(java.util.Date.class, " + + "new flexjson.transformer.DateTransformer(\"yyyy-MM-dd\"))"); + } + bodyBuilder.appendFormalLine(".use(\"values\", " + bean + + ".class).deserialize(json);"); + + final List parameterNames = Arrays + .asList(new JavaSymbolName("json")); + + final JavaType collection = new JavaType( + COLLECTION.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(destination)); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + collection, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + methodBuilder.putCustomData(CustomDataJsonTags.FROM_JSON_ARRAY_METHOD, + null); + return methodBuilder; + } + + public JavaSymbolName getFromJsonArrayMethodName() { + final String methodLabel = annotationValues.getFromJsonArrayMethod(); + if (StringUtils.isBlank(methodLabel)) { + return null; + } + + return new JavaSymbolName(methodLabel.replace("", + typeNamePlural)); + } + + private MethodMetadataBuilder getFromJsonMethod() { + final JavaSymbolName methodName = getFromJsonMethodName(); + if (methodName == null) { + return null; + } + + final JavaType parameterType = JavaType.STRING; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String deserializer = JSON_DESERIALIZER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + bodyBuilder.appendFormalLine("return new " + deserializer + "<" + + destination.getSimpleTypeName() + + ">()"); + if (annotationValues.isIso8601Dates()) { + bodyBuilder + .appendFormalLine(".use(java.util.Date.class, " + + "new flexjson.transformer.DateTransformer(\"yyyy-MM-dd\"))"); + } + bodyBuilder.appendFormalLine(".use(null, " + + destination.getSimpleTypeName() + + ".class).deserialize(json);"); + + final List parameterNames = Arrays + .asList(new JavaSymbolName("json")); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + destination, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + methodBuilder.putCustomData(CustomDataJsonTags.FROM_JSON_METHOD, null); + return methodBuilder; + } + + public JavaSymbolName getFromJsonMethodName() { + final String methodLabel = annotationValues.getFromJsonMethod(); + if (StringUtils.isBlank(methodLabel)) { + return null; + } + + // Compute the relevant method name + return new JavaSymbolName(methodLabel.replace("", + destination.getSimpleTypeName())); + } + + private MethodMetadataBuilder getToJsonArrayMethod(boolean includeParams) { + // Compute the relevant method name + final JavaSymbolName methodName = getToJsonArrayMethodName(); + if (methodName == null) { + return null; + } + + final JavaType parameterType = new JavaType(Collection.class.getName(), + 0, DataType.TYPE, null, Arrays.asList(destination)); + + // See if the type itself declared the method + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final List parameterNames = new ArrayList(); + parameterNames.add(new JavaSymbolName("collection")); + + final List parameterTypes = AnnotatedJavaType + .convertFromJavaTypes(parameterType); + + if (includeParams) { + parameterTypes.add(new AnnotatedJavaType(JavaType.STRING_ARRAY)); + parameterNames.add(new JavaSymbolName("fields")); + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String serializer = JSON_SERIALIZER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final String root = annotationValues.getRootName() != null + && annotationValues.getRootName().length() > 0 ? ".rootName(\"" + + annotationValues.getRootName() + "\")" : ""; + bodyBuilder.appendFormalLine("return new " + serializer + "()" + root); + if (annotationValues.isIso8601Dates()) { + bodyBuilder + .appendFormalLine(".transform(" + + "new flexjson.transformer.DateTransformer" + + "(\"yyyy-MM-dd\"), java.util.Date.class)"); + } + bodyBuilder + .appendFormalLine( + (!includeParams ? "" : ".include(fields)") + + ".exclude(\"*.class\")" + + (annotationValues.isDeepSerialize() ? ".deepSerialize(collection)" + : ".serialize(collection)") + ";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.putCustomData(CustomDataJsonTags.TO_JSON_ARRAY_METHOD, + null); + return methodBuilder; + } + + public JavaSymbolName getToJsonArrayMethodName() { + final String methodLabel = annotationValues.getToJsonArrayMethod(); + if (StringUtils.isBlank(methodLabel)) { + return null; + } + return new JavaSymbolName(methodLabel); + } + + private MethodMetadataBuilder getToJsonMethod(boolean includeParams) { + // Compute the relevant method name + final JavaSymbolName methodName = getToJsonMethodName(); + if (methodName == null) { + return null; + } + + // See if the type itself declared the method + if (governorHasMethod(methodName)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String serializer = JSON_SERIALIZER + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final String root = annotationValues.getRootName() != null + && annotationValues.getRootName().length() > 0 ? ".rootName(\"" + + annotationValues.getRootName() + "\")" : ""; + bodyBuilder.appendFormalLine("return new " + + serializer + + "()" + + root); + if (annotationValues.isIso8601Dates()) { + bodyBuilder + .appendFormalLine(".transform(" + + "new flexjson.transformer.DateTransformer" + + "(\"yyyy-MM-dd\"), java.util.Date.class)"); + } + bodyBuilder.appendFormalLine( + (!includeParams ? "" : ".include(fields)") + + ".exclude(\"*.class\")" + + (annotationValues.isDeepSerialize() ? ".deepSerialize(this)" + : ".serialize(this)") + ";"); + + List parameterTypes = new ArrayList(); + List parameterNames = new ArrayList(); + + if (includeParams) { + parameterTypes.add(new AnnotatedJavaType(JavaType.STRING_ARRAY)); + parameterNames.add(new JavaSymbolName("fields")); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.putCustomData(CustomDataJsonTags.TO_JSON_METHOD, null); + return methodBuilder; + } + + public JavaSymbolName getToJsonMethodName() { + final String methodLabel = annotationValues.getToJsonMethod(); + if (StringUtils.isBlank(methodLabel)) { + return null; + } + return new JavaSymbolName(methodLabel); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/JsonMetadataProvider.java b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonMetadataProvider.java new file mode 100644 index 000000000..8fe7178e7 --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonMetadataProvider.java @@ -0,0 +1,95 @@ +package org.springframework.roo.addon.json; + +import static org.springframework.roo.model.RooJavaType.ROO_IDENTIFIER; +import static org.springframework.roo.model.RooJavaType.ROO_JSON; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Provides {@link JsonMetadata}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class JsonMetadataProvider extends AbstractItdMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTriggers(ROO_JSON, ROO_IDENTIFIER); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return JsonMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTriggers(ROO_JSON, ROO_IDENTIFIER); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = JsonMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JsonMetadata + .getPath(metadataIdentificationString); + final String physicalTypeIdentifier = PhysicalTypeIdentifier + .createIdentifier(javaType, path); + return physicalTypeIdentifier; + } + + public String getItdUniquenessFilenameSuffix() { + return "Json"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // Acquire bean info (we need getters details, specifically) + final JavaType javaType = JsonMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = JsonMetadata + .getPath(metadataIdentificationString); + + // We need to parse the annotation, if it is not present we will simply + // get the default annotation values + final JsonAnnotationValues annotationValues = new JsonAnnotationValues( + governorPhysicalTypeMetadata); + + String plural = javaType.getSimpleTypeName() + "s"; + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(PluralMetadata.createIdentifier(javaType, path)); + if (pluralMetadata != null) { + plural = pluralMetadata.getPlural(); + } + + return new JsonMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, plural, annotationValues); + } + + public String getProvidesType() { + return JsonMetadata.getMetadataIdentiferType(); + } +} \ No newline at end of file diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/JsonOperations.java b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonOperations.java new file mode 100644 index 000000000..892d3d673 --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonOperations.java @@ -0,0 +1,89 @@ +package org.springframework.roo.addon.json; + +import org.springframework.roo.model.JavaType; + +/** + * Interface of operations for addon-json operations. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface JsonOperations { + + /** + * Annotate all types in the project which are annotated with @ + * {@link org.springframework.roo.addon.javabean.RooJavaBean}. + */ + @Deprecated + void annotateAll(); + + /** + * Annotate all types in the project which are annotated with @ + * {@link org.springframework.roo.addon.javabean.RooJavaBean}. + * + * @param deepSerialize Indication if deep serialization should be enabled + * (optional) + */ + @Deprecated + void annotateAll(boolean deepSerialize); + + /** + * Annotate all types in the project which are annotated with @ + * {@link org.springframework.roo.addon.javabean.RooJavaBean}. + * + * @param deepSerialize + * Indication if deep serialization should be enabled (optional) + * @param iso8601Dates + * Indication if dates should be de/serialized in ISO 8601 format + * (optional) + */ + void annotateAll(boolean deepSerialize, boolean iso8601Dates); + + /** + * Annotate a given {@link JavaType} with @{@link RooJson} annotation. + * + * @param type The type to annotate (required) + * @param rootName The root name which should be used to wrap the JSON + * document (optional) + */ + @Deprecated + void annotateType(JavaType type, String rootName); + + /** + * Annotate a given {@link JavaType} with @{@link RooJson} annotation. + * + * @param type + * The type to annotate (required) + * @param rootName + * The root name which should be used to wrap the JSON document + * (optional) + * @param deepSerialize + * Indication if deep serialization should be enabled (optional) + */ + @Deprecated + void annotateType(JavaType type, String rootName, boolean deepSerialize); + + /** + * Annotate a given {@link JavaType} with @{@link RooJson} annotation. + * + * @param type + * The type to annotate (required) + * @param rootName + * The root name which should be used to wrap the JSON document + * (optional) + * @param deepSerialize + * Indication if deep serialization should be enabled (optional) + * @param iso8601Dates + * Indication if dates should be de/serialized in ISO 8601 format + * (optional) + */ + void annotateType(JavaType type, String rootName, boolean deepSerialize, + boolean iso8601Dates); + + /** + * Indicates whether this commands for this add-on should be available. + * + * @return true if commands are available + */ + boolean isJsonInstallationPossible(); +} \ No newline at end of file diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/JsonOperationsImpl.java b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonOperationsImpl.java new file mode 100644 index 000000000..e42a221b3 --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/JsonOperationsImpl.java @@ -0,0 +1,92 @@ +package org.springframework.roo.addon.json; + +import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.ProjectOperations; + +/** + * Implementation of addon-json operations interface. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class JsonOperationsImpl implements JsonOperations { + + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void annotateAll() { + annotateAll(false, false); + } + + public void annotateAll(final boolean deepSerialize) { + annotateAll(deepSerialize, false); + } + + public void annotateAll(final boolean deepSerialize, + final boolean iso8601Dates) { + for (final JavaType type : typeLocationService + .findTypesWithAnnotation(ROO_JAVA_BEAN)) { + annotateType(type, "", deepSerialize, iso8601Dates); + } + } + + public void annotateType(final JavaType javaType, final String rootName) { + annotateType(javaType, rootName, false); + } + + public void annotateType(final JavaType javaType, final String rootName, + final boolean deepSerialize) { + annotateType(javaType, rootName, false, false); + } + + public void annotateType(final JavaType javaType, final String rootName, + final boolean deepSerialize, final boolean iso8601Dates) { + Validate.notNull(javaType, "Java type required"); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(javaType); + if (cid == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + javaType.getFullyQualifiedTypeName() + "'"); + } + + if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), + RooJavaType.ROO_JSON) == null) { + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + RooJavaType.ROO_JSON); + if (rootName != null && rootName.length() > 0) { + annotationBuilder.addStringAttribute("rootName", rootName); + } + if (deepSerialize) { + annotationBuilder.addBooleanAttribute("deepSerialize", true); + } + if (iso8601Dates) { + annotationBuilder.addBooleanAttribute("iso8601Dates", true); + } + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + cidBuilder.addAnnotation(annotationBuilder); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + } + + public boolean isJsonInstallationPossible() { + return projectOperations.isFocusedProjectAvailable(); + } +} \ No newline at end of file diff --git a/addon-json/src/main/java/org/springframework/roo/addon/json/RooJson.java b/addon-json/src/main/java/org/springframework/roo/addon/json/RooJson.java new file mode 100644 index 000000000..a36cc420b --- /dev/null +++ b/addon-json/src/main/java/org/springframework/roo/addon/json/RooJson.java @@ -0,0 +1,77 @@ +package org.springframework.roo.addon.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Trigger annotation for addon-json + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJson { + + /** + * Enable deep serialization of object graph + * + * @return an indication if deep serialization should be enabled (defaults + * to false; optional) + */ + boolean deepSerialize() default false; + + /** + * Enable formatting of dates as specified by ISO 8601. + * + * @return an indication if dates should be formatted according to ISO 8601 + * (defaults to false; optional) + */ + boolean iso8601Dates() default false; + + /** + * Specify name of the "fromJsonArrayTo" method to generate. + * Use a value of "" to avoid the generation of this method. + * + * @return the name of the "fromJsonArrayTo" method to + * generate (defaults to "fromJsonArrayTo"; + * mandatory) + */ + String fromJsonArrayMethod() default "fromJsonArrayTo"; + + /** + * Specify name of the "fromJsonTo" method to generate. Use a + * value of "" to avoid the generation of this method. + * + * @return the name of the "fromJsonTo" method to generate + * (defaults to "fromJsonTo"; mandatory) + */ + String fromJsonMethod() default "fromJsonTo"; + + /** + * Specify the root name of the JSON document. + * + * @return the custom root name (optional) + */ + String rootName() default ""; + + /** + * Specify name of the "toJsonArray" method to generate. Use a value of "" + * to avoid the generation of this method. + * + * @return the name of the "toJsonArray" method to generate (defaults to + * "toJsonArray"; mandatory) + */ + String toJsonArrayMethod() default "toJsonArray"; + + /** + * Specify name of the "toJson" method to generate. Use a value of "" to + * avoid the generation of this method. + * + * @return the name of the "toJson" method to generate (defaults to + * "toJson"; mandatory) + */ + String toJsonMethod() default "toJson"; +} diff --git a/addon-json/src/main/resources/org/springframework/roo/addon/json/configuration.xml b/addon-json/src/main/resources/org/springframework/roo/addon/json/configuration.xml new file mode 100644 index 000000000..d9be7aee4 --- /dev/null +++ b/addon-json/src/main/resources/org/springframework/roo/addon/json/configuration.xml @@ -0,0 +1,12 @@ + + + + + + net.sf.flexjson + flexjson + 2.1 + + + + \ No newline at end of file diff --git a/addon-json/src/test/java/org/springframework/roo/addon/json/JsonAnnotationValuesTest.java b/addon-json/src/test/java/org/springframework/roo/addon/json/JsonAnnotationValuesTest.java new file mode 100644 index 000000000..2fcbde8cd --- /dev/null +++ b/addon-json/src/test/java/org/springframework/roo/addon/json/JsonAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.json; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link JsonAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JsonAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooJson.class; + } + + @Override + protected Class getValuesClass() { + return JsonAnnotationValues.class; + } +} diff --git a/addon-layers-repository-jpa/pom.xml b/addon-layers-repository-jpa/pom.xml new file mode 100644 index 000000000..770af17a8 --- /dev/null +++ b/addon-layers-repository-jpa/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.layers.repository.jpa + bundle + Spring Roo - Addon - JPA Repository Layer + Support for common layering options in Java Enterprise Applications + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaAnnotationValues.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaAnnotationValues.java new file mode 100644 index 000000000..f42a7c871 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaAnnotationValues.java @@ -0,0 +1,40 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * The values of a {@link RooJpaRepository} annotation. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public class RepositoryJpaAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private JavaType domainType; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata the metadata to parse (required) + */ + public RepositoryJpaAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_REPOSITORY_JPA); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Returns the domain type managed by the annotated repository + * + * @return a non-null type + */ + public JavaType getDomainType() { + return domainType; + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaCommands.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaCommands.java new file mode 100644 index 000000000..b2d6f2ee2 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaCommands.java @@ -0,0 +1,38 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the JPA repository add-on. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryJpaCommands implements CommandMarker { + + @Reference private RepositoryJpaOperations repositoryJpaOperations; + + @CliAvailabilityIndicator("repository jpa") + public boolean isRepositoryCommandAvailable() { + return repositoryJpaOperations.isRepositoryInstallationPossible(); + } + + @CliCommand(value = "repository jpa", help = "Adds @RooJpaRepository annotation to target type") + public void repository( + @CliOption(key = "interface", mandatory = true, help = "The java interface to apply this annotation to") final JavaType interfaceType, + @CliOption(key = "entity", unspecifiedDefaultValue = "*", optionContext = PROJECT, mandatory = false, help = "The domain entity this repository should expose") final JavaType domainType) { + + repositoryJpaOperations.setupRepository(interfaceType, domainType); + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerMethod.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerMethod.java new file mode 100644 index 000000000..011ce0242 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerMethod.java @@ -0,0 +1,244 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FLUSH_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * A method provided by the {@link LayerType#REPOSITORY} layer. + *

    + * A Spring Data JPA repository provides the following methods out of the box, + * of which those marked * are not implemented below: + * + *

    + *     long         count()
    + *     *void        delete(ID)
    + *     *void        delete(Iterable)
    + *     void         delete(T)
    + *     *void        deleteAll()
    + *     *void        deleteInBatch(Iterable)
    + *     *boolean     exists(ID)
    + *     List      findAll()
    + *     *org.springframework.data.domain.Page findAll(org.springframework.data.domain.Pageable)
    + *     *List     findAll(org.springframework.data.domain.Sort)
    + *     T            findOne(ID)
    + *     void         flush()
    + *     *List     save(Iterable)
    + *     T            save(T)
    + *     *T           saveAndFlush(T)
    + * 
    + * + * @author Andrew Swan + * @since 1.2.0 + */ +public enum RepositoryJpaLayerMethod { + + COUNT("count", COUNT_ALL_METHOD) { + + @Override + public String getCall(final List parameters) { + return "count()"; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + }, + + /** + * Deletes the passed-in entity (does not delete by ID). + */ + DELETE("delete", REMOVE_METHOD) { + + @Override + public String getCall(final List parameters) { + return "delete(" + parameters.get(0).getValue() + ")"; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + }, + + FIND("find", FIND_METHOD) { + + @Override + public String getCall(final List parameters) { + return "findOne(" + parameters.get(0).getValue() + ")"; + } + + @Override + protected List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays.asList(idType); + } + }, + + FIND_ALL("findAll", FIND_ALL_METHOD) { + + @Override + public String getCall(final List parameters) { + return "findAll()"; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + }, + + /** + * Finds entities starting from a given zero-based index, up to a given + * maximum number of results. This method isn't directly implemented by + * Spring Data JPA, so we use its findAll(Pageable) API. + */ + FIND_ENTRIES("findEntries", FIND_ENTRIES_METHOD) { + + @Override + public String getCall(final List parameters) { + final JavaSymbolName firstResultParameter = parameters.get(0) + .getValue(); + final JavaSymbolName maxResultsParameter = parameters.get(1) + .getValue(); + final String pageNumberExpression = firstResultParameter + " / " + + maxResultsParameter; + return "findAll(new org.springframework.data.domain.PageRequest(" + + pageNumberExpression + ", " + maxResultsParameter + + ")).getContent()"; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays + .asList(JavaType.INT_PRIMITIVE, JavaType.INT_PRIMITIVE); + } + }, + + FLUSH("flush", FLUSH_METHOD) { + + @Override + public String getCall(final List parameters) { + return "flush()"; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + // Even though Spring Data JPA's flush() method doesn't take a + // parameter, the caller provides one, so we list it here. + return Arrays.asList(targetEntity); + } + }, + + /** + * Spring Data JPA makes no distinction between + * create/persist/save/update/merge + */ + SAVE("save", MERGE_METHOD, PERSIST_METHOD) { + + @Override + public String getCall(final List parameters) { + return "save(" + parameters.get(0).getValue() + ")"; + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + }; + + /** + * Returns the {@link RepositoryJpaLayerMethod} with the given ID and + * parameter types. + * + * @param methodId the ID to match upon + * @param parameterTypes the parameter types to match upon + * @param targetEntity the entity type being managed by the repository + * @param idType specifies the ID type used by the target entity (required) + * @return null if no such method exists + */ + public static RepositoryJpaLayerMethod valueOf(final String methodId, + final List parameterTypes, final JavaType targetEntity, + final JavaType idType) { + for (final RepositoryJpaLayerMethod method : values()) { + if (method.ids.contains(methodId) + && method.getParameterTypes(targetEntity, idType).equals( + parameterTypes)) { + return method; + } + } + return null; + } + + private final List ids; + private final String name; + + /** + * Constructor + * + * @param key the unique key for this method (required) + * @param name the Java name of this method (required) + */ + private RepositoryJpaLayerMethod(final String name, + final MethodMetadataCustomDataKey... keys) { + Validate.notBlank(name, "Name is required"); + Validate.isTrue(keys.length > 0, "One or more ids are required"); + ids = new ArrayList(); + for (final MethodMetadataCustomDataKey key : keys) { + ids.add(key.name()); + } + this.name = name; + } + + /** + * Returns a Java snippet that invokes this method (minus the target) + * + * @param parameters the parameters used by the caller; can be + * null + * @return a non-blank Java snippet + */ + public abstract String getCall(List parameters); + + /** + * Returns the name of this method + * + * @return a non-blank name + */ + public String getName() { + return name; + } + + /** + * Instances must return the types of parameters they take + * + * @param targetEntity the type of entity being managed (required) + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null list + */ + protected abstract List getParameterTypes(JavaType targetEntity, + JavaType idType); +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerProvider.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerProvider.java new file mode 100644 index 000000000..33f5e10cc --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerProvider.java @@ -0,0 +1,124 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.layers.CoreLayerProvider; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.PairList; + +/** + * A provider of the {@link LayerType#REPOSITORY} layer. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryJpaLayerProvider extends CoreLayerProvider { + + @Reference private RepositoryJpaLocator repositoryLocator; + + public int getLayerPosition() { + return LayerType.REPOSITORY.getPosition(); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final MethodParameter... callerParameters) { + return getMemberTypeAdditions(callerMID, methodIdentifier, + targetEntity, idType, true, callerParameters); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, boolean autowire, + final MethodParameter... callerParameters) { + Validate.isTrue(StringUtils.isNotBlank(callerMID), + "Caller's metadata ID required"); + Validate.notBlank(methodIdentifier, "Method identifier required"); + Validate.notNull(targetEntity, "Target enitity type required"); + Validate.notNull(idType, "Enitity Id type required"); + + // Look for a repository layer method with this ID and parameter types + final List parameterTypes = new PairList( + callerParameters).getKeys(); + final RepositoryJpaLayerMethod method = RepositoryJpaLayerMethod + .valueOf(methodIdentifier, parameterTypes, targetEntity, idType); + if (method == null) { + return null; + } + + // Look for repositories that support this domain type + final Collection repositories = repositoryLocator + .getRepositories(targetEntity); + if (CollectionUtils.isEmpty(repositories)) { + return null; + } + + // Use the first such repository (could refine this later) + final ClassOrInterfaceTypeDetails repository = repositories.iterator() + .next(); + + // Return the additions the caller needs to make + return getMethodAdditions(callerMID, method, repository.getName(), + Arrays.asList(callerParameters)); + } + + /** + * Returns the additions that the caller needs to make in order to invoke + * the given method + * + * @param callerMID the caller's metadata ID (required) + * @param method the method being called (required) + * @param repositoryType the type of repository being called + * @param parameterNames the parameter names used by the caller + * @return a non-null set of additions + */ + private MemberTypeAdditions getMethodAdditions(final String callerMID, + final RepositoryJpaLayerMethod method, + final JavaType repositoryType, + final List parameters) { + // Create a builder to hold the repository field to be copied into the + // caller + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + callerMID); + final AnnotationMetadataBuilder autowiredAnnotation = new AnnotationMetadataBuilder( + AUTOWIRED); + final String repositoryFieldName = StringUtils + .uncapitalize(repositoryType.getSimpleTypeName()); + cidBuilder.addField(new FieldMetadataBuilder(callerMID, 0, Arrays + .asList(autowiredAnnotation), new JavaSymbolName( + repositoryFieldName), repositoryType)); + + // Create the additions to invoke the given method on this field + final String methodCall = repositoryFieldName + "." + + method.getCall(parameters); + return new MemberTypeAdditions(cidBuilder, method.getName(), + methodCall, false, parameters); + } + + // -------------------- Setters for use by unit tests ---------------------- + + void setRepositoryLocator(final RepositoryJpaLocator repositoryLocator) { + this.repositoryLocator = repositoryLocator; + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLocator.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLocator.java new file mode 100644 index 000000000..6f6a21ae3 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLocator.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import java.util.Collection; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; + +/** + * Locates Spring Data JPA Repositories within the user's project + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface RepositoryJpaLocator { + + /** + * Returns the repositories that support the given domain type + * + * @param domainType the domain type for which to find the repositories; can + * be null + * @return a non-null collection + */ + Collection getRepositories( + final JavaType domainType); +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLocatorImpl.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLocatorImpl.java new file mode 100644 index 000000000..ac8186f52 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLocatorImpl.java @@ -0,0 +1,59 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.DefaultPhysicalTypeMetadata; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * The {@link RepositoryJpaLocator} implementation. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryJpaLocatorImpl implements RepositoryJpaLocator { + + private final Map> cacheMap = new HashMap>(); + @Reference private TypeLocationService typeLocationService; + + public Collection getRepositories( + final JavaType domainType) { + if (!cacheMap.containsKey(domainType)) { + cacheMap.put(domainType, new HashSet()); + } + final Set existing = cacheMap + .get(domainType); + final Set located = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_REPOSITORY_JPA); + if (existing.containsAll(located)) { + return existing; + } + final Map toReturn = new HashMap(); + for (final ClassOrInterfaceTypeDetails cid : located) { + final RepositoryJpaAnnotationValues annotationValues = new RepositoryJpaAnnotationValues( + new DefaultPhysicalTypeMetadata( + cid.getDeclaredByMetadataId(), + typeLocationService + .getPhysicalTypeCanonicalPath(cid + .getDeclaredByMetadataId()), cid)); + if (annotationValues.getDomainType() != null + && annotationValues.getDomainType().equals(domainType)) { + toReturn.put(cid.getDeclaredByMetadataId(), cid); + } + } + return toReturn.values(); + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadata.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadata.java new file mode 100644 index 000000000..06a7fcc68 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadata.java @@ -0,0 +1,110 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import java.util.Arrays; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooJpaRepository}. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public class RepositoryJpaMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = RepositoryJpaMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final String SPRING_JPA_REPOSITORY = "org.springframework.data.jpa.repository.JpaRepository"; + private static final String SPRING_JPA_SPECIFICATION_EXECUTOR = "org.springframework.data.jpa.repository.JpaSpecificationExecutor"; + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + /** + * Constructor + * + * @param identifier the identifier for this item of metadata (required) + * @param aspectName the Java type of the ITD (required) + * @param governorPhysicalTypeMetadata the governor, which is expected to + * contain a {@link ClassOrInterfaceTypeDetails} (required) + * @param annotationValues (required) + * @param identifierType the type of the entity's identifier field + * (required) + */ + public RepositoryJpaMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final RepositoryJpaAnnotationValues annotationValues, + final JavaType identifierType) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(identifierType, "Id type required"); + + // Make the user's Repository interface extend Spring Data's + // JpaRepository interface if it doesn't already + ensureGovernorExtends(new JavaType(SPRING_JPA_REPOSITORY, 0, + DataType.TYPE, null, Arrays.asList( + annotationValues.getDomainType(), identifierType))); + + // ... and likewise extend JpaSpecificationExecutor, to allow query + // by specification + ensureGovernorExtends(new JavaType(SPRING_JPA_SPECIFICATION_EXECUTOR, + 0, DataType.TYPE, null, Arrays.asList(annotationValues + .getDomainType()))); + + builder.addAnnotation(new AnnotationMetadataBuilder( + SpringJavaType.REPOSITORY)); + + // Build the ITD + itdTypeDetails = builder.build(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadataProvider.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadataProvider.java new file mode 100644 index 000000000..d6655f080 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadataProvider.java @@ -0,0 +1,14 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides the metadata for an ITD that implements a Spring Data JPA + * repository. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface RepositoryJpaMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadataProviderImpl.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadataProviderImpl.java new file mode 100644 index 000000000..c03caec06 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaMetadataProviderImpl.java @@ -0,0 +1,178 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.springframework.roo.model.RooJavaType.ROO_REPOSITORY_JPA; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerTypeMatcher; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link RepositoryJpaMetadataProvider}. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryJpaMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + RepositoryJpaMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(RepositoryJpaMetadataProviderImpl.class); + + private CustomDataKeyDecorator customDataKeyDecorator; + private final Map domainTypeToRepositoryMidMap = new LinkedHashMap(); + private final Map repositoryMidToDomainTypeMap = new LinkedHashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + super.setDependsOnGovernorBeingAClass(false); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_REPOSITORY_JPA); + registerMatchers(); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return RepositoryJpaMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_REPOSITORY_JPA); + getCustomDataKeyDecorator().unregisterMatchers(getClass()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = RepositoryJpaMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = RepositoryJpaMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Jpa_Repository"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = domainTypeToRepositoryMidMap.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = domainTypeToRepositoryMidMap + .get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final RepositoryJpaAnnotationValues annotationValues = new RepositoryJpaAnnotationValues( + governorPhysicalTypeMetadata); + final JavaType domainType = annotationValues.getDomainType(); + final JavaType identifierType = getPersistenceMemberLocator() + .getIdentifierType(domainType); + if (identifierType == null) { + return null; + } + + // Remember that this entity JavaType matches up with this metadata + // identification string + // Start by clearing any previous association + final JavaType oldEntity = repositoryMidToDomainTypeMap + .get(metadataIdentificationString); + if (oldEntity != null) { + domainTypeToRepositoryMidMap.remove(oldEntity); + } + domainTypeToRepositoryMidMap.put(domainType, + metadataIdentificationString); + repositoryMidToDomainTypeMap.put(metadataIdentificationString, + domainType); + + return new RepositoryJpaMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, annotationValues, + identifierType); + } + + public String getProvidesType() { + return RepositoryJpaMetadata.getMetadataIdentiferType(); + } + + @SuppressWarnings("unchecked") + private void registerMatchers() { + getCustomDataKeyDecorator().registerMatchers(getClass(), + new LayerTypeMatcher(ROO_REPOSITORY_JPA, new JavaSymbolName( + RooJpaRepository.DOMAIN_TYPE_ATTRIBUTE))); + } + + public CustomDataKeyDecorator getCustomDataKeyDecorator(){ + if(customDataKeyDecorator == null){ + // Get all Services implement CustomDataKeyDecorator interface + try { + ServiceReference[] references = context.getAllServiceReferences(CustomDataKeyDecorator.class.getName(), null); + + for(ServiceReference ref : references){ + return (CustomDataKeyDecorator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CustomDataKeyDecorator on RepositoryJpaMetadataProviderImpl."); + return null; + } + }else{ + return customDataKeyDecorator; + } + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaOperations.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaOperations.java new file mode 100644 index 000000000..ad8e9a10a --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaOperations.java @@ -0,0 +1,15 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Feature; + +/** + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface RepositoryJpaOperations extends Feature { + + boolean isRepositoryInstallationPossible(); + + void setupRepository(JavaType interfaceType, JavaType domainType); +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaOperationsImpl.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaOperationsImpl.java new file mode 100644 index 000000000..59fee5845 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaOperationsImpl.java @@ -0,0 +1,246 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.springframework.roo.model.RooJavaType.ROO_REPOSITORY_JPA; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * The {@link RepositoryJpaOperations} implementation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryJpaOperationsImpl implements RepositoryJpaOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(RepositoryJpaOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private FileManager fileManager; + private PathResolver pathResolver; + private ProjectOperations projectOperations; + private TypeManagementService typeManagementService; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + private void configureProject() { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List springDependencies = XmlUtils.findElements( + "/configuration/spring-data-jpa/dependencies/dependency", + configuration); + for (final Element dependencyElement : springDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + + getProjectOperations().addDependencies( + getProjectOperations().getFocusedModuleName(), dependencies); + + final String appCtxId = getPathResolver().getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, "applicationContext-jpa.xml"); + if (getFileManager().exists(appCtxId)) { + return; + } + else { + InputStream templateInputStream = null; + OutputStream outputStream = null; + try { + templateInputStream = getClass().getResourceAsStream( + "applicationContext-jpa.xml"); + Validate.notNull(templateInputStream, + "Could not acquire 'applicationContext-jpa.xml' template"); + + String input = IOUtils.toString(templateInputStream); + input = input.replace("TO_BE_CHANGED_BY_ADDON", + getProjectOperations().getFocusedTopLevelPackage() + .getFullyQualifiedPackageName()); + final MutableFile mutableFile = getFileManager() + .createFile(appCtxId); + outputStream = mutableFile.getOutputStream(); + IOUtils.write(input, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to create '" + appCtxId + + "'", e); + } + finally { + IOUtils.closeQuietly(templateInputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + public String getName() { + return FeatureNames.JPA; + } + + public boolean isInstalledInModule(final String moduleName) { + final LogicalPath resourcesPath = LogicalPath.getInstance( + Path.SRC_MAIN_RESOURCES, moduleName); + return getProjectOperations().isFocusedProjectAvailable() + && getFileManager().exists(getProjectOperations().getPathResolver() + .getIdentifier(resourcesPath, + "META-INF/persistence.xml")); + } + + public boolean isRepositoryInstallationPossible() { + return isInstalledInModule(getProjectOperations().getFocusedModuleName()) + && !getProjectOperations() + .isFeatureInstalledInFocusedModule(FeatureNames.MONGO); + } + + public void setupRepository(final JavaType interfaceType, + final JavaType domainType) { + Validate.notNull(interfaceType, "Interface type required"); + Validate.notNull(domainType, "Domain type required"); + + final String interfaceIdentifier = getPathResolver() + .getFocusedCanonicalPath(Path.SRC_MAIN_JAVA, interfaceType); + + if (getFileManager().exists(interfaceIdentifier)) { + return; // Type exists already - nothing to do + } + + // Build interface type + final AnnotationMetadataBuilder interfaceAnnotationMetadata = new AnnotationMetadataBuilder( + ROO_REPOSITORY_JPA); + interfaceAnnotationMetadata.addAttribute(new ClassAttributeValue( + new JavaSymbolName("domainType"), domainType)); + final String interfaceMdId = PhysicalTypeIdentifier.createIdentifier( + interfaceType, getPathResolver().getPath(interfaceIdentifier)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + interfaceMdId, Modifier.PUBLIC, interfaceType, + PhysicalTypeCategory.INTERFACE); + cidBuilder.addAnnotation(interfaceAnnotationMetadata.build()); + getTypeManagementService().createOrUpdateTypeOnDisk(cidBuilder.build()); + + // Take care of project configuration + configureProject(); + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on RepositoryJpaOperationsImpl."); + return null; + } + }else{ + return fileManager; + } + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on RepositoryJpaOperationsImpl."); + return null; + } + }else{ + return pathResolver; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on RepositoryJpaOperationsImpl."); + return null; + } + }else{ + return projectOperations; + } + } + + public TypeManagementService getTypeManagementService(){ + if(typeManagementService == null){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on RepositoryJpaOperationsImpl."); + return null; + } + }else{ + return typeManagementService; + } + } +} diff --git a/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RooJpaRepository.java b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RooJpaRepository.java new file mode 100644 index 000000000..a4c41ea04 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/java/org/springframework/roo/addon/layers/repository/jpa/RooJpaRepository.java @@ -0,0 +1,34 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks the annotated type as a Spring Data JPA repository interface. For the + * time being, we don't allow users to customise the names of repository methods + * like we do for service interfaces, because Spring Data JPA provides a + * complete pre-named set of CRUD methods out of the box. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooJpaRepository { + + /** + * The name of this annotation's attribute that specifies the managed domain + * type. + */ + String DOMAIN_TYPE_ATTRIBUTE = "domainType"; + + /** + * The domain type managed by the annotated repository + * + * @return a non-null entity type + */ + Class domainType(); // No default => mandatory +} diff --git a/addon-layers-repository-jpa/src/main/resources/org/springframework/roo/addon/layers/repository/jpa/applicationContext-jpa.xml b/addon-layers-repository-jpa/src/main/resources/org/springframework/roo/addon/layers/repository/jpa/applicationContext-jpa.xml new file mode 100644 index 000000000..e5d41b796 --- /dev/null +++ b/addon-layers-repository-jpa/src/main/resources/org/springframework/roo/addon/layers/repository/jpa/applicationContext-jpa.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/addon-layers-repository-jpa/src/main/resources/org/springframework/roo/addon/layers/repository/jpa/configuration.xml b/addon-layers-repository-jpa/src/main/resources/org/springframework/roo/addon/layers/repository/jpa/configuration.xml new file mode 100644 index 000000000..25730a8ac --- /dev/null +++ b/addon-layers-repository-jpa/src/main/resources/org/springframework/roo/addon/layers/repository/jpa/configuration.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaAnnotationValuesTest.java b/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaAnnotationValuesTest.java new file mode 100644 index 000000000..f58745577 --- /dev/null +++ b/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaAnnotationValuesTest.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link RepositoryJpaAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class RepositoryJpaAnnotationValuesTest + extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooJpaRepository.class; + } + + @Override + protected Class getValuesClass() { + return RepositoryJpaAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerProviderTest.java b/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerProviderTest.java new file mode 100644 index 000000000..e883abab6 --- /dev/null +++ b/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryJpaLayerProviderTest.java @@ -0,0 +1,122 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FLUSH_METHOD; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link RepositoryJpaLayerProvider} + * + * @author Andrew Swan + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class RepositoryJpaLayerProviderTest { + + private static final String CALLER_MID = "MID:anything#com.example.PetService"; + + // Fixture + private RepositoryJpaLayerProvider layerProvider; + @Mock private JavaType mockIdType; + @Mock private RepositoryJpaLocator mockRepositoryLocator; + @Mock private JavaType mockTargetEntity; + + /** + * Asserts that the {@link RepositoryJpaLayerProvider} generates the + * expected call for the given method with the given parameters + * + * @param expectedMethodCall + * @param methodKey + * @param callerParameters + */ + private void assertMethodCall(final String expectedMethodCall, + final MethodMetadataCustomDataKey methodKey, + final MethodParameter... callerParameters) { + // Set up + setUpMockRepository(); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, methodKey.name(), + mockTargetEntity, mockIdType, callerParameters); + + // Check + assertEquals(expectedMethodCall, additions.getMethodCall()); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + layerProvider = new RepositoryJpaLayerProvider(); + layerProvider.setRepositoryLocator(mockRepositoryLocator); + } + + /** + * Sets up the mock {@link RepositoryJpaLocator} and + * {@link PersistenceMemberLocator} to return a mock repository for our test + * entity. + */ + private void setUpMockRepository() { + final ClassOrInterfaceTypeDetails mockRepositoryDetails = mock(ClassOrInterfaceTypeDetails.class); + final FieldMetadata mockFieldMetadata = mock(FieldMetadata.class); + final JavaType mockRepositoryType = mock(JavaType.class); + when(mockRepositoryType.getSimpleTypeName()).thenReturn("ClinicRepo"); + when(mockIdType.getFullyQualifiedTypeName()).thenReturn( + Long.class.getName()); + when(mockRepositoryDetails.getName()).thenReturn(mockRepositoryType); + when(mockFieldMetadata.getFieldType()).thenReturn(mockIdType); + when(mockRepositoryLocator.getRepositories(mockTargetEntity)) + .thenReturn(Arrays.asList(mockRepositoryDetails)); + } + + @Test + public void testGetAdditionsForNonRepositoryLayerMethod() { + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, "bogus", mockTargetEntity, + mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsWhenNoRepositoriesExist() { + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetFindAllAdditions() { + assertMethodCall("clinicRepo.findAll()", FIND_ALL_METHOD); + } + + @Test + public void testGetFlushAdditions() { + final MethodParameter entityParameter = new MethodParameter( + mockTargetEntity, "anything"); + assertMethodCall("clinicRepo.flush()", FLUSH_METHOD, entityParameter); + } +} diff --git a/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryLayerMethodTest.java b/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryLayerMethodTest.java new file mode 100644 index 000000000..b4697efaf --- /dev/null +++ b/addon-layers-repository-jpa/src/test/java/org/springframework/roo/addon/layers/repository/jpa/RepositoryLayerMethodTest.java @@ -0,0 +1,66 @@ +package org.springframework.roo.addon.layers.repository.jpa; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of the {@link RepositoryLayerMethod} enum. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class RepositoryLayerMethodTest { + + @Mock private JavaType mockIdType; + // Fixture + @Mock private JavaType mockTargetEntity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testCallFlushMethod() { + // Invoke + final String methodCall = RepositoryJpaLayerMethod.FLUSH + .getCall(Collections. emptyList()); + + // Check + assertEquals("flush()", methodCall); + } + + @Test + public void testNamesAreUniqueAndNotBlank() { + final Set names = new HashSet(); + for (final RepositoryJpaLayerMethod method : RepositoryJpaLayerMethod + .values()) { + final String name = method.getName(); + names.add(name); + assertTrue(StringUtils.isNotBlank(name)); + } + assertEquals(RepositoryJpaLayerMethod.values().length, names.size()); + } + + @Test + public void testParameterTypesAreNotNull() { + for (final RepositoryJpaLayerMethod method : RepositoryJpaLayerMethod + .values()) { + assertNotNull(method + .getParameterTypes(mockTargetEntity, mockIdType)); + } + } +} diff --git a/addon-layers-repository-mongo/pom.xml b/addon-layers-repository-mongo/pom.xml new file mode 100644 index 000000000..e3947863d --- /dev/null +++ b/addon-layers-repository-mongo/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.layers.repository.mongo + bundle + Spring Roo - Addon - MongoDB Repository Layer + Support for common layering options in Java Enterprise Applications + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.addon.test + + + org.springframework.roo + org.springframework.roo.addon.dod + + + diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoCommands.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoCommands.java new file mode 100644 index 000000000..87299d8e9 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoCommands.java @@ -0,0 +1,72 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import java.math.BigInteger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the MongoDB repository add-on. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class MongoCommands implements CommandMarker { + + @Reference private MongoOperations mongoOperations; + + @CliAvailabilityIndicator("mongo setup") + public boolean isMongoSetupAvailable() { + return mongoOperations.isMongoInstallationPossible(); + } + + @CliAvailabilityIndicator({ "repository mongo", "entity mongo" }) + public boolean isRepositoryCommandAvailable() { + return mongoOperations.isRepositoryInstallationPossible(); + } + + @CliCommand(value = "repository mongo", help = "Adds @RooMongoRepository annotation to target type") + public void repository( + @CliOption(key = "interface", mandatory = true, help = "The java interface to apply this annotation to") final JavaType interfaceType, + @CliOption(key = "entity", unspecifiedDefaultValue = "*", optionContext = PROJECT, mandatory = false, help = "The domain entity this repository should expose") final JavaType domainType) { + + mongoOperations.setupRepository(interfaceType, domainType); + } + + @CliCommand(value = "mongo setup", help = "Configures the project for MongoDB peristence.") + public void setup( + @CliOption(key = "username", mandatory = false, help = "Username for accessing the database (defaults to '')") final String username, + @CliOption(key = "password", mandatory = false, help = "Password for accessing the database (defaults to '')") final String password, + @CliOption(key = "databaseName", mandatory = false, help = "Name of the database (defaults to project name)") final String name, + @CliOption(key = "port", mandatory = false, help = "Port for the database (defaults to '27017')") final String port, + @CliOption(key = "host", mandatory = false, help = "Host for the database (defaults to '127.0.0.1')") final String host, + @CliOption(key = "cloudFoundry", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Deploy to CloudFoundry (defaults to 'false')") final boolean cloudFoundry) { + + mongoOperations.setup(username, password, name, port, host, + cloudFoundry); + } + + @CliCommand(value = "entity mongo", help = "Creates a domain entity which can be backed by a MongoDB repository") + public void type( + @CliOption(key = "class", mandatory = true, optionContext = UPDATE_PROJECT, help = "Implementation class for the specified interface") final JavaType classType, + @CliOption(key = "identifierType", mandatory = false, help = "The ID type to be used for this domain type (defaults to BigInteger)") MongoIdType idType, + @CliOption(key = "testAutomatically", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Create automatic integration tests for this entity") final boolean testAutomatically) { + + if (idType == null) { + idType = new MongoIdType(BigInteger.class.getName()); + } + mongoOperations.createType(classType, idType.getJavaType(), + testAutomatically); + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityAnnotationValues.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityAnnotationValues.java new file mode 100644 index 000000000..7d49c4253 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityAnnotationValues.java @@ -0,0 +1,40 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * The values of a {@link RooMongoRepository} annotation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class MongoEntityAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private JavaType identifierType = JdkJavaType.BIG_INTEGER; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata the metadata to parse (required) + */ + public MongoEntityAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_MONGO_ENTITY); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Returns the Identifier type for this domain entity + * + * @return a non-null type + */ + public JavaType getIdentifierType() { + return identifierType; + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadata.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadata.java new file mode 100644 index 000000000..de5a4a7aa --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadata.java @@ -0,0 +1,237 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Creates metadata for domain entity ITDs (annotated with + * {@link RooMongoEntity}. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class MongoEntityMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = MongoEntityMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private MemberDetails memberDetails; + private MongoEntityMetadata parent; + private JavaType idType; + private FieldMetadata idField; + + /** + * Constructor + * + * @param identifier the identifier for this item of metadata (required) + * @param aspectName the Java type of the ITD (required) + * @param governorPhysicalTypeMetadata the governor, which is expected to + * contain a {@link ClassOrInterfaceTypeDetails} (required) + * @param parent + * @param idType the type of the entity's identifier field (required) + * @param governorMemberDetails the member details of the entity + */ + public MongoEntityMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + MongoEntityMetadata parent, final JavaType idType, + final MemberDetails memberDetails) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(idType, "Id type required"); + Validate.notNull(memberDetails, "Entity MemberDetails required"); + + if (!isValid()) { + return; + } + + this.memberDetails = memberDetails; + this.parent = parent; + this.idType = idType; + + builder.addAnnotation(getTypeAnnotation(SpringJavaType.PERSISTENT)); + + idField = getIdentifierField(); + if (idField != null) { + builder.addField(idField); + builder.addMethod(getIdentifierAccessor()); + builder.addMethod(getIdentifierMutator()); + } + + // Build the ITD + itdTypeDetails = builder.build(); + } + + private MethodMetadataBuilder getIdentifierAccessor() { + if (parent != null) { + final MethodMetadataBuilder parentIdAccessor = parent + .getIdentifierAccessor(); + if (parentIdAccessor != null + && parentIdAccessor.getReturnType().equals(idType)) { + return parentIdAccessor; + } + } + + JavaSymbolName requiredAccessorName = BeanInfoUtils + .getAccessorMethodName(idField); + + // See if the user provided the field + if (!getId().equals(idField.getDeclaredByMetadataId())) { + // Locate an existing accessor + final MethodMetadata method = memberDetails.getMethod( + requiredAccessorName, new ArrayList()); + if (method != null) { + if (Modifier.isPublic(method.getModifier())) { + // Method exists and is public so return it + return new MethodMetadataBuilder(method); + } + + // Method is not public so make the required accessor name + // unique + requiredAccessorName = new JavaSymbolName( + requiredAccessorName.getSymbolName() + "_"); + } + } + + // We declared the field in this ITD, so produce a public accessor for + // it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return this." + + idField.getFieldName().getSymbolName() + ";"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + requiredAccessorName, idField.getFieldType(), bodyBuilder); + } + + private FieldMetadata getIdentifierField() { + if (parent != null) { + final FieldMetadata parentIdField = parent.getIdentifierField(); + if (parentIdField.getFieldType().equals(idType)) { + return parentIdField; + } + } + + // Try to locate an existing field with DATA_ID + final List idFields = governorTypeDetails + .getFieldsWithAnnotation(SpringJavaType.DATA_ID); + if (!idFields.isEmpty()) { + return idFields.get(0); + } + final JavaSymbolName idFieldName = governorTypeDetails + .getUniqueFieldName("id"); + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId(), Modifier.PRIVATE, idFieldName, idType, null); + fieldBuilder.addAnnotation(new AnnotationMetadataBuilder( + SpringJavaType.DATA_ID)); + return fieldBuilder.build(); + } + + private MethodMetadataBuilder getIdentifierMutator() { + if (parent != null) { + final MethodMetadataBuilder parentIdMutator = parent + .getIdentifierMutator(); + if (parentIdMutator != null + && parentIdMutator.getParameterTypes().get(0).getJavaType() + .equals(idType)) { + return parentIdMutator; + } + } + + JavaSymbolName requiredMutatorName = BeanInfoUtils + .getMutatorMethodName(idField); + + final List parameterTypes = Arrays.asList(idField + .getFieldType()); + final List parameterNames = Arrays + .asList(new JavaSymbolName("id")); + + // See if the user provided the field + if (!getId().equals(idField.getDeclaredByMetadataId())) { + // Locate an existing mutator + final MethodMetadata method = memberDetails.getMethod( + requiredMutatorName, parameterTypes); + if (method != null) { + if (Modifier.isPublic(method.getModifier())) { + // Method exists and is public so return it + return new MethodMetadataBuilder(method); + } + + // Method is not public so make the required mutator name unique + requiredMutatorName = new JavaSymbolName( + requiredMutatorName.getSymbolName() + "_"); + } + } + + // We declared the field in this ITD, so produce a public mutator for it + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("this." + + idField.getFieldName().getSymbolName() + " = id;"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + requiredMutatorName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadataProvider.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadataProvider.java new file mode 100644 index 000000000..35fd2fd23 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadataProvider.java @@ -0,0 +1,14 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides the metadata for an ITD that implements a Spring Data Mongo domain + * entity. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface MongoEntityMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadataProviderImpl.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadataProviderImpl.java new file mode 100644 index 000000000..119d2808e --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityMetadataProviderImpl.java @@ -0,0 +1,170 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_MUTATOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSISTENT_TYPE; +import static org.springframework.roo.model.RooJavaType.ROO_MONGO_ENTITY; + +import java.util.Arrays; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.taggers.AnnotatedTypeMatcher; +import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator; +import org.springframework.roo.classpath.customdata.taggers.FieldMatcher; +import org.springframework.roo.classpath.customdata.taggers.MethodMatcher; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link MongoEntityMetadataProvider}. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class MongoEntityMetadataProviderImpl extends + AbstractItdMetadataProvider implements MongoEntityMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(MongoEntityMetadataProviderImpl.class); + + private static final FieldMatcher ID_FIELD_MATCHER = new FieldMatcher( + IDENTIFIER_FIELD, + AnnotationMetadataBuilder.getInstance(SpringJavaType.DATA_ID + .getFullyQualifiedTypeName())); + private static final MethodMatcher ID_ACCESSOR_MATCHER = new MethodMatcher( + Arrays.asList(ID_FIELD_MATCHER), IDENTIFIER_ACCESSOR_METHOD, true); + private static final MethodMatcher ID_MUTATOR_MATCHER = new MethodMatcher( + Arrays.asList(ID_FIELD_MATCHER), IDENTIFIER_MUTATOR_METHOD, false); + private static final AnnotatedTypeMatcher PERSISTENT_TYPE_MATCHER = new AnnotatedTypeMatcher( + PERSISTENT_TYPE, ROO_MONGO_ENTITY); + + private CustomDataKeyDecorator customDataKeyDecorator; + + @SuppressWarnings("unchecked") + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + super.setDependsOnGovernorBeingAClass(false); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_MONGO_ENTITY); + getCustomDataKeyDecorator().registerMatchers(getClass(), + PERSISTENT_TYPE_MATCHER, ID_FIELD_MATCHER, ID_ACCESSOR_MATCHER, + ID_MUTATOR_MATCHER); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return MongoEntityMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_MONGO_ENTITY); + getCustomDataKeyDecorator().unregisterMatchers(getClass()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = MongoEntityMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = MongoEntityMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Mongo_Entity"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalType, + final String itdFilename) { + final MongoEntityAnnotationValues annotationValues = new MongoEntityAnnotationValues( + governorPhysicalType); + final JavaType identifierType = annotationValues.getIdentifierType(); + if (!annotationValues.isAnnotationFound() || identifierType == null) { + return null; + } + + // Get the governor's member details + final MemberDetails memberDetails = getMemberDetails(governorPhysicalType); + if (memberDetails == null) { + return null; + } + + final MongoEntityMetadata parent = getParentMetadata(governorPhysicalType + .getMemberHoldingTypeDetails()); + + // If the parent is null, but the type has a super class it is likely + // that the we don't have information to proceed + if (parent == null + && governorPhysicalType.getMemberHoldingTypeDetails() + .getSuperclass() != null) { + // If the superclass is not annotated with the RooMongoEntity + // trigger + // annotation then we can be pretty sure that we don't have enough + // information to proceed + if (MemberFindingUtils.getAnnotationOfType(governorPhysicalType + .getMemberHoldingTypeDetails().getAnnotations(), + ROO_MONGO_ENTITY) != null) { + return null; + } + } + + return new MongoEntityMetadata(metadataIdentificationString, + aspectName, governorPhysicalType, parent, identifierType, + memberDetails); + } + + public String getProvidesType() { + return MongoEntityMetadata.getMetadataIdentiferType(); + } + + public CustomDataKeyDecorator getCustomDataKeyDecorator(){ + if(customDataKeyDecorator == null){ + // Get all Services implement CustomDataKeyDecorator interface + try { + ServiceReference[] references = context.getAllServiceReferences(CustomDataKeyDecorator.class.getName(), null); + + for(ServiceReference ref : references){ + return (CustomDataKeyDecorator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CustomDataKeyDecorator on MongoEntityMetadataProviderImpl."); + return null; + } + }else{ + return customDataKeyDecorator; + } + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoIdType.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoIdType.java new file mode 100644 index 000000000..136648ec6 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoIdType.java @@ -0,0 +1,27 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.model.JavaType; + +/** + * Custom id type to limit options in {@link MongoCommands} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class MongoIdType { + + private final JavaType javaType; + + /** + * Constructor + * + * @param type the fully-qualified type name (required) + */ + public MongoIdType(final String type) { + javaType = new JavaType(type); + } + + public JavaType getJavaType() { + return javaType; + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoIdTypeConverter.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoIdTypeConverter.java new file mode 100644 index 000000000..0d08469d7 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoIdTypeConverter.java @@ -0,0 +1,52 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.math.BigInteger; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Custom id type converter for {@link MongoIdType} to limit options in + * {@link MongoCommands} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class MongoIdTypeConverter implements Converter { + + public MongoIdType convertFromText(final String value, + final Class targetType, final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + return new MongoIdType(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class targetType, final String existingData, + final String optionContext, final MethodTarget target) { + final SortedSet types = new TreeSet(); + types.add(BigInteger.class.getName()); + types.add("org.bson.types.ObjectId"); + + for (final String type : types) { + if (type.startsWith(existingData) || existingData.startsWith(type)) { + completions.add(new Completion(type)); + } + } + return false; + } + + public boolean supports(final Class type, final String optionContext) { + return MongoIdType.class.isAssignableFrom(type); + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoOperations.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoOperations.java new file mode 100644 index 000000000..9d28a7339 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoOperations.java @@ -0,0 +1,65 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.math.BigInteger; + +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Feature; + +/** + * Operations for Spring Data MongoDB repository add-on. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface MongoOperations extends Feature { + + /** + * Creates a new domain type ready for backing a Spring Data MongoDB + * repository + * + * @param classType (required) + * @param idType (optional, defaults to {@link BigInteger} + * @param testAutomatically + */ + void createType(JavaType classType, JavaType idType, + boolean testAutomatically); + + /** + * Indicate if the 'mongo setup' command should be available for this + * project. + * + * @return true if command should be made available + */ + boolean isMongoInstallationPossible(); + + /** + * Indicate if the 'repository mongo' command should be available for this + * project. + * + * @return true if command should be made available + */ + boolean isRepositoryInstallationPossible(); + + /** + * Setup current project for Spring Data MongoDB configuration. + * + * @param username (optional) + * @param password (optional) + * @param name database name (optional, defaults to project name) + * @param port (optional, defaults to 27017) + * @param host (optional, defaults to 127.0.0.1) + * @param cloudFoundry indicate if project should be deployable on VMware + * CloudFoundry (optional, defaults to false) + */ + void setup(String username, String password, String name, String port, + String host, boolean cloudFoundry); + + /** + * Creates a new Repository interface for Spring Data JPA MongoDB + * integration. + * + * @param interfaceType (required) + * @param classType (optional) + */ + void setupRepository(JavaType interfaceType, JavaType classType); +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoOperationsImpl.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoOperationsImpl.java new file mode 100644 index 000000000..92ae2102a --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/MongoOperationsImpl.java @@ -0,0 +1,486 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.springframework.roo.model.RooJavaType.ROO_REPOSITORY_MONGO; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dod.DataOnDemandOperations; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.addon.test.IntegrationTestOperations; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.Repository; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + + +/** + * The {@link MongoOperations} implementation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class MongoOperationsImpl implements MongoOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(MongoOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private static final String MONGO_XML = "applicationContext-mongo.xml"; + + private DataOnDemandOperations dataOnDemandOperations; + private FileManager fileManager; + private IntegrationTestOperations integrationTestOperations; + private PathResolver pathResolver; + private ProjectOperations projectOperations; + private PropFileOperations propFileOperations; + private TypeLocationService typeLocationService; + private TypeManagementService typeManagementService; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + public void createType(final JavaType classType, final JavaType idType, + final boolean testAutomatically) { + Validate.notNull(classType, "Class type required"); + Validate.notNull(idType, "Identifier type required"); + + final String classIdentifier = getTypeLocationService() + .getPhysicalTypeCanonicalPath(classType, + getPathResolver().getFocusedPath(Path.SRC_MAIN_JAVA)); + if (getFileManager().exists(classIdentifier)) { + return; // Type exists already - nothing to do + } + + final String classMdId = PhysicalTypeIdentifier.createIdentifier( + classType, getPathResolver().getPath(classIdentifier)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + classMdId, Modifier.PUBLIC, classType, + PhysicalTypeCategory.CLASS); + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + RooJavaType.ROO_JAVA_BEAN)); + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + RooJavaType.ROO_TO_STRING)); + + final List> attributes = new ArrayList>(); + if (!idType.equals(JdkJavaType.BIG_INTEGER)) { + attributes.add(new ClassAttributeValue(new JavaSymbolName( + "identifierType"), idType)); + } + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + RooJavaType.ROO_MONGO_ENTITY, attributes)); + getTypeManagementService().createOrUpdateTypeOnDisk(cidBuilder.build()); + + if (testAutomatically) { + getIntegrationTestOperations().newIntegrationTest(classType, false); + getDataOnDemandOperations().newDod(classType, + new JavaType(classType.getFullyQualifiedTypeName() + + "DataOnDemand")); + } + } + + public String getName() { + return FeatureNames.MONGO; + } + + public boolean isInstalledInModule(final String moduleName) { + return getProjectOperations().isFocusedProjectAvailable() + && getFileManager().exists(getPathResolver().getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, MONGO_XML)); + } + + public boolean isMongoInstallationPossible() { + return getProjectOperations().isFocusedProjectAvailable() + && !getProjectOperations() + .isFeatureInstalledInFocusedModule(FeatureNames.JPA); + } + + public boolean isRepositoryInstallationPossible() { + return isInstalledInModule(getProjectOperations().getFocusedModuleName()) + && !getProjectOperations() + .isFeatureInstalledInFocusedModule(FeatureNames.JPA); + } + + private void manageAppCtx(final String username, final String password, + final String name, final boolean cloudFoundry, + final String moduleName) { + final String appCtxId = getPathResolver().getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, MONGO_XML); + if (!getFileManager().exists(appCtxId)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), MONGO_XML); + final MutableFile mutableFile = getFileManager() + .createFile(appCtxId); + String input = IOUtils.toString(inputStream); + input = input.replace("TO_BE_CHANGED_BY_ADDON", + getProjectOperations().getTopLevelPackage(moduleName) + .getFullyQualifiedPackageName()); + outputStream = mutableFile.getOutputStream(); + IOUtils.write(input, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to create file " + + appCtxId); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + final Document document = XmlUtils.readXml(getFileManager() + .getInputStream(appCtxId)); + final Element root = document.getDocumentElement(); + Element mongoSetup = XmlUtils.findFirstElement("/beans/db-factory", + root); + Element mongoCloudSetup = XmlUtils.findFirstElement( + "/beans/mongo-db-factory", root); + if (!cloudFoundry) { + if (mongoCloudSetup != null) { + root.removeChild(mongoCloudSetup); + } + if (mongoSetup == null) { + mongoSetup = document.createElement("mongo:db-factory"); + root.appendChild(mongoSetup); + } + if (StringUtils.isNotBlank(name)) { + mongoSetup.setAttribute("dbname", "${mongo.database}"); + } + if (StringUtils.isNotBlank(username)) { + mongoSetup.setAttribute("username", "${mongo.username}"); + } + if (StringUtils.isNotBlank(password)) { + mongoSetup.setAttribute("password", "${mongo.password}"); + } + mongoSetup.setAttribute("host", "${mongo.host}"); + mongoSetup.setAttribute("port", "${mongo.port}"); + mongoSetup.setAttribute("id", "mongoDbFactory"); + } + else { + if (mongoSetup != null) { + root.removeChild(mongoSetup); + } + if (mongoCloudSetup == null) { + mongoCloudSetup = XmlUtils.findFirstElement( + "/beans/mongo-db-factory", root); + } + if (mongoCloudSetup == null) { + mongoCloudSetup = document + .createElement("cloud:mongo-db-factory"); + mongoCloudSetup.setAttribute("id", "mongoDbFactory"); + root.appendChild(mongoCloudSetup); + } + } + getFileManager().createOrUpdateTextFileIfRequired(appCtxId, + XmlUtils.nodeToString(document), false); + } + + private void manageDependencies(final String moduleName) { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List springDependencies = XmlUtils.findElements( + "/configuration/spring-data-mongodb/dependencies/dependency", + configuration); + for (final Element dependencyElement : springDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + + final List repositories = new ArrayList(); + final List repositoryElements = XmlUtils.findElements( + "/configuration/spring-data-mongodb/repositories/repository", + configuration); + for (final Element repositoryElement : repositoryElements) { + repositories.add(new Repository(repositoryElement)); + } + + getProjectOperations().addRepositories(moduleName, repositories); + getProjectOperations().addDependencies(moduleName, dependencies); + } + + public void setup(final String username, final String password, + final String name, final String port, final String host, + final boolean cloudFoundry) { + final String moduleName = getProjectOperations().getFocusedModuleName(); + writeProperties(username, password, name, port, host, moduleName); + manageDependencies(moduleName); + manageAppCtx(username, password, name, cloudFoundry, moduleName); + } + + public void setupRepository(final JavaType interfaceType, + final JavaType domainType) { + Validate.notNull(interfaceType, "Interface type required"); + Validate.notNull(domainType, "Domain type required"); + + final String interfaceIdentifier = getPathResolver() + .getFocusedCanonicalPath(Path.SRC_MAIN_JAVA, interfaceType); + + if (getFileManager().exists(interfaceIdentifier)) { + return; // Type exists already - nothing to do + } + + // Build interface type + final AnnotationMetadataBuilder interfaceAnnotationMetadata = new AnnotationMetadataBuilder( + ROO_REPOSITORY_MONGO); + interfaceAnnotationMetadata.addAttribute(new ClassAttributeValue( + new JavaSymbolName("domainType"), domainType)); + final String interfaceMdId = PhysicalTypeIdentifier.createIdentifier( + interfaceType, getPathResolver().getPath(interfaceIdentifier)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + interfaceMdId, Modifier.PUBLIC, interfaceType, + PhysicalTypeCategory.INTERFACE); + cidBuilder.addAnnotation(interfaceAnnotationMetadata.build()); + final JavaType listType = new JavaType(List.class.getName(), 0, + DataType.TYPE, null, Arrays.asList(domainType)); + cidBuilder.addMethod(new MethodMetadataBuilder(interfaceMdId, 0, + new JavaSymbolName("findAll"), listType, + new InvocableMemberBodyBuilder())); + getTypeManagementService().createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + private void writeProperties(String username, String password, String name, + String port, String host, final String moduleName) { + if (StringUtils.isBlank(username)) { + username = ""; + } + if (StringUtils.isBlank(password)) { + password = ""; + } + if (StringUtils.isBlank(name)) { + name = getProjectOperations().getProjectName(moduleName); + } + if (StringUtils.isBlank(port)) { + port = "27017"; + } + if (StringUtils.isBlank(host)) { + host = "127.0.0.1"; + } + + final Map properties = new HashMap(); + properties.put("mongo.username", username); + properties.put("mongo.password", password); + properties.put("mongo.database", name); + properties.put("mongo.port", port); + properties.put("mongo.host", host); + getPropFileOperations().addProperties(Path.SPRING_CONFIG_ROOT + .getModulePathId(getProjectOperations().getFocusedModuleName()), + "database.properties", properties, true, false); + } + + public DataOnDemandOperations getDataOnDemandOperations(){ + if(dataOnDemandOperations == null){ + // Get all Services implement DataOnDemandOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(DataOnDemandOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (DataOnDemandOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load DataOnDemandOperations on MongoOperationsImpl."); + return null; + } + }else{ + return dataOnDemandOperations; + } + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on MongoOperationsImpl."); + return null; + } + }else{ + return fileManager; + } + } + + public IntegrationTestOperations getIntegrationTestOperations(){ + if(integrationTestOperations == null){ + // Get all Services implement IntegrationTestOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(IntegrationTestOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (IntegrationTestOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load IntegrationTestOperations on MongoOperationsImpl."); + return null; + } + }else{ + return integrationTestOperations; + } + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on MongoOperationsImpl."); + return null; + } + }else{ + return pathResolver; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on MongoOperationsImpl."); + return null; + } + }else{ + return projectOperations; + } + } + + public PropFileOperations getPropFileOperations(){ + if(propFileOperations == null){ + // Get all Services implement PropFileOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PropFileOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (PropFileOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PropFileOperations on MongoOperationsImpl."); + return null; + } + }else{ + return propFileOperations; + } + } + + public TypeLocationService getTypeLocationService(){ + if(typeLocationService == null){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on MongoOperationsImpl."); + return null; + } + }else{ + return typeLocationService; + } + } + + public TypeManagementService getTypeManagementService(){ + if(typeManagementService == null){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on MongoOperationsImpl."); + return null; + } + }else{ + return typeManagementService; + } + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoAnnotationValues.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoAnnotationValues.java new file mode 100644 index 000000000..33b4ed221 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoAnnotationValues.java @@ -0,0 +1,39 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * The values of a {@link RooMongoRepository} annotation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class RepositoryMongoAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private JavaType domainType; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata the metadata to parse (required) + */ + public RepositoryMongoAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_REPOSITORY_MONGO); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Returns the domain type managed by the annotated repository + * + * @return a non-null type + */ + public JavaType getDomainType() { + return domainType; + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerMethod.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerMethod.java new file mode 100644 index 000000000..9f9df8f90 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerMethod.java @@ -0,0 +1,294 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * A method provided by the {@link LayerType#REPOSITORY} layer. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public enum RepositoryMongoLayerMethod { + + COUNT("count", COUNT_ALL_METHOD) { + + @Override + public String getCall(final List parameters) { + return "count()"; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Collections.emptyList(); + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.LONG_PRIMITIVE; + } + }, + + /** + * Deletes the passed-in entity (does not delete by ID). + */ + DELETE("delete", REMOVE_METHOD) { + + @Override + public String getCall(final List parameters) { + return "delete(" + parameters.get(0).getValue() + ")"; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(JavaSymbolName + .getReservedWordSafeName(entityType)); + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.VOID_PRIMITIVE; + } + }, + + FIND("find", FIND_METHOD) { + + @Override + public String getCall(final List parameters) { + return "findOne(" + parameters.get(0).getValue() + ")"; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(new JavaSymbolName("id")); + } + + @Override + protected List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays.asList(idType); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return entityType; + } + }, + + FIND_ALL("findAll", FIND_ALL_METHOD) { + + @Override + public String getCall(final List parameters) { + return "findAll()"; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Collections.emptyList(); + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Collections.emptyList(); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.listOf(entityType); + } + }, + + /** + * Finds entities starting from a given zero-based index, up to a given + * maximum number of results. This method isn't directly implemented by + * Spring Data JPA, so we use its findAll(Pageable) API. + */ + FIND_ENTRIES("findEntries", FIND_ENTRIES_METHOD) { + + @Override + public String getCall(final List parameters) { + final JavaSymbolName firstResultParameter = parameters.get(0) + .getValue(); + final JavaSymbolName maxResultsParameter = parameters.get(1) + .getValue(); + final String pageNumberExpression = firstResultParameter + " / " + + maxResultsParameter; + return "findAll(new org.springframework.data.domain.PageRequest(" + + pageNumberExpression + ", " + maxResultsParameter + + ")).getContent()"; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(new JavaSymbolName("firstResult"), + new JavaSymbolName("maxResults")); + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays + .asList(JavaType.INT_PRIMITIVE, JavaType.INT_PRIMITIVE); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.listOf(entityType); + } + }, + + /** + * Spring Data JPA makes no distinction between + * create/persist/save/update/merge + */ + SAVE("save", MERGE_METHOD, PERSIST_METHOD) { + + @Override + public String getCall(final List parameters) { + return "save(" + parameters.get(0).getValue() + ")"; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(JavaSymbolName + .getReservedWordSafeName(entityType)); + } + + @Override + protected List getParameterTypes(final JavaType targetEntity, + final JavaType idType) { + return Arrays.asList(targetEntity); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.VOID_PRIMITIVE; + } + }; + + /** + * Returns the {@link RepositoryMongoLayerMethod} with the given ID and + * parameter types. + * + * @param methodId the ID to match upon + * @param parameterTypes the parameter types to match upon + * @param targetEntity the entity type being managed by the repository + * @param idType specifies the ID type used by the target entity (required) + * @return null if no such method exists + */ + public static RepositoryMongoLayerMethod valueOf(final String methodId, + final List parameterTypes, final JavaType targetEntity, + final JavaType idType) { + for (final RepositoryMongoLayerMethod method : values()) { + if (method.ids.contains(methodId) + && method.getParameterTypes(targetEntity, idType).equals( + parameterTypes)) { + return method; + } + } + return null; + } + + private final List ids; + private final String name; + + /** + * Constructor + * + * @param key the unique key for this method (required) + * @param name the Java name of this method (required) + */ + private RepositoryMongoLayerMethod(final String name, + final MethodMetadataCustomDataKey... keys) { + Validate.notBlank(name, "Name is required"); + Validate.isTrue(keys.length > 0, "One or more ids are required"); + ids = new ArrayList(); + for (final MethodMetadataCustomDataKey key : keys) { + ids.add(key.name()); + } + this.name = name; + } + + /** + * Returns a Java snippet that invokes this method (minus the target) + * + * @param parameters the parameters used by the caller; can be + * null + * @return a non-blank Java snippet + */ + public abstract String getCall(List parameters); + + /** + * Returns the name of this method + * + * @return a non-blank name + */ + public String getName() { + return name; + } + + /** + * Returns the names of this method's declared parameters + * + * @param entityType the type of domain entity managed by the service + * (required) + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null list (might be empty) + */ + public abstract List getParameterNames(JavaType entityType, + JavaType idType); + + /** + * Instances must return the types of parameters they take + * + * @param targetEntity the type of entity being managed (required) + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null list + */ + protected abstract List getParameterTypes(JavaType targetEntity, + JavaType idType); + + /** + * Returns this method's return type + * + * @param entityType the type of entity being managed + * @return a non-null type + */ + public abstract JavaType getReturnType(JavaType entityType); +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerProvider.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerProvider.java new file mode 100644 index 000000000..6aa2f5ac6 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerProvider.java @@ -0,0 +1,122 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.layers.CoreLayerProvider; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.PairList; + +/** + * A provider of the {@link LayerType#REPOSITORY} layer. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryMongoLayerProvider extends CoreLayerProvider { + + @Reference private RepositoryMongoLocator repositoryLocator; + + public int getLayerPosition() { + return LayerType.REPOSITORY.getPosition() - 1; + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final MethodParameter... callerParameters) { + return getMemberTypeAdditions(callerMID, methodIdentifier, + targetEntity, idType, true, callerParameters); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, boolean autowire, + final MethodParameter... callerParameters) { + Validate.notBlank(callerMID, "Caller's metadata ID required"); + Validate.notBlank(methodIdentifier, "Method identifier required"); + Validate.notNull(targetEntity, "Target enitity type required"); + Validate.notNull(idType, "Enitity Id type required"); + + // Look for a repository layer method with this ID and parameter types + final List parameterTypes = new PairList( + callerParameters).getKeys(); + final RepositoryMongoLayerMethod method = RepositoryMongoLayerMethod + .valueOf(methodIdentifier, parameterTypes, targetEntity, idType); + if (method == null) { + return null; + } + + // Look for repositories that support this domain type + final Collection repositories = repositoryLocator + .getRepositories(targetEntity); + if (CollectionUtils.isEmpty(repositories)) { + return null; + } + + // Use the first such repository (could refine this later) + final ClassOrInterfaceTypeDetails repository = repositories.iterator() + .next(); + + // Return the additions the caller needs to make + return getMethodAdditions(callerMID, method, repository.getName(), + Arrays.asList(callerParameters)); + } + + /** + * Returns the additions that the caller needs to make in order to invoke + * the given method + * + * @param callerMID the caller's metadata ID (required) + * @param method the method being called (required) + * @param repositoryType the type of repository being called + * @param parameterNames the parameter names used by the caller + * @return a non-null set of additions + */ + private MemberTypeAdditions getMethodAdditions(final String callerMID, + final RepositoryMongoLayerMethod method, + final JavaType repositoryType, + final List parameters) { + // Create a builder to hold the repository field to be copied into the + // caller + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + callerMID); + final AnnotationMetadataBuilder autowiredAnnotation = new AnnotationMetadataBuilder( + AUTOWIRED); + final String repositoryFieldName = StringUtils + .uncapitalize(repositoryType.getSimpleTypeName()); + cidBuilder.addField(new FieldMetadataBuilder(callerMID, 0, Arrays + .asList(autowiredAnnotation), new JavaSymbolName( + repositoryFieldName), repositoryType)); + + // Create the additions to invoke the given method on this field + final String methodCall = repositoryFieldName + "." + + method.getCall(parameters); + return new MemberTypeAdditions(cidBuilder, method.getName(), + methodCall, false, parameters); + } + + // -------------------- Setters for use by unit tests ---------------------- + + void setRepositoryLocator(final RepositoryMongoLocator repositoryLocator) { + this.repositoryLocator = repositoryLocator; + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLocator.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLocator.java new file mode 100644 index 000000000..5f39b6638 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLocator.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.util.Collection; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; + +/** + * Locates Spring Data Mongo Repositories within the user's project + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface RepositoryMongoLocator { + + /** + * Returns the repositories that support the given domain type + * + * @param domainType the domain type for which to find the repositories; can + * be null + * @return a non-null collection + */ + Collection getRepositories( + final JavaType domainType); +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLocatorImpl.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLocatorImpl.java new file mode 100644 index 000000000..6f2080112 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLocatorImpl.java @@ -0,0 +1,58 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.DefaultPhysicalTypeMetadata; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * The {@link RepositoryMongoLocator} implementation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryMongoLocatorImpl implements RepositoryMongoLocator { + + @Reference private TypeLocationService typeLocationService; + private final Map> cacheMap = new HashMap>(); + + public Collection getRepositories( + final JavaType domainType) { + if (!cacheMap.containsKey(domainType)) { + cacheMap.put(domainType, new HashSet()); + } + final Set existing = cacheMap + .get(domainType); + final Set located = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_REPOSITORY_MONGO); + if (existing.containsAll(located)) { + return existing; + } + final Map toReturn = new HashMap(); + for (final ClassOrInterfaceTypeDetails cid : located) { + final RepositoryMongoAnnotationValues annotationValues = new RepositoryMongoAnnotationValues( + new DefaultPhysicalTypeMetadata( + cid.getDeclaredByMetadataId(), + typeLocationService + .getPhysicalTypeCanonicalPath(cid + .getDeclaredByMetadataId()), cid)); + if (annotationValues.getDomainType() != null + && annotationValues.getDomainType().equals(domainType)) { + toReturn.put(cid.getDeclaredByMetadataId(), cid); + } + } + return toReturn.values(); + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadata.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadata.java new file mode 100644 index 000000000..bbaf00289 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadata.java @@ -0,0 +1,101 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.util.Arrays; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Creates metadata for repository ITDs (annotated with + * {@link RooMongoRepository}. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class RepositoryMongoMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = RepositoryMongoMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final String SPRING_DATA_REPOSITORY = "org.springframework.data.repository.PagingAndSortingRepository"; + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + /** + * Constructor + * + * @param identifier the identifier for this item of metadata (required) + * @param aspectName the Java type of the ITD (required) + * @param governorPhysicalTypeMetadata the governor, which is expected to + * contain a {@link ClassOrInterfaceTypeDetails} (required) + * @param annotationValues (required) + * @param identifierType the type of the entity's identifier field + * (required) + */ + public RepositoryMongoMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final RepositoryMongoAnnotationValues annotationValues, + final JavaType identifierType) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(identifierType, "Identifier type required"); + + // Make the user's Repository interface extend Spring Data's Repository + // interface if it doesn't already + ensureGovernorExtends(new JavaType(SPRING_DATA_REPOSITORY, 0, + DataType.TYPE, null, Arrays.asList( + annotationValues.getDomainType(), identifierType))); + + builder.addAnnotation(getTypeAnnotation(SpringJavaType.REPOSITORY)); + + // Build the ITD + itdTypeDetails = builder.build(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadataProvider.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadataProvider.java new file mode 100644 index 000000000..4ae4970f6 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadataProvider.java @@ -0,0 +1,14 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides the metadata for an ITD that implements a Spring Data Mongo + * repository. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface RepositoryMongoMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadataProviderImpl.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadataProviderImpl.java new file mode 100644 index 000000000..865eca817 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoMetadataProviderImpl.java @@ -0,0 +1,177 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.springframework.roo.model.RooJavaType.ROO_REPOSITORY_MONGO; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerTypeMatcher; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link RepositoryMongoMetadataProvider}. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class RepositoryMongoMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + RepositoryMongoMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(RepositoryMongoMetadataProviderImpl.class); + + private CustomDataKeyDecorator customDataKeyDecorator; + private final Map domainTypeToRepositoryMidMap = new LinkedHashMap(); + private final Map repositoryMidToDomainTypeMap = new LinkedHashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + super.setDependsOnGovernorBeingAClass(false); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_REPOSITORY_MONGO); + registerMatchers(); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return RepositoryMongoMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_REPOSITORY_MONGO); + getCustomDataKeyDecorator().unregisterMatchers(getClass()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = RepositoryMongoMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = RepositoryMongoMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Mongo_Repository"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = domainTypeToRepositoryMidMap.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = domainTypeToRepositoryMidMap + .get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final RepositoryMongoAnnotationValues annotationValues = new RepositoryMongoAnnotationValues( + governorPhysicalTypeMetadata); + final JavaType domainType = annotationValues.getDomainType(); + final JavaType identifierType = getPersistenceMemberLocator() + .getIdentifierType(domainType); + if (identifierType == null) { + return null; + } + + // Remember that this entity JavaType matches up with this metadata + // identification string + // Start by clearing any previous association + final JavaType oldEntity = repositoryMidToDomainTypeMap + .get(metadataIdentificationString); + if (oldEntity != null) { + domainTypeToRepositoryMidMap.remove(oldEntity); + } + domainTypeToRepositoryMidMap.put(domainType, + metadataIdentificationString); + repositoryMidToDomainTypeMap.put(metadataIdentificationString, + domainType); + + return new RepositoryMongoMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, annotationValues, + identifierType); + } + + public String getProvidesType() { + return RepositoryMongoMetadata.getMetadataIdentiferType(); + } + + @SuppressWarnings("unchecked") + private void registerMatchers() { + getCustomDataKeyDecorator().registerMatchers(getClass(), + new LayerTypeMatcher(ROO_REPOSITORY_MONGO, new JavaSymbolName( + RooMongoRepository.DOMAIN_TYPE_ATTRIBUTE))); + } + + public CustomDataKeyDecorator getCustomDataKeyDecorator(){ + if(customDataKeyDecorator == null){ + // Get all Services implement CustomDataKeyDecorator interface + try { + ServiceReference[] references = context.getAllServiceReferences(CustomDataKeyDecorator.class.getName(), null); + + for(ServiceReference ref : references){ + return (CustomDataKeyDecorator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CustomDataKeyDecorator on RepositoryMongoMetadataProviderImpl."); + return null; + } + }else{ + return customDataKeyDecorator; + } + } +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RooMongoEntity.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RooMongoEntity.java new file mode 100644 index 000000000..a2b36a2df --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RooMongoEntity.java @@ -0,0 +1,30 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigInteger; + +/** + * Marks the annotated type as a Spring Data Mongo domain entity. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooMongoEntity { + + /** + * The name of this annotation's attribute that specifies the managed domain + * type. + */ + String ID_TYPE_ATTRIBUTE = "identifierType"; + + /** + * @return the class of identifier that should be used (defaults to + * {@link BigInteger}; must be provided) + */ + Class identifierType() default BigInteger.class; +} diff --git a/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RooMongoRepository.java b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RooMongoRepository.java new file mode 100644 index 000000000..6f26f9ef9 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/java/org/springframework/roo/addon/layers/repository/mongo/RooMongoRepository.java @@ -0,0 +1,33 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks the annotated type as a Spring Data Mongo repository interface. For the + * time being, we don't allow users to customise the names of repository methods + * like we do for service interfaces, because Spring Data Mongo provides a + * complete pre-named set of CRUD methods out of the box. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooMongoRepository { + + /** + * The name of this annotation's attribute that specifies the managed domain + * type. + */ + String DOMAIN_TYPE_ATTRIBUTE = "domainType"; + + /** + * The domain type managed by the annotated repository + * + * @return a non-null entity type + */ + Class domainType(); // No default => mandatory +} diff --git a/addon-layers-repository-mongo/src/main/resources/org/springframework/roo/addon/layers/repository/mongo/applicationContext-mongo.xml b/addon-layers-repository-mongo/src/main/resources/org/springframework/roo/addon/layers/repository/mongo/applicationContext-mongo.xml new file mode 100644 index 000000000..6f2966f81 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/resources/org/springframework/roo/addon/layers/repository/mongo/applicationContext-mongo.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-layers-repository-mongo/src/main/resources/org/springframework/roo/addon/layers/repository/mongo/configuration.xml b/addon-layers-repository-mongo/src/main/resources/org/springframework/roo/addon/layers/repository/mongo/configuration.xml new file mode 100644 index 000000000..c77479602 --- /dev/null +++ b/addon-layers-repository-mongo/src/main/resources/org/springframework/roo/addon/layers/repository/mongo/configuration.xml @@ -0,0 +1,19 @@ + + + + + + spring-maven-snapshot + Spring Maven MILESTONE Repository + http://maven.springframework.org/milestone + + + + + + + + + + + \ No newline at end of file diff --git a/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityAnnotationValuesTest.java b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityAnnotationValuesTest.java new file mode 100644 index 000000000..9b5d4ae55 --- /dev/null +++ b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/MongoEntityAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link MongoEntityAnnotationValues} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class MongoEntityAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooMongoEntity.class; + } + + @Override + protected Class getValuesClass() { + return MongoEntityAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoAnnotationValuesTest.java b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoAnnotationValuesTest.java new file mode 100644 index 000000000..fee9d5e01 --- /dev/null +++ b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoAnnotationValuesTest.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link RepositoryMongoAnnotationValues} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class RepositoryMongoAnnotationValuesTest + extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooMongoRepository.class; + } + + @Override + protected Class getValuesClass() { + return RepositoryMongoAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerMethodTest.java b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerMethodTest.java new file mode 100644 index 000000000..f3bb5237e --- /dev/null +++ b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerMethodTest.java @@ -0,0 +1,54 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of the {@link RepositoryMongoLayerMethod} enum. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class RepositoryMongoLayerMethodTest { + + @Mock private JavaType mockIdType; + // Fixture + @Mock private JavaType mockTargetEntity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testNamesAreUniqueAndNotBlank() { + final Set names = new HashSet(); + for (final RepositoryMongoLayerMethod method : RepositoryMongoLayerMethod + .values()) { + final String name = method.getName(); + names.add(name); + assertTrue(StringUtils.isNotBlank(name)); + } + assertEquals(RepositoryMongoLayerMethod.values().length, names.size()); + } + + @Test + public void testParameterTypesAreNotNull() { + for (final RepositoryMongoLayerMethod method : RepositoryMongoLayerMethod + .values()) { + assertNotNull(method + .getParameterTypes(mockTargetEntity, mockIdType)); + } + } +} diff --git a/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerProviderTest.java b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerProviderTest.java new file mode 100644 index 000000000..bd9f5b4be --- /dev/null +++ b/addon-layers-repository-mongo/src/test/java/org/springframework/roo/addon/layers/repository/mongo/RepositoryMongoLayerProviderTest.java @@ -0,0 +1,113 @@ +package org.springframework.roo.addon.layers.repository.mongo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link RepositoryMongoLayerProvider} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class RepositoryMongoLayerProviderTest { + + private static final String CALLER_MID = "MID:anything#com.example.PetService"; + + // Fixture + private RepositoryMongoLayerProvider layerProvider; + @Mock private JavaType mockIdType; + @Mock private RepositoryMongoLocator mockRepositoryLocator; + @Mock private JavaType mockTargetEntity; + + /** + * Asserts that the {@link RepositoryMongoLayerProvider} generates the + * expected call for the given method with the given parameters + * + * @param expectedMethodCall + * @param methodKey + * @param callerParameters + */ + private void assertMethodCall(final String expectedMethodCall, + final MethodMetadataCustomDataKey methodKey, + final MethodParameter... callerParameters) { + // Set up + setUpMockRepository(); + + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, methodKey.name(), + mockTargetEntity, mockIdType, callerParameters); + + // Check + assertEquals(expectedMethodCall, additions.getMethodCall()); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + layerProvider = new RepositoryMongoLayerProvider(); + layerProvider.setRepositoryLocator(mockRepositoryLocator); + } + + /** + * Sets up the mock {@link RepositoryMongoLocator} and + * {@link PersistenceMemberLocator} to return a mock repository for our test + * entity. + */ + private void setUpMockRepository() { + final ClassOrInterfaceTypeDetails mockRepositoryDetails = mock(ClassOrInterfaceTypeDetails.class); + final FieldMetadata mockFieldMetadata = mock(FieldMetadata.class); + final JavaType mockRepositoryType = mock(JavaType.class); + when(mockRepositoryType.getSimpleTypeName()).thenReturn("ClinicRepo"); + when(mockIdType.getFullyQualifiedTypeName()).thenReturn( + Long.class.getName()); + when(mockRepositoryDetails.getName()).thenReturn(mockRepositoryType); + when(mockFieldMetadata.getFieldType()).thenReturn(mockIdType); + when(mockRepositoryLocator.getRepositories(mockTargetEntity)) + .thenReturn(Arrays.asList(mockRepositoryDetails)); + } + + @Test + public void testGetAdditionsForNonRepositoryLayerMethod() { + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, "bogus", mockTargetEntity, + mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsWhenNoRepositoriesExist() { + // Invoke + final MemberTypeAdditions additions = layerProvider + .getMemberTypeAdditions(CALLER_MID, FIND_ALL_METHOD.name(), + mockTargetEntity, mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetFindAllAdditions() { + assertMethodCall("clinicRepo.findAll()", FIND_ALL_METHOD); + } +} diff --git a/addon-layers-service/pom.xml b/addon-layers-service/pom.xml new file mode 100644 index 000000000..4114a5645 --- /dev/null +++ b/addon-layers-service/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.layers.service + bundle + Spring Roo - Addon - Service Layer + Support for common layering options in Java Enterprise Applications + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + + org.springframework.roo + org.springframework.roo.file.monitor + + + + org.springframework.roo + org.springframework.roo.file.undo + + + + org.springframework.roo + org.springframework.roo.metadata + + + + org.springframework.roo + org.springframework.roo.model + + + + org.springframework.roo + org.springframework.roo.process.manager + + + + org.springframework.roo + org.springframework.roo.project + + + + org.springframework.roo + org.springframework.roo.shell + + + + org.springframework.roo + org.springframework.roo.support + + + + org.springframework.roo + org.springframework.roo.addon.plural + + + + org.springframework.roo + org.springframework.roo.addon.security + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.hapax + + + diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/RooService.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/RooService.java new file mode 100644 index 000000000..12a22bf80 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/RooService.java @@ -0,0 +1,165 @@ +package org.springframework.roo.addon.layers.service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating a service interface in a user project + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooService { + + /** + * The default prefix of the "count all" method + */ + String COUNT_ALL_METHOD = "countAll"; + + /** + * The default name of the "delete" method + */ + String DELETE_METHOD = "delete"; + + /** + * The name of this annotation's "domain types" attribute + */ + String DOMAIN_TYPES_ATTRIBUTE = "domainTypes"; + + /** + * The default prefix of the "find all" method + */ + String FIND_ALL_METHOD = "findAll"; + + /** + * The default prefix of the "find entries" method + */ + String FIND_ENTRIES_METHOD = "find"; + + /** + * The default prefix of the "find" method + */ + String FIND_METHOD = "find"; + + /** + * The default name of the "save" method + */ + String SAVE_METHOD = "save"; + + /** + * The default name of the "update" method + */ + String UPDATE_METHOD = "update"; + + /** + * Returns the prefix of the "count all" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String countAllMethod() default COUNT_ALL_METHOD; + + /** + * Returns the name of the "delete" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String deleteMethod() default DELETE_METHOD; + + /** + * Returns the domain type(s) managed by this service + * + * @return a non-null array + */ + Class[] domainTypes(); + + /** + * Returns the name of the "find all" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String findAllMethod() default FIND_ALL_METHOD; + + /** + * Returns the prefix of the "findFooEntries" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String findEntriesMethod() default FIND_ENTRIES_METHOD; + + /** + * Returns the name of the "find" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String findMethod() default FIND_METHOD; + + /** + * Returns the name of the "save" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String saveMethod() default SAVE_METHOD; + + /** + * Indicates whether the annotated service should be transactional + * + * @return see above + */ + boolean transactional() default true; + + /** + * Returns the name of the "update" method + * + * @return a blank string if the annotated type doesn't support this method + */ + String updateMethod() default UPDATE_METHOD; + + /** + * Annotates services methods with @PreAuthorize(isAuthenticated()) + * + * @return a blank string if the annotated type doesn't support this method + */ + boolean requireAuthentication() default false; + + /** + * Annotates services methods with @PreAuthorize(hasPermission()) + * + * @return a blank string if the annotated type doesn't support this method + */ + boolean usePermissionEvaluator() default false; + + /** + * Annotates update service methods with @PreAuthorize(hasRole()) + * + * @return a blank string if the annotated type doesn't support this method + */ + String[] authorizedCreateOrUpdateRoles() default { }; + + /** + * Annotates delete service methods with @PreAuthorize(hasRole()) + * + * @return a blank string if the annotated type doesn't support this method + */ + String[] authorizedDeleteRoles() default { }; + + /** + * Annotates find service methods with @PreAuthorize(hasRole()) + * + * @return a blank string if the annotated type doesn't support this method + */ + String[] authorizedReadRoles() default { }; + + /** + * Indicates whether the annotated service should be instantiated using XML + * configuration + * + * @return see above + */ + boolean useXmlConfiguration() default false; + +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValues.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValues.java new file mode 100644 index 000000000..47c427f42 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValues.java @@ -0,0 +1,106 @@ +package org.springframework.roo.addon.layers.service; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * The values of a given {@link RooService} annotation. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public class ServiceAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String countAllMethod = RooService.COUNT_ALL_METHOD; + @AutoPopulate private String deleteMethod = RooService.DELETE_METHOD; + @AutoPopulate private JavaType[] domainTypes; + @AutoPopulate private String findAllMethod = RooService.FIND_ALL_METHOD; + @AutoPopulate private String findEntriesMethod = RooService.FIND_ENTRIES_METHOD; + @AutoPopulate private String findMethod = RooService.FIND_METHOD; + @AutoPopulate private String saveMethod = RooService.SAVE_METHOD; + @AutoPopulate private boolean transactional = true; + @AutoPopulate private String updateMethod = RooService.UPDATE_METHOD; + @AutoPopulate private boolean requireAuthentication = false; + @AutoPopulate private boolean usePermissionEvaluator = false; + @AutoPopulate private String[] authorizedCreateOrUpdateRoles = new String[0]; + @AutoPopulate private String[] authorizedReadRoles = new String[0]; + @AutoPopulate private String[] authorizedDeleteRoles = new String[0]; + @AutoPopulate private boolean useXmlConfiguration = false; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata to parse (required) + */ + public ServiceAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_SERVICE); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getCountAllMethod() { + return countAllMethod; + } + + public String getDeleteMethod() { + return deleteMethod; + } + + public JavaType[] getDomainTypes() { + return domainTypes; + } + + public String getFindAllMethod() { + return findAllMethod; + } + + public String getFindEntriesMethod() { + return findEntriesMethod; + } + + public String getFindMethod() { + return findMethod; + } + + public String getSaveMethod() { + return saveMethod; + } + + public String getUpdateMethod() { + return updateMethod; + } + + public boolean isTransactional() { + return transactional; + } + + public boolean requireAuthentication() { + return requireAuthentication; + } + + public boolean usePermissionEvaluator() { + return usePermissionEvaluator; + } + + public String[] getAuthorizedCreateOrUpdateRoles() { + return authorizedCreateOrUpdateRoles; + } + + public String[] getAuthorizedReadRoles() { + return authorizedReadRoles; + } + + public String[] getAuthorizedDeleteRoles() { + return authorizedDeleteRoles; + } + + public boolean useXmlConfiguration() { + return useXmlConfiguration; + } + +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesFactory.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesFactory.java new file mode 100644 index 000000000..c13f680fa --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesFactory.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.layers.service; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.project.Path; + +/** + * A factory for {@link ServiceAnnotationValues} instances. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface ServiceAnnotationValuesFactory { + + /** + * Returns the values of the {@link RooService} annotation on the given + * service interface (assumed to be in {@link Path#SRC_MAIN_JAVA}). + * + * @param serviceInterface (required) + * @return null if the values aren't available, e.g. because + * the interface's physical details are unknown + */ + ServiceAnnotationValues getInstance( + ClassOrInterfaceTypeDetails serviceInterface); +} \ No newline at end of file diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesFactoryImpl.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesFactoryImpl.java new file mode 100644 index 000000000..31b28b15d --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesFactoryImpl.java @@ -0,0 +1,32 @@ +package org.springframework.roo.addon.layers.service; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.metadata.MetadataService; + +/** + * Factory for {@link ServiceAnnotationValues} instances. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class ServiceAnnotationValuesFactoryImpl implements + ServiceAnnotationValuesFactory { + + @Reference private MetadataService metadataService; + + public ServiceAnnotationValues getInstance( + final ClassOrInterfaceTypeDetails serviceInterface) { + final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) metadataService + .get(serviceInterface.getDeclaredByMetadataId()); + if (physicalTypeMetadata == null) { + return null; + } + return new ServiceAnnotationValues(physicalTypeMetadata); + } +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceClassMetadata.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceClassMetadata.java new file mode 100644 index 000000000..8fc83df27 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceClassMetadata.java @@ -0,0 +1,285 @@ +package org.springframework.roo.addon.layers.service; + +import static org.springframework.roo.model.SpringJavaType.PRE_AUTHORIZE; +import static org.springframework.roo.model.SpringJavaType.SERVICE; +import static org.springframework.roo.model.SpringJavaType.TRANSACTIONAL; + +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class ServiceClassMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = ServiceClassMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + /** + * Constructor + * + * @param identifier the identifier for this item of metadata (required) + * @param aspectName the Java type of the ITD (required) + * @param governorPhysicalTypeMetadata the governor, which is expected to + * contain a {@link ClassOrInterfaceTypeDetails} (required) + * @param governorDetails (required) + * @param serviceAnnotationValues (required) + * @param domainTypeToIdTypeMap (required) + * @param allCrudAdditions any additions to be made to the service class in + * order to invoke lower-layer methods (required) + * @param domainTypePlurals the plurals of each domain type managed by the + * service + */ + public ServiceClassMetadata( + final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final MemberDetails governorDetails, + final ServiceAnnotationValues serviceAnnotationValues, + final Map domainTypeToIdTypeMap, + final Map> allCrudAdditions, + final Map domainTypePlurals, String serviceName) { + + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(allCrudAdditions, "CRUD additions required"); + Validate.notNull(serviceAnnotationValues, "Annotation values required"); + Validate.notNull(governorDetails, "Governor details required"); + Validate.notNull(domainTypePlurals, "Domain type plurals required"); + + for (final Entry entry : domainTypeToIdTypeMap + .entrySet()) { + final JavaType domainType = entry.getKey(); + final JavaType idType = entry.getValue(); + final Map crudAdditions = allCrudAdditions + .get(domainType); + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + final JavaSymbolName methodName = method.getSymbolName( + serviceAnnotationValues, domainType, + domainTypePlurals.get(domainType)); + + if (methodName != null + && !governorDetails.isMethodDeclaredByAnother( + methodName, + method.getParameterTypes(domainType, idType), + getId())) { + + // The method is desired and the service class' Java file + // doesn't contain it, so generate it + final MemberTypeAdditions lowerLayerCallAdditions = crudAdditions + .get(method); + if (lowerLayerCallAdditions != null) { + // A lower layer implements it + lowerLayerCallAdditions.copyAdditionsTo(builder, + governorTypeDetails); + } + final String body = method.getBody(lowerLayerCallAdditions); + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(body); + List parameterNames = method + .getParameterNames(domainType, idType); + MethodMetadataBuilder methodMetadataBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, + method.getReturnType(domainType), + AnnotatedJavaType.convertFromJavaTypes(method + .getParameterTypes(domainType, idType)), + parameterNames, bodyBuilder); + + boolean isCreateOrUpdateMethod = false; + boolean isReadMethod = false; + boolean isDeleteMethod = false; + + // checks to see if the method is a "save" method + if (method.getKey().equals( + CustomDataKeys.PERSIST_METHOD.name()) + || method.getKey().equals( + CustomDataKeys.MERGE_METHOD.name())) { + isCreateOrUpdateMethod = true; + } + + // Checks to see if the method is a "delete method + if (method.getKey().equals( + CustomDataKeys.REMOVE_METHOD.name())) { + isDeleteMethod = true; + } + + // Checks to see if the method is a "read" method + if (method.getKey().equals( + CustomDataKeys.FIND_ALL_METHOD.name()) + || method.getKey().equals( + CustomDataKeys.FIND_ENTRIES_METHOD.name()) + || method.getKey().equals( + CustomDataKeys.FIND_METHOD.name()) + || method.getKey().equals( + CustomDataKeys.COUNT_ALL_METHOD)) { + isReadMethod = true; + } + + String authorizeValue = ""; + String authorizedRolesComponent = ""; + String permissionEvalutorComponent = ""; + + // Adds required roles to @PreAuthorize or @PostAuthorize annotation if the + // required roles for persist methods + if (serviceAnnotationValues.getAuthorizedCreateOrUpdateRoles() != null && + serviceAnnotationValues.getAuthorizedCreateOrUpdateRoles().length > 0 + && isCreateOrUpdateMethod) { + authorizedRolesComponent = getRoles(serviceAnnotationValues.getAuthorizedCreateOrUpdateRoles()); + } + + // Adds required roles to @PreAuthorize or @PostAuthorize annotation if the + // required roles exist for read methods + if (serviceAnnotationValues.getAuthorizedReadRoles() != null && + serviceAnnotationValues.getAuthorizedReadRoles().length > 0 + && isReadMethod) { + authorizedRolesComponent = getRoles(serviceAnnotationValues.getAuthorizedReadRoles()); + } + + // Adds required roles to @PreAuthorize or @PostAuthorize annotation if the + // required roles exist for delete methods + if (serviceAnnotationValues.getAuthorizedDeleteRoles() != null && + serviceAnnotationValues.getAuthorizedDeleteRoles().length > 0 + && isDeleteMethod) { + authorizedRolesComponent = getRoles(serviceAnnotationValues.getAuthorizedDeleteRoles()); + } + + final String permissionName = method.getPermissionName(domainType, + domainTypePlurals.get(domainType)); + + if (permissionName != null && serviceAnnotationValues.usePermissionEvaluator()) { + // Add hasPermission to @PreAuthorize or @PostAuthorize annotation if + // required + permissionEvalutorComponent = String.format("hasPermission(%s, '%s')", method.usesPostAuthorize() ? "returnObject" : "#" + parameterNames.get(0).getSymbolName(), permissionName); + } + + // Builds value for @PreAuthorize + if (!authorizedRolesComponent.equals("") && !permissionEvalutorComponent.equals("")) { + authorizeValue= String.format("isAuthenticated() AND ((%s) OR %s)", authorizedRolesComponent, permissionEvalutorComponent); + } + else if (!authorizedRolesComponent.equals("")) { + authorizeValue= String.format("isAuthenticated() AND (%s)", authorizedRolesComponent); + } + else if (!permissionEvalutorComponent.equals("")) { + authorizeValue= String.format("isAuthenticated() AND %s", permissionEvalutorComponent); + } + else if (serviceAnnotationValues.requireAuthentication()) { + authorizeValue ="isAuthenticated()"; + } + + if (!authorizeValue.equals("")) { + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder(method.usesPostAuthorize() ? SpringJavaType.POST_AUTHORIZE : PRE_AUTHORIZE); + annotationMetadataBuilder.addStringAttribute("value", + authorizeValue.toString()); + methodMetadataBuilder + .addAnnotation(annotationMetadataBuilder + .build()); + } + + builder.addMethod(methodMetadataBuilder); + } + } + } + + // If useXmlConfiguration is true, do not add @Service + if (!serviceAnnotationValues.useXmlConfiguration()) { + // Introduce the @Service annotation via the ITD if it's not already + // on + // the service's Java class + final AnnotationMetadata serviceAnnotation = new AnnotationMetadataBuilder( + SERVICE).build(); + if (!governorDetails.isRequestingAnnotatedWith(serviceAnnotation, + getId())) { + builder.addAnnotation(serviceAnnotation); + } + } + + // Introduce the @Transactional annotation via the ITD if it's not + // already on the service's Java class + if (serviceAnnotationValues.isTransactional()) { + final AnnotationMetadata transactionalAnnotation = new AnnotationMetadataBuilder( + TRANSACTIONAL).build(); + if (!governorDetails.isRequestingAnnotatedWith( + transactionalAnnotation, getId())) { + builder.addAnnotation(transactionalAnnotation); + } + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private String getRoles(String[] roles) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < roles.length; i++) { + if (i > 0) { + sb.append(" OR "); + } + + sb.append(String.format("hasRole('%s')", roles[i])); + } + + return sb.toString(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceClassMetadataProvider.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceClassMetadataProvider.java new file mode 100644 index 000000000..22dee1889 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceClassMetadataProvider.java @@ -0,0 +1,299 @@ +package org.springframework.roo.addon.layers.service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Provides {@link ServiceClassMetadata} for building the ITD for the + * implementation class of a user project's service. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class ServiceClassMetadataProvider extends + AbstractMemberDiscoveringItdMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ServiceClassMetadataProvider.class); + + private static final int LAYER_POSITION = LayerType.SERVICE.getPosition(); + + ProjectOperations projectOperations; + FileManager fileManager; + ServiceLayerTemplateService templateService; + + private LayerService layerService; + + private final Map managedEntityTypes = new HashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + setIgnoreTriggerAnnotations(true); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return ServiceClassMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = ServiceClassMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = ServiceClassMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Service"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = managedEntityTypes.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = managedEntityTypes.get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final ClassOrInterfaceTypeDetails serviceClass = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (serviceClass == null) { + return null; + } + + ServiceInterfaceMetadata serviceInterfaceMetadata = null; + ClassOrInterfaceTypeDetails serviceInterface = null; + for (final JavaType implementedType : serviceClass.getImplementsTypes()) { + final ClassOrInterfaceTypeDetails potentialServiceInterfaceTypeDetails = getTypeLocationService() + .getTypeDetails(implementedType); + if (potentialServiceInterfaceTypeDetails != null) { + final LogicalPath path = PhysicalTypeIdentifier + .getPath(potentialServiceInterfaceTypeDetails + .getDeclaredByMetadataId()); + final String implementedTypeId = ServiceInterfaceMetadata + .createIdentifier(implementedType, path); + if ((serviceInterfaceMetadata = (ServiceInterfaceMetadata) getMetadataService() + .get(implementedTypeId)) != null) { + // Found the metadata for the service interface + serviceInterface = potentialServiceInterfaceTypeDetails; + break; + } + } + } + if (serviceInterface == null || serviceInterfaceMetadata == null + || !serviceInterfaceMetadata.isValid()) { + return null; + } + + // Register this provider for changes to the service interface // TODO + // move this down in case we return null early below? + getMetadataDependencyRegistry().registerDependency( + serviceInterfaceMetadata.getId(), metadataIdentificationString); + + final ServiceAnnotationValues serviceAnnotationValues = serviceInterfaceMetadata + .getServiceAnnotationValues(); + final JavaType[] domainTypes = serviceAnnotationValues.getDomainTypes(); + if (domainTypes == null) { + return null; + } + + /* + * For each domain type, collect (1) the plural and (2) the additions to + * make to the service class for calling a lower layer when implementing + * each service layer method. We use LinkedHashMaps for the latter + * nested map to ensure repeatable order of code generation. + */ + final Map domainTypePlurals = new HashMap(); + final Map domainTypeToIdTypeMap = new HashMap(); + // Collect the additions for each method for each supported domain type + final Map> allCrudAdditions = new LinkedHashMap>(); + for (final JavaType domainType : domainTypes) { + + final JavaType idType = getPersistenceMemberLocator() + .getIdentifierType(domainType); + if (idType == null) { + return null; + } + domainTypeToIdTypeMap.put(domainType, idType); + // Collect the plural for this domain type + + final ClassOrInterfaceTypeDetails domainTypeDetails = getTypeLocationService() + .getTypeDetails(domainType); + if (domainTypeDetails == null) { + return null; + } + final LogicalPath path = PhysicalTypeIdentifier + .getPath(domainTypeDetails.getDeclaredByMetadataId()); + final String pluralId = PluralMetadata.createIdentifier(domainType, + path); + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(pluralId); + if (pluralMetadata == null) { + return null; + } + domainTypePlurals.put(domainType, pluralMetadata.getPlural()); + + // Maintain a list of entities that are being handled by this layer + managedEntityTypes.put(domainType, metadataIdentificationString); + + // Collect the additions the service class needs in order to invoke + // each service layer method + final Map methodAdditions = new LinkedHashMap(); + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + final Collection methodParameters = MethodParameter + .asList(method.getParameters(domainType, idType)); + final MemberTypeAdditions memberTypeAdditions = getLayerService() + .getMemberTypeAdditions(metadataIdentificationString, + method.getKey(), domainType, idType, + LAYER_POSITION, methodParameters); + if (memberTypeAdditions != null) { + // A lower layer implements this method + methodAdditions.put(method, memberTypeAdditions); + } + } + allCrudAdditions.put(domainType, methodAdditions); + + // Register this provider for changes to the domain type or its + // plural + getMetadataDependencyRegistry().registerDependency( + domainTypeDetails.getDeclaredByMetadataId(), + metadataIdentificationString); + getMetadataDependencyRegistry().registerDependency(pluralId, + metadataIdentificationString); + } + + final MemberDetails serviceClassDetails = getMemberDetailsScanner() + .getMemberDetails(getClass().getName(), serviceClass); + + // Adds or removes service from XML configuration + if (serviceAnnotationValues.useXmlConfiguration()) { + getTemplateService().addServiceToXmlConfiguration(serviceInterface, + serviceClass); + } + else { + getTemplateService().removeServiceFromXmlConfiguration(serviceInterface); + } + + return new ServiceClassMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, serviceClassDetails, + serviceAnnotationValues, domainTypeToIdTypeMap, + allCrudAdditions, domainTypePlurals, serviceInterface.getName() + .getSimpleTypeName()); + + } + + public String getProvidesType() { + return ServiceClassMetadata.getMetadataIdentiferType(); + } + + public ServiceLayerTemplateService getTemplateService(){ + if(templateService == null){ + // Get all Services implement ServiceLayerTemplateService interface + try { + ServiceReference[] references = context.getAllServiceReferences(ServiceLayerTemplateService.class.getName(), null); + + for(ServiceReference ref : references){ + return (ServiceLayerTemplateService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ServiceLayerTemplateService on SecurityOperationsImpl."); + return null; + } + }else{ + return templateService; + } + } + + public LayerService getLayerService(){ + if(layerService == null){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on SecurityOperationsImpl."); + return null; + } + }else{ + return layerService; + } + } +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceCommands.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceCommands.java new file mode 100644 index 000000000..93c50a8e4 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceCommands.java @@ -0,0 +1,114 @@ +package org.springframework.roo.addon.layers.service; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Shell commands that create domain services. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class ServiceCommands implements CommandMarker { + + @Reference private ServiceOperations serviceOperations; + @Reference private ProjectOperations projectOperations; + + @CliAvailabilityIndicator({ "service type", "service all" }) + public boolean isServiceCommandAvailable() { + return serviceOperations.isServiceInstallationPossible(); + } + + @CliAvailabilityIndicator({ "service secure type", "service secure all" }) + public boolean isSecureServiceCommandAvailable() { + return serviceOperations.isSecureServiceInstallationPossible(); + } + + @CliCommand(value = "service type", help = "Adds @RooService annotation to target type") + public void service( + @CliOption(key = "interface", mandatory = true, help = "The java interface to apply this annotation to") final JavaType interfaceType, + @CliOption(key = "class", mandatory = false, help = "Implementation class for the specified interface") JavaType classType, + @CliOption(key = "entity", unspecifiedDefaultValue = "*", optionContext = PROJECT, mandatory = false, help = "The domain entity this service should expose") final JavaType domainType, + @CliOption(key = "useXmlConfiguration", mandatory = false, help = "When true, Spring Roo will configure services using XML.") Boolean useXmlConfiguration) { + + if (classType == null) { + classType = new JavaType(interfaceType.getFullyQualifiedTypeName() + + "Impl"); + } + if (useXmlConfiguration == null) { + useXmlConfiguration = Boolean.FALSE; + } + serviceOperations.setupService(interfaceType, classType, domainType, + false, "", false, useXmlConfiguration); + } + + @CliCommand(value = "service all", help = "Adds @RooService annotation to all entities") + public void service( + @CliOption(key = "interfacePackage", mandatory = true, help = "The java interface package") final JavaPackage interfacePackage, + @CliOption(key = "classPackage", mandatory = false, help = "The java package of the implementation classes for the interfaces") JavaPackage classPackage, + @CliOption(key = "useXmlConfiguration", mandatory = false, help = "When true, Spring Roo will configure services using XML. This is the default behavior for services using GAE") Boolean useXmlConfiguration) { + + if (classPackage == null) { + classPackage = interfacePackage; + } + if (useXmlConfiguration == null) { + useXmlConfiguration = Boolean.FALSE; + } + serviceOperations.setupAllServices(interfacePackage, classPackage, + false, "", false, useXmlConfiguration); + } + + @CliCommand(value = "service secure type", help = "Adds @RooService annotation to target type with options for authentication, authorization, and a permission evaluator") + public void secureService( + @CliOption(key = "interface", mandatory = true, help = "The java interface to apply this annotation to") final JavaType interfaceType, + @CliOption(key = "class", mandatory = false, help = "Implementation class for the specified interface") JavaType classType, + @CliOption(key = "entity", unspecifiedDefaultValue = "*", optionContext = PROJECT, mandatory = false, help = "The domain entity this service should expose") final JavaType domainType, + @CliOption(key = "requireAuthentication", unspecifiedDefaultValue = "false", specifiedDefaultValue = "ture", mandatory = false, help = "Whether or not users must be authenticated to use the service") final boolean requireAuthentication, + @CliOption(key = "authorizedRoles", mandatory = false, help = "The role authorized the use the methods in the service") final String role, + @CliOption(key = "usePermissionEvaluator", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Whether or not to use a PermissionEvaluator") final boolean usePermissionEvaluator, + @CliOption(key = "useXmlConfiguration", mandatory = false, help = "When true, Spring Roo will configure services using XML.") Boolean useXmlConfiguration) { + + if (classType == null) { + classType = new JavaType(interfaceType.getFullyQualifiedTypeName() + + "Impl"); + } + if (useXmlConfiguration == null) { + useXmlConfiguration = Boolean.FALSE; + } + serviceOperations.setupService(interfaceType, classType, domainType, + requireAuthentication, role, usePermissionEvaluator, + useXmlConfiguration); + } + + @CliCommand(value = "service secure all", help = "Adds @RooService annotation to all entities with options for authentication, authorization, and a permission evaluator") + public void secureServiceAll( + @CliOption(key = "interfacePackage", mandatory = true, help = "The java interface package") final JavaPackage interfacePackage, + @CliOption(key = "classPackage", mandatory = false, help = "The java package of the implementation classes for the interfaces") JavaPackage classPackage, + @CliOption(key = "requireAuthentication", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Whether or not users must be authenticated to use the service") final boolean requireAuthentication, + @CliOption(key = "authorizedRole", mandatory = false, help = "The role authorized the use the methods in the service (additional roles can be added after creation)") final String role, + @CliOption(key = "usePermissionEvaluator", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", mandatory = false, help = "Whether or not to use a PermissionEvaluator") final boolean usePermissionEvaluator, + @CliOption(key = "useXmlConfiguration", mandatory = false, help = "When true, Spring Roo will configure services using XML.") Boolean useXmlConfiguration) { + + if (classPackage == null) { + classPackage = interfacePackage; + } + if (useXmlConfiguration == null) { + useXmlConfiguration = Boolean.FALSE; + } + serviceOperations.setupAllServices(interfacePackage, classPackage, + requireAuthentication, role, usePermissionEvaluator, + useXmlConfiguration); + } +} \ No newline at end of file diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceLocator.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceLocator.java new file mode 100644 index 000000000..0dbeaf677 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceLocator.java @@ -0,0 +1,26 @@ +package org.springframework.roo.addon.layers.service; + +import java.util.Collection; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; + +/** + * Locates service interfaces within the user's project. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface ServiceInterfaceLocator { + + /** + * Returns the details of any interfaces annotated with {@link RooService} + * that claim to support the given type of entity. + * + * @param entityType can't be null + * @return a non-null collection; empty if there's no such + * services or the given entity is null + */ + Collection getServiceInterfaces( + JavaType entityType); +} \ No newline at end of file diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceLocatorImpl.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceLocatorImpl.java new file mode 100644 index 000000000..de803c3c7 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceLocatorImpl.java @@ -0,0 +1,57 @@ +package org.springframework.roo.addon.layers.service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.DefaultPhysicalTypeMetadata; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Locates interfaces annotated with {@link RooService} that meet certain + * criteria. Factored out of {@link ServiceLayerProvider} to simplify unit + * testing of that class. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class ServiceInterfaceLocatorImpl implements ServiceInterfaceLocator { + + @Reference private TypeLocationService typeLocationService; + + public Collection getServiceInterfaces( + final JavaType domainType) { + final Set located = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_SERVICE); + final Map toReturn = new HashMap(); + for (final ClassOrInterfaceTypeDetails cid : located) { + final ServiceAnnotationValues annotationValues = new ServiceAnnotationValues( + new DefaultPhysicalTypeMetadata( + cid.getDeclaredByMetadataId(), + typeLocationService + .getPhysicalTypeCanonicalPath(cid + .getDeclaredByMetadataId()), cid)); + Validate.notNull( + annotationValues.getDomainTypes(), + "The \"domainTypes\" attribute of @RooService for type %s must be an array of types", + cid.getName().getFullyQualifiedTypeName()); + for (final JavaType javaType : annotationValues.getDomainTypes()) { + if (javaType != null && javaType.equals(domainType)) { + toReturn.put(cid.getDeclaredByMetadataId(), cid); + } + } + } + return toReturn.values(); + } +} \ No newline at end of file diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceMetadata.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceMetadata.java new file mode 100644 index 000000000..f841c7031 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceMetadata.java @@ -0,0 +1,156 @@ +package org.springframework.roo.addon.layers.service; + +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * The metadata about a service interface within a user project + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public class ServiceInterfaceMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final InvocableMemberBodyBuilder BODY = new InvocableMemberBodyBuilder(); + private static final String PROVIDES_TYPE_STRING = ServiceInterfaceMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final int PUBLIC_ABSTRACT = Modifier.PUBLIC + | Modifier.ABSTRACT; + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final ServiceAnnotationValues annotationValues; + + private final MemberDetails governorDetails; + + /** + * Constructor + * + * @param identifier (required) + * @param aspectName (required) + * @param governorPhysicalTypeMetadata (required) + * @param governorDetails (required) + * @param domainTypeToIdTypeMap (required) + * @param annotationValues (required) + * @param domainTypePlurals + */ + public ServiceInterfaceMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final MemberDetails governorDetails, + final Map domainTypeToIdTypeMap, + final ServiceAnnotationValues annotationValues, + final Map domainTypePlurals) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(governorDetails, "Governor member details required"); + Validate.notNull(domainTypeToIdTypeMap, + "Domain type to ID type map required required"); + Validate.notNull(domainTypePlurals, + "Domain type plural values required"); + + this.annotationValues = annotationValues; + this.governorDetails = governorDetails; + + for (final Entry entry : domainTypeToIdTypeMap + .entrySet()) { + final JavaType domainType = entry.getKey(); + final String plural = domainTypePlurals.get(domainType); + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + builder.addMethod(getMethod(method, domainType, + entry.getValue(), plural)); + } + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Returns the metadata for declaring the given method in the service + * interface + * + * @param method the method to declare + * @param domainType the domain type being managed + * @param idType + * @param plural the domain type's plural + * @return null if the method isn't required or is already + * declared in the governor + */ + private MethodMetadataBuilder getMethod(final ServiceLayerMethod method, + final JavaType domainType, final JavaType idType, + final String plural) { + final JavaSymbolName methodName = method.getSymbolName( + annotationValues, domainType, plural); + if (methodName != null + && governorDetails.isMethodDeclaredByAnother(methodName, + method.getParameterTypes(domainType, idType), getId())) { + // We don't want this method, or the governor already declares it + return null; + } + + return new MethodMetadataBuilder(getId(), PUBLIC_ABSTRACT, methodName, + method.getReturnType(domainType), + AnnotatedJavaType.convertFromJavaTypes(method + .getParameterTypes(domainType, idType)), + method.getParameterNames(domainType, idType), BODY); + } + + public ServiceAnnotationValues getServiceAnnotationValues() { + return annotationValues; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceMetadataProvider.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceMetadataProvider.java new file mode 100644 index 000000000..f9f5391ce --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceInterfaceMetadataProvider.java @@ -0,0 +1,227 @@ +package org.springframework.roo.addon.layers.service; + +import static org.springframework.roo.model.RooJavaType.ROO_PERMISSION_EVALUATOR; +import static org.springframework.roo.model.RooJavaType.ROO_SERVICE; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.addon.security.PermissionEvaluatorMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerTypeMatcher; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * {@link MetadataProvider} providing {@link ServiceInterfaceMetadata} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class ServiceInterfaceMetadataProvider extends + AbstractMemberDiscoveringItdMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ServiceInterfaceMetadataProvider.class); + + private CustomDataKeyDecorator customDataKeyDecorator; + + private final Map managedEntityTypes = new HashMap(); + + @SuppressWarnings("unchecked") + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + super.setDependsOnGovernorBeingAClass(false); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_SERVICE); + getCustomDataKeyDecorator().registerMatchers(getClass(), + new LayerTypeMatcher(ROO_SERVICE, new JavaSymbolName( + RooService.DOMAIN_TYPES_ATTRIBUTE))); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return ServiceInterfaceMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_SERVICE); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = ServiceInterfaceMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = ServiceInterfaceMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Service"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = managedEntityTypes.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = managedEntityTypes.get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final ServiceAnnotationValues annotationValues = new ServiceAnnotationValues( + governorPhysicalTypeMetadata); + final ClassOrInterfaceTypeDetails cid = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (cid == null) { + return null; + } + final MemberDetails memberDetails = getMemberDetailsScanner() + .getMemberDetails(getClass().getName(), cid); + final JavaType[] domainTypes = annotationValues.getDomainTypes(); + if (domainTypes == null || domainTypes.length == 0) { + return null; + } + final Map domainTypePlurals = new HashMap(); + final Map domainTypeToIdTypeMap = new HashMap(); + for (final JavaType type : domainTypes) { + final JavaType idType = getPersistenceMemberLocator() + .getIdentifierType(type); + if (idType == null) { + continue; + } + // We simply take the first disregarding any further fields which + // may be identifiers + domainTypeToIdTypeMap.put(type, idType); + final String domainTypeId = getTypeLocationService() + .getPhysicalTypeIdentifier(type); + if (domainTypeId == null) { + return null; + } + final LogicalPath path = PhysicalTypeIdentifier + .getPath(domainTypeId); + final String pluralId = PluralMetadata.createIdentifier(type, path); + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(pluralId); + if (pluralMetadata == null) { + return null; + } + // Maintain a list of entities that are being handled by this layer + managedEntityTypes.put(type, metadataIdentificationString); + getMetadataDependencyRegistry().registerDependency(pluralId, + metadataIdentificationString); + domainTypePlurals.put(type, pluralMetadata.getPlural()); + } + + PermissionEvaluatorMetadata permissionEvaluatorMetadata = null; + for (final ClassOrInterfaceTypeDetails permissionEvaluator : getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(ROO_PERMISSION_EVALUATOR)) { + if (permissionEvaluator != null) { + final LogicalPath path = PhysicalTypeIdentifier + .getPath(permissionEvaluator.getDeclaredByMetadataId()); + final String permissionEvaluatorId = PermissionEvaluatorMetadata + .createIdentifier(permissionEvaluator.getName(), path); + permissionEvaluatorMetadata = (PermissionEvaluatorMetadata) getMetadataService() + .get(permissionEvaluatorId); + if (permissionEvaluatorMetadata != null + && permissionEvaluatorMetadata.isValid()) { + if (annotationValues.usePermissionEvaluator()) { + getMetadataDependencyRegistry().registerDependency( + metadataIdentificationString, + permissionEvaluatorMetadata.getId()); + } + else { + getMetadataDependencyRegistry().deregisterDependency( + metadataIdentificationString, + permissionEvaluatorMetadata.getId()); + } + + } + } + } + + return new ServiceInterfaceMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, memberDetails, + domainTypeToIdTypeMap, annotationValues, domainTypePlurals); + } + + public String getProvidesType() { + return ServiceInterfaceMetadata.getMetadataIdentiferType(); + } + + public CustomDataKeyDecorator getCustomDataKeyDecorator(){ + if(customDataKeyDecorator == null){ + // Get all Services implement CustomDataKeyDecorator interface + try { + ServiceReference[] references = context.getAllServiceReferences(CustomDataKeyDecorator.class.getName(), null); + + for(ServiceReference ref : references){ + return (CustomDataKeyDecorator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CustomDataKeyDecorator on ServiceInterfaceMetadataProvider."); + return null; + } + }else{ + return customDataKeyDecorator; + } + + } + +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerMethod.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerMethod.java new file mode 100644 index 000000000..70ca887d3 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerMethod.java @@ -0,0 +1,471 @@ +package org.springframework.roo.addon.layers.service; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.security.Permission; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.PairList; + +/** + * A method provided by a user project's service layer + * + * @author Andrew Swan + * @author Stefan Schmidt + * @since 1.2.0 + */ +enum ServiceLayerMethod { + + // The names of these enum constants are arbitrary; calling code refers to + // these methods by their String key. + + COUNT(CustomDataKeys.COUNT_ALL_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getCountAllMethod())) { + return annotationValues.getCountAllMethod() + plural; + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Collections.emptyList(); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Collections.emptyList(); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.LONG_PRIMITIVE; + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getCountAllMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.COUNT.getName(entityType, plural); + } + }, + + DELETE(CustomDataKeys.REMOVE_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getDeleteMethod())) { + return annotationValues.getDeleteMethod() + + entityType.getSimpleTypeName(); + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(JavaSymbolName + .getReservedWordSafeName(entityType)); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays.asList(entityType); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.VOID_PRIMITIVE; + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getDeleteMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.DELETE.getName(entityType, plural); + } + }, + + FIND(CustomDataKeys.FIND_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindMethod())) { + return annotationValues.getFindMethod() + + entityType.getSimpleTypeName(); + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(new JavaSymbolName("id")); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays.asList(idType); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return entityType; + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getFindMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.FIND.getName(entityType, plural); + } + + @Override + public boolean usesPostAuthorize() { + return true; + } + }, + + FIND_ALL(CustomDataKeys.FIND_ALL_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindAllMethod())) { + return annotationValues.getFindAllMethod() + plural; + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Collections.emptyList(); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Collections.emptyList(); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.listOf(entityType); + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getFindAllMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.FIND_ALL.getName(entityType, plural); + } + }, + + FIND_ENTRIES(CustomDataKeys.FIND_ENTRIES_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getFindEntriesMethod())) { + return annotationValues.getFindEntriesMethod() + + entityType.getSimpleTypeName() + "Entries"; + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(new JavaSymbolName("firstResult"), + new JavaSymbolName("maxResults")); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays + .asList(JavaType.INT_PRIMITIVE, JavaType.INT_PRIMITIVE); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.listOf(entityType); + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getFindEntriesMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.FIND_ENTRIES.getName(entityType, plural); + } + }, + + SAVE(CustomDataKeys.PERSIST_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getSaveMethod())) { + return annotationValues.getSaveMethod() + + entityType.getSimpleTypeName(); + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(JavaSymbolName + .getReservedWordSafeName(entityType)); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays.asList(entityType); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return JavaType.VOID_PRIMITIVE; + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getSaveMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.SAVE.getName(entityType, plural); + } + }, + + UPDATE(CustomDataKeys.MERGE_METHOD) { + @Override + public String getName(final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(annotationValues.getUpdateMethod())) { + return annotationValues.getUpdateMethod() + + entityType.getSimpleTypeName(); + } + return null; + } + + @Override + public List getParameterNames( + final JavaType entityType, final JavaType idType) { + return Arrays.asList(JavaSymbolName + .getReservedWordSafeName(entityType)); + } + + @Override + public List getParameterTypes(final JavaType entityType, + final JavaType idType) { + return Arrays.asList(entityType); + } + + @Override + public JavaType getReturnType(final JavaType entityType) { + return entityType; + } + + @Override + public String getBaseName(final ServiceAnnotationValues annotationValues) { + return annotationValues.getUpdateMethod(); + } + + @Override + public String getPermissionName(final JavaType entityType, final String plural) { + return Permission.UPDATE.getName(entityType, plural); + } + }; + + /** + * Returns the {@link ServiceLayerMethod} with the given properties, if any + * + * @param methodIdentifier the internal ID of the method (can be blank) + * @param callerParameters the types of parameter to be passed to the method + * (required) + * @param targetEntity the type of entity being managed (required) + * @param idType specifies the ID type used by the target entity (required) + * @return null if a blank or unknown ID is given + */ + public static ServiceLayerMethod valueOf(final String methodIdentifier, + final List callerParameters, final JavaType targetEntity, + final JavaType idType) { + // Look for matching method name and parameter types + for (final ServiceLayerMethod method : values()) { + if (method.getKey().equals(methodIdentifier) + && method.getParameterTypes(targetEntity, idType).equals( + callerParameters)) { + return method; + } + } + return null; + } + + private final MethodMetadataCustomDataKey key; + + /** + * Constructor + * + * @param key the internal key for this method (required) + */ + private ServiceLayerMethod(final MethodMetadataCustomDataKey key) { + Validate.notNull(key, "Method key is required"); + this.key = key; + } + + /** + * Returns the line(s) of Java code that implement this method + * + * @param lowerLayerAdditions the details of a call to a lower layer, if any + * @return a non-blank string + */ + public String getBody(final MemberTypeAdditions lowerLayerAdditions) { + if (lowerLayerAdditions == null) { + // No lower layer implements this method; so we stub it + return "throw new UnsupportedOperationException(\"Implement me!\");"; + } + // A lower layer implements it; generate a delegation call + String line = ""; + if (!isVoid()) { + line = "return "; + } + line += lowerLayerAdditions.getMethodCall() + ";"; + return line; + } + + /** + * Returns the key identifying this method + * + * @return a non-blank string that's unique within this enum + */ + public String getKey() { + return key.name(); + } + + public abstract String getBaseName(final ServiceAnnotationValues annotationValues); + + public abstract String getPermissionName(final JavaType entityType, final String plural); + + /** + * Returns the name of this method, based on the given inputs + * + * @param annotationValues the values of the {@link RooService} annotation + * on the service + * @param entityType the type of domain entity managed by the service + * @param plural the plural form of the entity + * @return null if the method is not implemented + */ + public abstract String getName(ServiceAnnotationValues annotationValues, + JavaType entityType, String plural); + + /** + * Returns the names of this method's declared parameters + * + * @param entityType the type of domain entity managed by the service + * (required) + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null list (might be empty) + */ + public abstract List getParameterNames(JavaType entityType, + JavaType idType); + + /** + * Returns the types and names of the parameters declared by this method for + * the given domain type + * + * @param domainType the domain type to which the method applies (required) + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null list + */ + public PairList getParameters( + final JavaType domainType, final JavaType idType) { + return new PairList(getParameterTypes( + domainType, idType), getParameterNames(domainType, idType)); + } + + /** + * Returns the types of parameters taken by this method + * + * @param entityType the type of entity to which this method applies + * (required) + * @param idType specifies the ID type used by the target entity (required) + * @return a non-null copy of list (might be empty) + */ + public abstract List getParameterTypes(JavaType entityType, + JavaType idType); + + /** + * Returns this method's return type + * + * @param entityType the type of entity being managed + * @return a non-null type + */ + public abstract JavaType getReturnType(JavaType entityType); + + /** + * Returns the name of this method, based on the given inputs + * + * @param annotationValues the values of the {@link RooService} annotation + * on the service + * @param entityType the type of domain entity managed by the service + * @param plural the plural form of the entity + * @return null if the method is not implemented + */ + public JavaSymbolName getSymbolName( + final ServiceAnnotationValues annotationValues, + final JavaType entityType, final String plural) { + final String methodName = getName(annotationValues, entityType, plural); + if (StringUtils.isNotBlank(methodName)) { + return new JavaSymbolName(methodName); + } + return null; + } + + /** + * Indicates whether this method is void, i.e. returns nothing + * + * @return see above + */ + boolean isVoid() { + return JavaType.VOID_PRIMITIVE.equals(getReturnType(null)); + } + + /** + * Indicates whether Spring method security should be applied after the method has executed + * + * @return see above + */ + public boolean usesPostAuthorize() { + return false; + } +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerProvider.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerProvider.java new file mode 100644 index 000000000..a9a60ddd9 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerProvider.java @@ -0,0 +1,318 @@ +package org.springframework.roo.addon.layers.service; + +import static java.lang.reflect.Modifier.PRIVATE; +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.CoreLayerProvider; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.PairList; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * The {@link org.springframework.roo.classpath.layers.LayerProvider} that + * provides an application's service layer. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class ServiceLayerProvider extends CoreLayerProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ServiceLayerProvider.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private MetadataService metadataService; + private ServiceAnnotationValuesFactory serviceAnnotationValuesFactory; + private ServiceInterfaceLocator serviceInterfaceLocator; + TypeLocationService typeLocationService; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + public int getLayerPosition() { + return LayerType.SERVICE.getPosition(); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final MethodParameter... methodParameters) { + return getMemberTypeAdditions(callerMID, methodIdentifier, + targetEntity, idType, true, methodParameters); + } + + public MemberTypeAdditions getMemberTypeAdditions(final String callerMID, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, boolean autowire, + final MethodParameter... methodParameters) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + + Validate.notNull(metadataService, "MetadataService is required"); + + if(serviceAnnotationValuesFactory == null){ + serviceAnnotationValuesFactory = getServiceAnnotationValuesFactory(); + } + + Validate.notNull(serviceAnnotationValuesFactory, "ServiceAnnotationValuesFactory is required"); + + if(serviceInterfaceLocator == null){ + serviceInterfaceLocator = getServiceInterfaceLocator(); + } + + Validate.notNull(serviceInterfaceLocator, "ServiceInterfaceLocator is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + Validate.notBlank(callerMID, "Caller's metadata identifier required"); + Validate.notNull(methodIdentifier, "Method identifier required"); + Validate.notNull(targetEntity, "Target entity type required"); + Validate.notNull(methodParameters, + "Method param names and types required (may be empty)"); + + // Check whether this is even a known service layer method + final List parameterTypes = new PairList( + methodParameters).getKeys(); + final ServiceLayerMethod method = ServiceLayerMethod.valueOf( + methodIdentifier, parameterTypes, targetEntity, idType); + if (method == null) { + return null; + } + + // Check the entity has a plural form + final String pluralId = PluralMetadata.createIdentifier(targetEntity, + typeLocationService.getTypePath(targetEntity)); + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(pluralId); + if (pluralMetadata == null || pluralMetadata.getPlural() == null) { + return null; + } + + // Loop through the service interfaces that claim to support the given + // target entity + for (final ClassOrInterfaceTypeDetails serviceInterface : serviceInterfaceLocator + .getServiceInterfaces(targetEntity)) { + // Get the values of the @RooService annotation for this service + // interface + final ServiceAnnotationValues annotationValues = serviceAnnotationValuesFactory + .getInstance(serviceInterface); + if (annotationValues != null) { + + // Check whether this method is implemented by the given service + final String methodName = method.getName(annotationValues, + targetEntity, pluralMetadata.getPlural()); + if (StringUtils.isNotBlank(methodName)) { + // The service implements the method; get the additions to + // be made by the caller + final MemberTypeAdditions methodAdditions = getMethodAdditions( + callerMID, methodName, serviceInterface.getName(), + Arrays.asList(methodParameters), autowire); + + // Return these additions + return methodAdditions; + } + } + } + // None of the services for this entity were able to provide the method + return null; + } + + /** + * Returns the additions the caller should make in order to invoke the given + * method for the given domain entity. + * + * @param callerMID the caller's metadata ID (required) + * @param methodName the name of the method being invoked (required) + * @param serviceInterface the domain service type (required) + * @param parameterNames the names of the parameters being passed by the + * caller to the method + * @return a non-null set of additions + */ + private MemberTypeAdditions getMethodAdditions(final String callerMID, + final String methodName, final JavaType serviceInterface, + final List parameters, boolean autowire) { + // The method is supported by this service interface; make a builder + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + callerMID); + + final String fieldName = StringUtils.uncapitalize(serviceInterface + .getSimpleTypeName()); + + if (autowire) { + // Add an autowired field of the type of this service + cidBuilder.addField(new FieldMetadataBuilder(callerMID, 0, Arrays + .asList(new AnnotationMetadataBuilder(AUTOWIRED)), + new JavaSymbolName(fieldName), serviceInterface)); + } + else { + // Add a set method of the type of this service + cidBuilder.addField(new FieldMetadataBuilder(callerMID, 0, + new JavaSymbolName(fieldName), serviceInterface, null)); + JavaSymbolName setMethodName = new JavaSymbolName("set" + + serviceInterface.getSimpleTypeName()); + List parameterTypes = new ArrayList(); + parameterTypes.add(serviceInterface); + List parameterNames = new ArrayList(); + parameterNames.add(new JavaSymbolName(fieldName)); + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.append("\n\tthis." + fieldName + " = " + fieldName + + ";\n"); + + MethodMetadataBuilder setSeviceMethod = new MethodMetadataBuilder( + callerMID, PUBLIC, setMethodName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + + cidBuilder.addMethod(setSeviceMethod); + } + + // Generate an additions object that includes a call to the method + return MemberTypeAdditions.getInstance(cidBuilder, fieldName, + methodName, false, parameters); + } + + // -------------------- Setters for use by unit tests ---------------------- + + void setMetadataService(final MetadataService metadataService) { + if(metadataService == null){ + this.metadataService = getMetadataService(); + + Validate.notNull(metadataService, "MetadataService is required"); + + }else{ + this.metadataService = metadataService; + } + } + + void setServiceAnnotationValuesFactory( + final ServiceAnnotationValuesFactory serviceAnnotationValuesFactory) { + if(serviceAnnotationValuesFactory == null){ + this.serviceAnnotationValuesFactory = getServiceAnnotationValuesFactory(); + Validate.notNull(serviceAnnotationValuesFactory, "ServiceAnnotationValuesFactory is required"); + }else{ + this.serviceAnnotationValuesFactory = serviceAnnotationValuesFactory; + } + + } + + void setServiceInterfaceLocator( + final ServiceInterfaceLocator serviceInterfaceLocator) { + if(serviceInterfaceLocator == null){ + this.serviceInterfaceLocator = getServiceInterfaceLocator(); + Validate.notNull(serviceInterfaceLocator, "ServiceInterfaceLocator is required"); + }else{ + this.serviceInterfaceLocator = serviceInterfaceLocator; + } + + } + + public MetadataService getMetadataService(){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on ServiceLayerProvider."); + return null; + } + } + + public ServiceAnnotationValuesFactory getServiceAnnotationValuesFactory(){ + // Get all Services implement ServiceAnnotationValuesFactory interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ServiceAnnotationValuesFactory.class.getName(), null); + + for(ServiceReference ref : references){ + return (ServiceAnnotationValuesFactory) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ServiceAnnotationValuesFactory on ServiceLayerProvider."); + return null; + } + } + + public ServiceInterfaceLocator getServiceInterfaceLocator(){ + // Get all Services implement ServiceInterfaceLocator interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ServiceInterfaceLocator.class.getName(), null); + + for(ServiceReference ref : references){ + return (ServiceInterfaceLocator) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ServiceInterfaceLocator on ServiceLayerProvider."); + return null; + } + } + + public TypeLocationService getTypeLocationService(){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on ServiceLayerProvider."); + return null; + } + } + + +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerTemplateService.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerTemplateService.java new file mode 100644 index 000000000..5d2e37f64 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerTemplateService.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.layers.service; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; + +public interface ServiceLayerTemplateService { + public void addServiceToXmlConfiguration( + ClassOrInterfaceTypeDetails serviceInterface, + ClassOrInterfaceTypeDetails serviceClass); + + public void removeServiceFromXmlConfiguration( + ClassOrInterfaceTypeDetails serviceInterface); +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerTemplateServiceImpl.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerTemplateServiceImpl.java new file mode 100644 index 000000000..689e437e6 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceLayerTemplateServiceImpl.java @@ -0,0 +1,168 @@ +package org.springframework.roo.addon.layers.service; + +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringWriter; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +@Component +@Service +public class ServiceLayerTemplateServiceImpl implements + ServiceLayerTemplateService { + @Reference ProjectOperations projectOperations; + @Reference FileManager fileManager; + + @Override + public void addServiceToXmlConfiguration( + ClassOrInterfaceTypeDetails serviceInterface, + ClassOrInterfaceTypeDetails serviceClass) { + final PathResolver pathResolver = projectOperations.getPathResolver(); + + final String fileIdentifier = pathResolver.getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, "applicationContext-services.xml"); + + if (!fileManager.exists(fileIdentifier)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "applicationContext-services-template.xml"); + outputStream = fileManager.createFile(fileIdentifier) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + try { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + + InputSource source = new InputSource(); + FileReader fileReader = new FileReader(fileIdentifier); + source.setCharacterStream(fileReader); + final Document document = builder.parse(source); + + final String serviceName = StringUtils + .uncapitalize(serviceInterface.getType() + .getSimpleTypeName()); + + Element serviceElement = XmlUtils.findFirstElement("//*[@id='" + + serviceName + "']", document.getDocumentElement()); + + if (serviceElement != null) + return; + + serviceElement = document.createElement("bean"); + serviceElement.setAttribute("id", serviceName); + serviceElement.setAttribute("class", serviceClass.getType() + .getFullyQualifiedTypeName()); + Node beansNode = document.getElementsByTagName("beans").item(0); + if (beansNode.getNodeType() == Node.ELEMENT_NODE) { + Element beansElement = (Element) beansNode; + beansElement.appendChild(serviceElement); + // final Transformer transformer = + // XmlUtils.createIndentingTransformer(); + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer transformer = transfac.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + final DOMSource domSource = new DOMSource(document); + final StreamResult result = new StreamResult(new StringWriter()); + transformer.transform(domSource, result); + String output = result.getWriter().toString(); + + fileManager.createOrUpdateTextFileIfRequired(fileIdentifier, + output, true); + } + + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public void removeServiceFromXmlConfiguration( + ClassOrInterfaceTypeDetails serviceInterface) { + final PathResolver pathResolver = projectOperations.getPathResolver(); + + final String fileIdentifier = pathResolver.getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, "applicationContext-services.xml"); + + if (!fileManager.exists(fileIdentifier)) { + return; + } + + try { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + + InputSource source = new InputSource(); + FileReader fileReader = new FileReader(fileIdentifier); + source.setCharacterStream(fileReader); + final Document document = builder.parse(source); + + final String serviceName = StringUtils + .uncapitalize(serviceInterface.getType() + .getSimpleTypeName()); + + Element serviceElement = XmlUtils.findFirstElement("//*[@id='" + + serviceName + "']", document.getDocumentElement()); + + if (serviceElement == null) + return; + + Node beansNode = document.getElementsByTagName("beans").item(0); + if (beansNode.getNodeType() == Node.ELEMENT_NODE) { + Element beansElement = (Element) beansNode; + beansElement.removeChild(serviceElement); + // final Transformer transformer = + // XmlUtils.createIndentingTransformer(); + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer transformer = transfac.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + final DOMSource domSource = new DOMSource(document); + final StreamResult result = new StreamResult(new StringWriter()); + transformer.transform(domSource, result); + String output = result.getWriter().toString(); + + fileManager.createOrUpdateTextFileIfRequired(fileIdentifier, + output, true); + } + + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + + } +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceOperations.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceOperations.java new file mode 100644 index 000000000..4b0a31ff5 --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceOperations.java @@ -0,0 +1,26 @@ +package org.springframework.roo.addon.layers.service; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface ServiceOperations { + + boolean isServiceInstallationPossible(); + + boolean isSecureServiceInstallationPossible(); + + void setupService(JavaType interfaceType, JavaType classType, + JavaType domainType, boolean requireAuthentication, + String authorizedRole, boolean usePermissionEvalutor, + boolean useXmlConfiguration); + + void setupAllServices(JavaPackage interfacePackage, + JavaPackage classPackage, boolean requireAuthentication, + String authorizedRole, boolean usePermissionEvalutor, + boolean useXmlConfiguration); + +} diff --git a/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceOperationsImpl.java b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceOperationsImpl.java new file mode 100644 index 000000000..2e81a001b --- /dev/null +++ b/addon-layers-service/src/main/java/org/springframework/roo/addon/layers/service/ServiceOperationsImpl.java @@ -0,0 +1,193 @@ +package org.springframework.roo.addon.layers.service; + +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.classpath.PhysicalTypeCategory.CLASS; +import static org.springframework.roo.classpath.PhysicalTypeCategory.INTERFACE; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY; +import static org.springframework.roo.model.RooJavaType.ROO_PERMISSION_EVALUATOR; +import static org.springframework.roo.model.RooJavaType.ROO_SERVICE; + +import java.util.Arrays; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; + +/** + * The {@link ServiceOperations} implementation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class ServiceOperationsImpl implements ServiceOperations { + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private TypeManagementService typeManagementService; + @Reference private TypeLocationService typeLocationService; + + private void createServiceClass(final JavaType interfaceType, + final JavaType classType) { + Validate.notNull(classType, "Class type required"); + final String classIdentifier = pathResolver.getFocusedCanonicalPath( + Path.SRC_MAIN_JAVA, classType); + if (fileManager.exists(classIdentifier)) { + return; // Type already exists - nothing to do + } + final String classMid = PhysicalTypeIdentifier.createIdentifier( + classType, pathResolver.getPath(classIdentifier)); + final ClassOrInterfaceTypeDetailsBuilder classTypeBuilder = new ClassOrInterfaceTypeDetailsBuilder( + classMid, PUBLIC, classType, CLASS); + classTypeBuilder.addImplementsType(interfaceType); + + typeManagementService + .createOrUpdateTypeOnDisk(classTypeBuilder.build()); + } + + private void createServiceInterface(final JavaType interfaceType, + final JavaType domainType, boolean requireAuthentication, + String role, boolean usePermissionEvaluator, + boolean useXmlConfiguration) { + final String interfaceIdentifier = pathResolver + .getFocusedCanonicalPath(Path.SRC_MAIN_JAVA, interfaceType); + if (fileManager.exists(interfaceIdentifier)) { + return; // Type already exists - nothing to do + } + Validate.notNull(domainType, "Domain type required"); + final AnnotationMetadataBuilder interfaceAnnotationMetadata = new AnnotationMetadataBuilder( + ROO_SERVICE); + interfaceAnnotationMetadata + .addAttribute(new ArrayAttributeValue( + new JavaSymbolName("domainTypes"), Arrays + .asList(new ClassAttributeValue( + new JavaSymbolName("foo"), domainType)))); + if (role == null) { + role = ""; + } + if (requireAuthentication || usePermissionEvaluator || !role.equals("")) { + interfaceAnnotationMetadata.addBooleanAttribute( + "requireAuthentication", requireAuthentication); + } + if (!role.equals("")) { + interfaceAnnotationMetadata + .addAttribute(new ArrayAttributeValue( + new JavaSymbolName("authorizedCreateOrUpdateRoles"), + Arrays.asList(new StringAttributeValue( + new JavaSymbolName("bar"), role)))); + interfaceAnnotationMetadata + .addAttribute(new ArrayAttributeValue( + new JavaSymbolName("authorizedReadRoles"), Arrays + .asList(new StringAttributeValue( + new JavaSymbolName("bar"), role)))); + interfaceAnnotationMetadata + .addAttribute(new ArrayAttributeValue( + new JavaSymbolName("authorizedDeleteRoles"), Arrays + .asList(new StringAttributeValue( + new JavaSymbolName("bar"), role)))); + } + if (usePermissionEvaluator) { + interfaceAnnotationMetadata.addBooleanAttribute( + "usePermissionEvaluator", true); + } + if (useXmlConfiguration) { + interfaceAnnotationMetadata.addBooleanAttribute( + "useXmlConfiguration", true); + } + final String interfaceMid = PhysicalTypeIdentifier.createIdentifier( + interfaceType, pathResolver.getPath(interfaceIdentifier)); + final ClassOrInterfaceTypeDetailsBuilder interfaceTypeBuilder = new ClassOrInterfaceTypeDetailsBuilder( + interfaceMid, PUBLIC, interfaceType, INTERFACE); + interfaceTypeBuilder.addAnnotation(interfaceAnnotationMetadata.build()); + typeManagementService.createOrUpdateTypeOnDisk(interfaceTypeBuilder + .build()); + } + + private boolean isPermissionEvaluatorInstalled() { + Set types = typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_PERMISSION_EVALUATOR); + return types.size() > 0; + } + + @Override + public boolean isServiceInstallationPossible() { + return projectOperations.isFocusedProjectAvailable(); + } + + @Override + public boolean isSecureServiceInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && projectOperations.isFeatureInstalled(FeatureNames.SECURITY); + } + + @Override + public void setupService(final JavaType interfaceType, + final JavaType classType, final JavaType domainType, + boolean requireAuthentication, String role, + boolean usePermissionEvaluator, boolean useXmlConfiguration) { + + // Verify that security is installed + if (requireAuthentication || !role.equals("") || usePermissionEvaluator) { + Validate.isTrue( + projectOperations.isFeatureInstalled(FeatureNames.SECURITY), + "Security must first be setup before securing a method"); + } + + // Verify PermissionEvaluator has been created + if (usePermissionEvaluator) { + Validate.isTrue(isPermissionEvaluatorInstalled(), + "Permission evaluator must be installed (use permissionEvaluator command)"); + } + + Validate.notNull(interfaceType, "Interface type required"); + createServiceInterface(interfaceType, domainType, + requireAuthentication, role, usePermissionEvaluator, + useXmlConfiguration); + createServiceClass(interfaceType, classType); + } + + @Override + public void setupAllServices(JavaPackage interfacePackage, + JavaPackage classPackage, boolean requireAuthentication, + String role, boolean usePermissionEvaluator, + boolean useXmlConfiguration) { + for (final ClassOrInterfaceTypeDetails domainType : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_JPA_ENTITY, + ROO_JPA_ACTIVE_RECORD)) { + JavaType interfaceType = new JavaType( + interfacePackage.getFullyQualifiedPackageName() + "." + + domainType.getName().getSimpleTypeName() + + "Service"); + JavaType classType = new JavaType( + classPackage.getFullyQualifiedPackageName() + "." + + domainType.getName().getSimpleTypeName() + + "ServiceImpl"); + setupService(interfaceType, classType, domainType.getName(), + requireAuthentication, role, usePermissionEvaluator, + useXmlConfiguration); + } + } + +} diff --git a/addon-layers-service/src/main/resources/org/springframework/roo/addon/layers/service/applicationContext-services-template.xml b/addon-layers-service/src/main/resources/org/springframework/roo/addon/layers/service/applicationContext-services-template.xml new file mode 100644 index 000000000..afa32c2dc --- /dev/null +++ b/addon-layers-service/src/main/resources/org/springframework/roo/addon/layers/service/applicationContext-services-template.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesTest.java b/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesTest.java new file mode 100644 index 000000000..deedd4579 --- /dev/null +++ b/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.layers.service; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link ServiceAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ServiceAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooService.class; + } + + @Override + protected Class getValuesClass() { + return ServiceAnnotationValues.class; + } +} diff --git a/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceLayerMethodTest.java b/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceLayerMethodTest.java new file mode 100644 index 000000000..e0869d4ee --- /dev/null +++ b/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceLayerMethodTest.java @@ -0,0 +1,164 @@ +package org.springframework.roo.addon.layers.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.FIND_ALL; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.FIND_ENTRIES; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.SAVE; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.UPDATE; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.valueOf; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.PairList; + +/** + * Unit test of the {@link ServiceLayerMethod} enum + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ServiceLayerMethodTest { + + private static final JavaType ID_TYPE = LONG_OBJECT; + private static final String PLURAL = "People"; + private static final JavaType TARGET_ENTITY = new JavaType( + "com.example.Person"); + + @Test + public void testEachMethodHasNonNullReturnType() { + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + assertNotNull(method.getReturnType(TARGET_ENTITY)); + } + } + + @Test + public void testEachMethodHasSameNumberOfParameterTypesAndNames() { + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + final List parameterNames = method + .getParameterNames(TARGET_ENTITY, ID_TYPE); + final List parameterTypes = method.getParameterTypes( + TARGET_ENTITY, ID_TYPE); + final PairList parameters = method + .getParameters(TARGET_ENTITY, ID_TYPE); + assertEquals(parameterTypes.size(), parameterNames.size()); + assertEquals(parameterTypes, parameters.getKeys()); + assertEquals(parameterNames, parameters.getValues()); + } + } + + @Test + public void testEachMethodHasUniqueParameterNames() { + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + final List allParameterNames = method + .getParameterNames(TARGET_ENTITY, ID_TYPE); + final Set distinctNames = new HashSet( + allParameterNames); + assertEquals(allParameterNames.size(), distinctNames.size()); + } + } + + @Test + public void testGetBodyWhenLowerLayerDoesNotImplementMethod() { + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + assertEquals( + "throw new UnsupportedOperationException(\"Implement me!\");", + method.getBody(null)); + } + } + + @Test + public void testGetBodyWhenLowerLayerImplementsMethod() { + final MemberTypeAdditions mockLowerLayerAdditions = mock(MemberTypeAdditions.class); + when(mockLowerLayerAdditions.getMethodCall()).thenReturn("foo()"); + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + if (method.isVoid()) { + assertEquals("foo();", method.getBody(mockLowerLayerAdditions)); + } + else { + assertEquals("return foo();", + method.getBody(mockLowerLayerAdditions)); + } + } + } + + @Test + public void testGetNameOfFindAllMethodWhenAnnotationHasNonBlankName() { + final ServiceAnnotationValues mockAnnotationValues = mock(ServiceAnnotationValues.class); + when(mockAnnotationValues.getFindAllMethod()).thenReturn("getAll"); + assertEquals("getAllPeople", + FIND_ALL.getName(mockAnnotationValues, TARGET_ENTITY, PLURAL)); + assertEquals( + "getAllPeople", + FIND_ALL.getSymbolName(mockAnnotationValues, TARGET_ENTITY, + PLURAL).getSymbolName()); + } + + @Test + public void testGetNameOfFindEntriesMethodWhenAnnotationHasNonBlankName() { + final ServiceAnnotationValues mockAnnotationValues = mock(ServiceAnnotationValues.class); + when(mockAnnotationValues.getFindEntriesMethod()).thenReturn("get"); + assertEquals("getPersonEntries", FIND_ENTRIES.getName( + mockAnnotationValues, TARGET_ENTITY, PLURAL)); + } + + @Test + public void testGetNameOfSaveMethodWhenAnnotationHasNonBlankName() { + final ServiceAnnotationValues mockAnnotationValues = mock(ServiceAnnotationValues.class); + when(mockAnnotationValues.getSaveMethod()).thenReturn("store"); + assertEquals("storePerson", + SAVE.getName(mockAnnotationValues, TARGET_ENTITY, PLURAL)); + } + + @Test + public void testGetNameOfUpdateMethodWhenAnnotationHasNonBlankName() { + final ServiceAnnotationValues mockAnnotationValues = mock(ServiceAnnotationValues.class); + when(mockAnnotationValues.getUpdateMethod()).thenReturn("change"); + assertEquals("changePerson", + UPDATE.getName(mockAnnotationValues, TARGET_ENTITY, PLURAL)); + } + + @Test + public void testGetNameWhenAnnotationHasBlankName() { + final ServiceAnnotationValues mockAnnotationValues = mock(ServiceAnnotationValues.class); + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + assertNull(method.getName(mockAnnotationValues, TARGET_ENTITY, "x")); + assertNull(method.getSymbolName(mockAnnotationValues, + TARGET_ENTITY, "x")); + } + } + + @Test + public void testValueOfMethodUsingCorrectDetails() { + for (final ServiceLayerMethod method : ServiceLayerMethod.values()) { + assertEquals( + method, + valueOf(method.getKey(), + method.getParameterTypes(TARGET_ENTITY, ID_TYPE), + TARGET_ENTITY, ID_TYPE)); + } + } + + @Test + public void testValueOfMethodUsingWrongName() { + assertNull(valueOf("x", Arrays. asList(), TARGET_ENTITY, + ID_TYPE)); + } + + @Test + public void testValueOfMethodUsingWrongParameterTypes() { + assertNull(valueOf(FIND_ALL.getKey(), + Arrays.asList(JavaType.BYTE_OBJECT), TARGET_ENTITY, ID_TYPE)); + } +} diff --git a/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceLayerProviderTest.java b/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceLayerProviderTest.java new file mode 100644 index 000000000..fc67a9b15 --- /dev/null +++ b/addon-layers-service/src/test/java/org/springframework/roo/addon/layers/service/ServiceLayerProviderTest.java @@ -0,0 +1,279 @@ +package org.springframework.roo.addon.layers.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.FIND_ALL; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.FIND_ENTRIES; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.SAVE; +import static org.springframework.roo.addon.layers.service.ServiceLayerMethod.UPDATE; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Path; + +/** + * Unit test of {@link ServiceLayerProvider} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ServiceLayerProviderTest { + + private static final String BOGUS_METHOD = "bogus"; + private static final String CALLER_MID = "MID:anything#com.example.web.PersonController"; + private static final String SERVICE_MID = "MID:anything#com.example.serv.PersonService"; + private static final MethodParameter SIZE_PARAMETER = new MethodParameter( + JavaType.INT_PRIMITIVE, "count"); + private static final MethodParameter START_PARAMETER = new MethodParameter( + JavaType.INT_PRIMITIVE, "start"); + + // Fixture + + @Mock private JavaType mockIdType; + @Mock private MetadataService mockMetadataService; + @Mock private ServiceAnnotationValuesFactory mockServiceAnnotationValuesFactory; + @Mock private ServiceInterfaceLocator mockServiceInterfaceLocator; + // -- Mocks + @Mock private JavaType mockTargetType; + @Mock private TypeLocationService mockTypeLocationService; + + private String pluralId; + // -- Others + private ServiceLayerProvider provider; + + /** + * Asserts that asking the {@link ServiceLayerProvider} for a method with + * the given name and parameters results in the given method signature + * + * @param plural + * @param mockServiceInterfaces can be empty + * @param methodId + * @param expectedMethodSignature null means no additions are + * expected + * @param methodParameters + */ + private void assertAdditions(final String plural, + final List mockServiceInterfaces, + final String methodId, final String expectedMethodSignature, + final MethodParameter... methodParameters) { + // Set up + setUpPluralMetadata(plural); + when(mockServiceInterfaceLocator.getServiceInterfaces(mockTargetType)) + .thenReturn(mockServiceInterfaces); + + // Invoke + final MemberTypeAdditions additions = provider.getMemberTypeAdditions( + CALLER_MID, methodId, mockTargetType, mockIdType, + methodParameters); + + // Check + if (expectedMethodSignature == null) { + assertNull("Expected no additions but found: " + additions, + additions); + } + else { + assertNotNull("Expected some additions but was null", additions); + assertEquals(expectedMethodSignature, additions.getMethodCall()); + } + } + + /** + * Sets up a mock {@link ClassOrInterfaceTypeDetails} for a service + * interface whose {@link RooService} annotation specifies the following + * method names + * + * @param findAllMethod can be blank + * @param saveMethod can be blank + * @param updateMethod can be blank + * @param findEntriesMethod can be blank + * @return a non-null mock + */ + private ClassOrInterfaceTypeDetails getMockService( + final String findAllMethod, final String saveMethod, + final String updateMethod, final String findEntriesMethod) { + final ClassOrInterfaceTypeDetails mockServiceInterface = mock(ClassOrInterfaceTypeDetails.class); + final JavaType mockServiceType = mock(JavaType.class); + final ServiceAnnotationValues mockServiceAnnotationValues = mock(ServiceAnnotationValues.class); + + when(mockServiceType.getSimpleTypeName()).thenReturn("PersonService"); + when(mockServiceInterface.getName()).thenReturn(mockServiceType); + when(mockServiceInterface.getDeclaredByMetadataId()).thenReturn( + SERVICE_MID); + when(mockServiceAnnotationValues.getFindAllMethod()).thenReturn( + findAllMethod); + when(mockServiceAnnotationValues.getFindEntriesMethod()).thenReturn( + findEntriesMethod); + when(mockServiceAnnotationValues.getSaveMethod()) + .thenReturn(saveMethod); + when(mockServiceAnnotationValues.getUpdateMethod()).thenReturn( + updateMethod); + when( + mockServiceAnnotationValuesFactory + .getInstance(mockServiceInterface)).thenReturn( + mockServiceAnnotationValues); + + return mockServiceInterface; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + provider = new ServiceLayerProvider(); + provider.setMetadataService(mockMetadataService); + provider.setServiceAnnotationValuesFactory(mockServiceAnnotationValuesFactory); + provider.setServiceInterfaceLocator(mockServiceInterfaceLocator); + provider.typeLocationService = mockTypeLocationService; + + when(mockTargetType.getFullyQualifiedTypeName()).thenReturn( + "com.example.domain.Person"); + when(mockIdType.getFullyQualifiedTypeName()).thenReturn( + Long.class.getName()); + when(mockTargetType.getSimpleTypeName()).thenReturn("Person"); + when(mockTypeLocationService.getTypePath(mockTargetType)).thenReturn( + Path.SRC_MAIN_JAVA.getModulePathId("")); + pluralId = PluralMetadata.createIdentifier(mockTargetType, + Path.SRC_MAIN_JAVA.getModulePathId("")); + } + + /** + * Sets up the mock {@link MetadataService} to return the given plural text + * for our test entity type. + * + * @param plural can be null + */ + private void setUpPluralMetadata(final String plural) { + final PluralMetadata mockPluralMetadata = mock(PluralMetadata.class); + when(mockPluralMetadata.getPlural()).thenReturn(plural); + when(mockMetadataService.get(pluralId)).thenReturn(mockPluralMetadata); + } + + @Test + public void testGetAdditionsForBogusMethod() { + final ClassOrInterfaceTypeDetails mockServiceInterface = mock(ClassOrInterfaceTypeDetails.class); + assertAdditions("x", Arrays.asList(mockServiceInterface), BOGUS_METHOD, + null); + } + + @Test + public void testGetAdditionsForEntityWithNoServices() { + assertAdditions("x", Arrays. asList(), + BOGUS_METHOD, null); + } + + @Test + public void testGetAdditionsForEntityWithNullPluralMetadata() { + // Set up + when(mockMetadataService.get(pluralId)).thenReturn(null); + + // Invoke + final MemberTypeAdditions additions = provider.getMemberTypeAdditions( + CALLER_MID, BOGUS_METHOD, mockTargetType, mockIdType); + + // Check + assertNull(additions); + } + + @Test + public void testGetAdditionsForEntityWithNullPluralText() { + assertAdditions(null, Arrays. asList(), + FIND_ALL.getKey(), null); + } + + @Test + public void testGetAdditionsForFindAllMethodWhenServiceDoesNotProvideIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "", "x", "x", "x"); + assertAdditions("x", Arrays.asList(mockServiceInterface), + FIND_ALL.getKey(), null); + } + + @Test + public void testGetAdditionsForFindAllMethodWhenServiceProvidesIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "findPerson", "", "x", "x"); + assertAdditions("s", Arrays.asList(mockServiceInterface), + FIND_ALL.getKey(), "personService.findPersons()"); + } + + @Test + public void testGetAdditionsForFindEntriesMethodWhenServiceDoesNotProvideIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "x", "x", "x", ""); + assertAdditions("x", Arrays.asList(mockServiceInterface), + FIND_ENTRIES.getKey(), null, START_PARAMETER, SIZE_PARAMETER); + } + + @Test + public void testGetAdditionsForFindEntriesMethodWhenServiceProvidesIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "x", "x", "x", "locate"); + assertAdditions("z", Arrays.asList(mockServiceInterface), + FIND_ENTRIES.getKey(), + "personService.locatePersonEntries(start, count)", + START_PARAMETER, SIZE_PARAMETER); + } + + @Test + public void testGetAdditionsForSaveMethodWhenServiceDoesNotProvideIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "x", null, "x", "x"); + final MethodParameter methodParameter = new MethodParameter( + mockTargetType, "anything"); + assertAdditions("x", Arrays.asList(mockServiceInterface), + SAVE.getKey(), null, methodParameter); + } + + @Test + public void testGetAdditionsForSaveMethodWhenServiceProvidesIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "x", "save", "x", "x"); + final MethodParameter methodParameter = new MethodParameter( + mockTargetType, "user"); + assertAdditions("x", Arrays.asList(mockServiceInterface), + SAVE.getKey(), "personService.savePerson(user)", + methodParameter); + } + + @Test + public void testGetAdditionsForUpdateMethodWhenServiceDoesNotProvideIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "x", "x", "", "x"); + final MethodParameter methodParameter = new MethodParameter( + mockTargetType, "employee"); + assertAdditions("x", Arrays.asList(mockServiceInterface), + UPDATE.getKey(), null, methodParameter); + } + + @Test + public void testGetAdditionsForUpdateMethodWhenServiceProvidesIt() { + final ClassOrInterfaceTypeDetails mockServiceInterface = getMockService( + "x", "x", "change", "x"); + final MethodParameter methodParameter = new MethodParameter( + mockTargetType, "bob"); + assertAdditions("x", Arrays.asList(mockServiceInterface), + UPDATE.getKey(), "personService.changePerson(bob)", + methodParameter); + } + + @Test + public void testGetAdditionsWhenServiceAnnotationValuesUnavailable() { + final ClassOrInterfaceTypeDetails mockServiceInterface = mock(ClassOrInterfaceTypeDetails.class); + assertAdditions("anything", Arrays.asList(mockServiceInterface), + BOGUS_METHOD, null); + } +} diff --git a/addon-logging/pom.xml b/addon-logging/pom.xml new file mode 100644 index 000000000..5ab27c939 --- /dev/null +++ b/addon-logging/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.logging + bundle + Spring Roo - Addon - Logging + Support for logging configuration of the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-logging/src/main/java/org/springframework/roo/addon/logging/LogLevel.java b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LogLevel.java new file mode 100644 index 000000000..6827395fd --- /dev/null +++ b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LogLevel.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.logging; + +/** + * Provides information related to the log level configuration of the LOGGER. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public enum LogLevel { + DEBUG, ERROR, FATAL, INFO, TRACE, WARN; + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("logLevel " + name()); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggerPackage.java b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggerPackage.java new file mode 100644 index 000000000..626a44921 --- /dev/null +++ b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggerPackage.java @@ -0,0 +1,37 @@ +package org.springframework.roo.addon.logging; + +import java.util.Arrays; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides information related to the configuration of the LOGGER. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public enum LoggerPackage { + ALL_SPRING("org.springframework"), AOP("org.springframework.aop", + "org.springframework.aspects"), PERSISTENCE( + "org.springframework.orm"), PROJECT, ROOT, SECURITY( + "org.springframework.security"), TRANSACTIONS( + "org.springframework.transactions"), WEB("org.springframework.web"); + + private String[] packageNames; + + private LoggerPackage(final String... packageNames) { + this.packageNames = packageNames; + } + + public String[] getPackageNames() { + return packageNames; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("layer", name()); + builder.append("package names", Arrays.asList(packageNames)); + return builder.toString(); + } +} diff --git a/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingCommands.java b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingCommands.java new file mode 100644 index 000000000..add173c40 --- /dev/null +++ b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingCommands.java @@ -0,0 +1,49 @@ +package org.springframework.roo.addon.logging; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Commands for the 'logging' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class LoggingCommands implements CommandMarker { + + @Reference private LoggingOperations loggingOperations; + @Reference private StaticFieldConverter staticFieldConverter; + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(LoggerPackage.class); + staticFieldConverter.add(LogLevel.class); + } + + @CliCommand(value = "logging setup", help = "Configure logging in your project") + public void configureLogging( + @CliOption(key = { "", "level" }, mandatory = true, help = "The log level to configure") final LogLevel logLevel, + @CliOption(key = "package", mandatory = false, help = "The package to append the logging level to (all by default)") final LoggerPackage loggerPackage) { + + loggingOperations.configureLogging(logLevel, + loggerPackage == null ? LoggerPackage.ROOT : loggerPackage); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(LoggerPackage.class); + staticFieldConverter.remove(LogLevel.class); + } + + @CliAvailabilityIndicator("logging setup") + public boolean isConfigureLoggingAvailable() { + return loggingOperations.isLoggingInstallationPossible(); + } +} \ No newline at end of file diff --git a/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingOperations.java b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingOperations.java new file mode 100644 index 000000000..77c4abe3d --- /dev/null +++ b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingOperations.java @@ -0,0 +1,14 @@ +package org.springframework.roo.addon.logging; + +/** + * Provides logging configuration operations. + * + * @author Ben Alex + * @since 1.0 + */ +public interface LoggingOperations { + + void configureLogging(LogLevel logLevel, LoggerPackage loggerPackage); + + boolean isLoggingInstallationPossible(); +} \ No newline at end of file diff --git a/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingOperationsImpl.java b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingOperationsImpl.java new file mode 100644 index 000000000..b2c3d852c --- /dev/null +++ b/addon-logging/src/main/java/org/springframework/roo/addon/logging/LoggingOperationsImpl.java @@ -0,0 +1,114 @@ +package org.springframework.roo.addon.logging; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; + +/** + * Implementation of {@link LoggingOperations}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class LoggingOperationsImpl implements LoggingOperations { + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + + public void configureLogging(final LogLevel logLevel, + final LoggerPackage loggerPackage) { + Validate.notNull(logLevel, "LogLevel required"); + Validate.notNull(loggerPackage, "LoggerPackage required"); + + setupProperties(logLevel, loggerPackage); + } + + public boolean isLoggingInstallationPossible() { + return projectOperations.isFocusedProjectAvailable(); + } + + private void setupProperties(final LogLevel logLevel, + final LoggerPackage loggerPackage) { + final String filePath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, "log4j.properties"); + MutableFile log4jMutableFile = null; + final Properties props = new Properties(); + + InputStream inputStream = null; + try { + if (fileManager.exists(filePath)) { + log4jMutableFile = fileManager.updateFile(filePath); + inputStream = log4jMutableFile.getInputStream(); + props.load(inputStream); + } + else { + log4jMutableFile = fileManager.createFile(filePath); + inputStream = FileUtils.getInputStream(getClass(), + "log4j-template.properties"); + Validate.notNull(inputStream, + "Could not acquire log4j configuration template"); + props.load(inputStream); + } + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(inputStream); + } + + final JavaPackage topLevelPackage = projectOperations + .getTopLevelPackage(projectOperations.getFocusedModuleName()); + final String logStr = "log4j.logger."; + + switch (loggerPackage) { + case ROOT: + props.remove("log4j.rootLogger"); + props.setProperty("log4j.rootLogger", logLevel.name() + ", stdout"); + break; + case PROJECT: + props.remove(logStr + + topLevelPackage.getFullyQualifiedPackageName()); + props.setProperty( + logStr + topLevelPackage.getFullyQualifiedPackageName(), + logLevel.name()); + break; + default: + for (final String packageName : loggerPackage.getPackageNames()) { + props.remove(logStr + packageName); + props.setProperty(logStr + packageName, logLevel.name()); + } + break; + } + + OutputStream outputStream = null; + try { + outputStream = log4jMutableFile.getOutputStream(); + props.store(outputStream, "Updated at " + new Date()); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } +} diff --git a/addon-logging/src/main/resources/org/springframework/roo/addon/logging/log4j-template.properties b/addon-logging/src/main/resources/org/springframework/roo/addon/logging/log4j-template.properties new file mode 100644 index 000000000..e55f9290f --- /dev/null +++ b/addon-logging/src/main/resources/org/springframework/roo/addon/logging/log4j-template.properties @@ -0,0 +1,17 @@ +log4j.rootLogger=info, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +log4j.appender.R=org.apache.log4j.RollingFileAppender +log4j.appender.R.File=application.log + +log4j.appender.R.MaxFileSize=100KB +# Keep one backup file +log4j.appender.R.MaxBackupIndex=1 + +log4j.appender.R.layout=org.apache.log4j.PatternLayout +log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n diff --git a/addon-op4j/pom.xml b/addon-op4j/pom.xml new file mode 100644 index 000000000..06c40e868 --- /dev/null +++ b/addon-op4j/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.op4j + bundle + Spring Roo - Addon - Op4J + Support for the configuration and integration of OP4J (http://www.op4j.org/) functionalities through an AspectJ ITD. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.bootstrap + + + org.springframework.roo + org.springframework.roo.classpath + + + \ No newline at end of file diff --git a/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jCommands.java b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jCommands.java new file mode 100644 index 000000000..e884d9f9e --- /dev/null +++ b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jCommands.java @@ -0,0 +1,42 @@ +package org.springframework.roo.addon.op4j; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for addon-op4j. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class Op4jCommands implements CommandMarker { + + @Reference private Op4jOperations op4jOperations; + + @CliCommand(value = "op4j add", help = "Some helpful description") + public void add( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The java type to apply the RooOp4j annotation to") final JavaType target) { + + op4jOperations.annotateType(target); + } + + @CliAvailabilityIndicator({ "op4j setup", "Op4j add" }) + public boolean isOp4jAvailable() { + return op4jOperations.isOp4jInstallationPossible(); + } + + @CliCommand(value = "op4j setup", help = "Setup Op4j addon") + public void setup() { + op4jOperations.setup(); + } +} \ No newline at end of file diff --git a/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jMetadata.java b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jMetadata.java new file mode 100644 index 000000000..df2cd025d --- /dev/null +++ b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jMetadata.java @@ -0,0 +1,124 @@ +package org.springframework.roo.addon.op4j; + +import static org.springframework.roo.model.JavaType.OBJECT; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata to be triggered by {@link RooOp4j} annotation + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class Op4jMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + // fully-qualified? + private static final JavaType JAVA_RUN_TYPE_TYPES = new JavaType( + "org.javaruntype.type.Types"); + private static final JavaType KEYS = new JavaType("Keys"); // TODO should be + private static final JavaType OP4J_GET = new JavaType( + "org.op4j.functions.Get"); + private static final String PROVIDES_TYPE_STRING = Op4jMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public Op4jMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + + if (!isValid()) { + return; + } + + builder.addInnerType(getInnerType()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private ClassOrInterfaceTypeDetails getInnerType() { + final List fields = new ArrayList(); + + builder.getImportRegistrationResolver().addImports(OP4J_GET, + JAVA_RUN_TYPE_TYPES); + + final String targetName = super.destination.getSimpleTypeName(); + final String initializer = "Get.attrOf(Types.forClass(" + targetName + + ".class),\"" + targetName.toLowerCase() + "\")"; + final List parameters = Arrays.asList(OBJECT, destination); + final JavaType function = new JavaType("org.op4j.functions.Function", + 0, DataType.TYPE, null, parameters); + final int fieldModifier = Modifier.PUBLIC | Modifier.STATIC + | Modifier.FINAL; + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId(), fieldModifier, new JavaSymbolName( + targetName.toUpperCase()), function, initializer); + fields.add(fieldBuilder); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, KEYS, + PhysicalTypeCategory.CLASS); + cidBuilder.setDeclaredFields(fields); + return cidBuilder.build(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jMetadataProvider.java b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jMetadataProvider.java new file mode 100644 index 000000000..0e5c74079 --- /dev/null +++ b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jMetadataProvider.java @@ -0,0 +1,73 @@ +package org.springframework.roo.addon.op4j; + +import static org.springframework.roo.model.RooJavaType.ROO_OP4J; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Provides {@link Op4jMetadata}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class Op4jMetadataProvider extends AbstractItdMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_OP4J); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return Op4jMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_OP4J); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = Op4jMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = Op4jMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Op4j"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + return new Op4jMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata); + } + + public String getProvidesType() { + return Op4jMetadata.getMetadataIdentiferType(); + } +} \ No newline at end of file diff --git a/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jOperations.java b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jOperations.java new file mode 100644 index 000000000..5d3d0bc98 --- /dev/null +++ b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jOperations.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.op4j; + +import org.springframework.roo.model.JavaType; + +/** + * Interface of Op4j commands that are available via the Roo shell. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface Op4jOperations { + + void annotateType(JavaType type); + + boolean isOp4jInstallationPossible(); + + void setup(); +} \ No newline at end of file diff --git a/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jOperationsImpl.java b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jOperationsImpl.java new file mode 100644 index 000000000..114804ead --- /dev/null +++ b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/Op4jOperationsImpl.java @@ -0,0 +1,75 @@ +package org.springframework.roo.addon.op4j; + +import static org.springframework.roo.model.RooJavaType.ROO_OP4J; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Implementation of commands that are available via the Roo shell. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class Op4jOperationsImpl implements Op4jOperations { + + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void annotateType(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(javaType); + if (cid == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + javaType.getFullyQualifiedTypeName() + "'"); + } + + if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), + ROO_OP4J) == null) { + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + ROO_OP4J); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + cidBuilder.addAnnotation(annotationBuilder); + typeManagementService.createOrUpdateTypeOnDisk(cid); + } + } + + public boolean isOp4jInstallationPossible() { + return projectOperations.isFocusedProjectAvailable(); + } + + public void setup() { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List op4jDependencies = XmlUtils.findElements( + "/configuration/op4j/dependencies/dependency", configuration); + for (final Element dependencyElement : op4jDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + projectOperations.addDependencies( + projectOperations.getFocusedModuleName(), dependencies); + } +} \ No newline at end of file diff --git a/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/RooOp4j.java b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/RooOp4j.java new file mode 100644 index 000000000..1becda137 --- /dev/null +++ b/addon-op4j/src/main/java/org/springframework/roo/addon/op4j/RooOp4j.java @@ -0,0 +1,17 @@ +package org.springframework.roo.addon.op4j; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Trigger annotation of Op4J add-on + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooOp4j { +} diff --git a/addon-op4j/src/main/resources/org/springframework/roo/addon/op4j/configuration.xml b/addon-op4j/src/main/resources/org/springframework/roo/addon/op4j/configuration.xml new file mode 100644 index 000000000..f434410a0 --- /dev/null +++ b/addon-op4j/src/main/resources/org/springframework/roo/addon/op4j/configuration.xml @@ -0,0 +1,12 @@ + + + + + + org.op4j + op4j + 1.1 + + + + \ No newline at end of file diff --git a/addon-oscommands/pom.xml b/addon-oscommands/pom.xml new file mode 100644 index 000000000..fafc14cbf --- /dev/null +++ b/addon-oscommands/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.oscommands + bundle + Spring Roo - Addon - OS Commands + A simple add-on to allow execution of native OS commands from the Roo shell. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + diff --git a/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsCommands.java b/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsCommands.java new file mode 100644 index 000000000..1010f5258 --- /dev/null +++ b/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsCommands.java @@ -0,0 +1,51 @@ +package org.springframework.roo.addon.oscommands; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Command type to allow execution of native OS commands from the Spring Roo + * shell. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class OsCommands implements CommandMarker { + + private static final Logger LOGGER = HandlerUtils + .getLogger(OsCommands.class); + + @Reference private OsOperations osOperations; + + @CliCommand(value = "!", help = "Allows execution of operating system (OS) commands.") + public void command( + @CliOption(key = { "", "command" }, mandatory = false, specifiedDefaultValue = "", unspecifiedDefaultValue = "", help = "The command to execute") final String command) { + + if (StringUtils.isNotBlank(command)) { + try { + osOperations.executeCommand(command); + } + catch (final IOException e) { + LOGGER.severe("Unable to execute command " + command + " [" + + e.getMessage() + "]"); + } + } + } + + @CliAvailabilityIndicator("!") + public boolean isCommandAvailable() { + return true; // This command is always available! + } +} \ No newline at end of file diff --git a/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsOperations.java b/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsOperations.java new file mode 100644 index 000000000..366d80e73 --- /dev/null +++ b/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsOperations.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.oscommands; + +import java.io.IOException; + +/** + * Operations type to allow execution of native OS commands from the Spring Roo + * shell. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface OsOperations { + + /** + * Attempts the execution of a commands and delegates the output to the + * standard logger. + * + * @param command the command to execute + * @throws IOException if an error occurs + */ + void executeCommand(String command) throws IOException; +} \ No newline at end of file diff --git a/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsOperationsImpl.java b/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsOperationsImpl.java new file mode 100644 index 000000000..a6a82f20b --- /dev/null +++ b/addon-oscommands/src/main/java/org/springframework/roo/addon/oscommands/OsOperationsImpl.java @@ -0,0 +1,112 @@ +package org.springframework.roo.addon.oscommands; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.ActiveProcessManager; +import org.springframework.roo.process.manager.ProcessManager; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link OsOperations} interface. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class OsOperationsImpl implements OsOperations { + + private static class LoggingInputStream extends Thread { + private final ProcessManager processManager; + private final Reader reader; + + public LoggingInputStream(final InputStream inputStream, + final ProcessManager processManager) { + + reader = new InputStreamReader(inputStream); + this.processManager = processManager; + } + + @Override + public void run() { + ActiveProcessManager.setActiveProcessManager(processManager); + // Prevent thread name from being presented in Roo shell + Thread.currentThread().setName(""); + try { + for (String line : IOUtils.readLines(reader)) { + if (line.startsWith("[ERROR]")) { + LOGGER.severe(line); + } + else if (line.startsWith("[WARNING]")) { + LOGGER.warning(line); + } + else { + LOGGER.info(line); + } + } + } + catch (final IOException e) { + if (e.getMessage().contains("No such file or directory") + || e.getMessage().contains("CreateProcess error=2")) { + LOGGER.severe("Could not locate executable; please ensure command is in your path"); + } + } + finally { + IOUtils.closeQuietly(reader); + ActiveProcessManager.clearActiveProcessManager(); + } + } + } + + private static final Logger LOGGER = HandlerUtils + .getLogger(OsOperationsImpl.class); + + @Reference private PathResolver pathResolver; + @Reference private ProcessManager processManager; + + public void executeCommand(final String command) throws IOException { + final File root = new File(getProjectRoot()); + Validate.isTrue(root.isDirectory() && root.exists(), + "Project root does not currently exist as a directory ('%s')", + root.getCanonicalPath()); + + // Prevent thread name from being presented in Roo shell + Thread.currentThread().setName(""); + final Process p = Runtime.getRuntime().exec(command, null, root); + + // Ensure separate threads are used for logging, as per ROO-652 + final LoggingInputStream input = new LoggingInputStream( + p.getInputStream(), processManager); + final LoggingInputStream errors = new LoggingInputStream( + p.getErrorStream(), processManager); + + p.getOutputStream().close(); + input.start(); + errors.start(); + + try { + if (p.waitFor() != 0) { + LOGGER.warning("The command '" + command + + "' did not complete successfully"); + } + } + catch (final InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private String getProjectRoot() { + return pathResolver.getRoot(); + } +} \ No newline at end of file diff --git a/addon-plural/legal-addon-plural.txt b/addon-plural/legal-addon-plural.txt new file mode 100644 index 000000000..bafa2cbb8 --- /dev/null +++ b/addon-plural/legal-addon-plural.txt @@ -0,0 +1,20 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: Inflector +Software Web Site: https://inflector.dev.java.net/ +Effective License: Apache License, Version 2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.html + +Inflector is a runtime dependency of this module. It provides a +fallback pluralisation facility should the user not nominate a +specific plural via the relevant annotation. + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/addon-plural/pom.xml b/addon-plural/pom.xml new file mode 100644 index 000000000..f211d1a2c --- /dev/null +++ b/addon-plural/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.plural + bundle + Spring Roo - Addon - Plural Details + Determines the plural term for a given String literal using Inflector (https://inflector.dev.java.net/). + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.inflector + + + \ No newline at end of file diff --git a/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralAnnotationValues.java b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralAnnotationValues.java new file mode 100644 index 000000000..f6d065acb --- /dev/null +++ b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralAnnotationValues.java @@ -0,0 +1,37 @@ +package org.springframework.roo.addon.plural; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; + +/** + * The values of a {@link RooPlural} annotation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PluralAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String value = ""; + + /** + * Constructor that reads the {@link RooPlural} annotation (if any) on the + * given governor. + * + * @param governor the governor's metadata (required) + */ + public PluralAnnotationValues(final PhysicalTypeMetadata governor) { + super(governor, RooPlural.class); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Returns the plural provided by the annotation + * + * @return + */ + public String getValue() { + return value; + } +} diff --git a/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadata.java b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadata.java new file mode 100644 index 000000000..8e1b9a7dc --- /dev/null +++ b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadata.java @@ -0,0 +1,207 @@ +package org.springframework.roo.addon.plural; + +import static org.springframework.roo.model.RooJavaType.ROO_PLURAL; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.jvnet.inflector.Noun; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooPlural}. + *

    + * Note that although this class extends + * {@link AbstractItdTypeDetailsProvidingMetadataItem}, it never adds anything + * to the ITD builder, hence it never generates an ITD source file. + * + * @author Ben Alex + * @since 1.0 + */ +public class PluralMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = PluralMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + /** + * Creates a plural identifier for the given type in the given path. + * + * @param javaType the type for which to create the identifier (required) + * @param path the path containing the type (required) + * @return a valid plural metadata instance ID + */ + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private Map cache; + private String plural; + + public PluralMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final PluralAnnotationValues pluralAnnotation) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), "Metadata id '%s' is invalid", + identifier); + + if (!isValid()) { + return; + } + + plural = getPlural(pluralAnnotation); + } + + @Override + public boolean equals(final Object obj) { + // We override equals because we overrode hashCode, see that method + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof PluralMetadata)) { + return false; + } + final PluralMetadata other = (PluralMetadata) obj; + return StringUtils.equals(plural, other.getPlural()); + } + + /** + * This method returns the plural term as per inflector. ATTENTION: this + * method does NOT take @RooPlural into account. Use getPlural(..) instead! + * + * @param term The term to be pluralized + * @param locale Locale + * @return pluralized term + */ + public String getInflectorPlural(final String term, final Locale locale) { + try { + return Noun.pluralOf(term, locale); + } + catch (final RuntimeException re) { + // Inflector failed (see for example ROO-305), so don't pluralize it + return term; + } + } + + /** + * @return the plural of the type name + */ + public String getPlural() { + return plural; + } + + /** + * @param field the field to obtain plural details for (required) + * @return a guaranteed plural, computed via an annotation or Inflector + * (never returns null or an empty string) + */ + public String getPlural(final FieldMetadata field) { + Validate.notNull(field, "Field required"); + // Obtain the plural from the cache, if available + final String symbolName = field.getFieldName().getSymbolName(); + if (cache != null && cache.containsKey(symbolName)) { + return cache.get(symbolName); + } + + // We need to build the plural + String thePlural = ""; + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), ROO_PLURAL); + if (annotation != null) { + // Use the plural the user defined via the annotation + final AnnotationAttributeValue attribute = annotation + .getAttribute(new JavaSymbolName("value")); + if (attribute != null) { + thePlural = attribute.getValue().toString(); + } + } + if ("".equals(thePlural)) { + // Manually compute the plural, as the user did not provided one + thePlural = getInflectorPlural(symbolName, Locale.ENGLISH); + } + if (cache == null) { + // Create the cache (we defer this in case there is no field plural + // retrieval ever required for this instance) + cache = new HashMap(); + } + + // Populate the cache for next time + cache.put(symbolName, thePlural); + + return thePlural; + } + + private String getPlural(final PluralAnnotationValues pluralAnnotation) { + if (StringUtils.isNotBlank(pluralAnnotation.getValue())) { + return pluralAnnotation.getValue(); + } + return getInflectorPlural(destination.getSimpleTypeName(), + Locale.ENGLISH); + } + + @Override + public int hashCode() { + /* + * We override hashCode because the superclass' implementation compares + * the contents of the ITD builder, and this class never modifies that + * builder; meaning that all instances have the same hash code. + * ITD-generating metadata providers like this one rely on the hash code + * changing when the underlying metadata (in our case the plural) + * changes. + */ + return plural == null ? 0 : plural.hashCode(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("plural", getPlural()); + builder.append("cachedLookups", cache == null ? "[None]" : cache + .keySet().toString()); + return builder.toString(); + } +} diff --git a/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadataProvider.java b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadataProvider.java new file mode 100644 index 000000000..fe0186258 --- /dev/null +++ b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadataProvider.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.plural; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link PluralMetadata}. + * + * @author Ben Alex + * @since 1.1 + */ +public interface PluralMetadataProvider extends ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadataProviderImpl.java b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadataProviderImpl.java new file mode 100644 index 000000000..2773e4b57 --- /dev/null +++ b/addon-plural/src/main/java/org/springframework/roo/addon/plural/PluralMetadataProviderImpl.java @@ -0,0 +1,78 @@ +package org.springframework.roo.addon.plural; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Implementation of {@link PluralMetadataProvider}. + *

    + * It's odd that this class extends {@link AbstractItdMetadataProvider}, as it + * doesn't produce an ITD, it just provides a plural String via + * {@link PluralMetadata#getPlural()}. We should probably refactor it. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class PluralMetadataProviderImpl extends AbstractItdMetadataProvider + implements PluralMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + setIgnoreTriggerAnnotations(true); + setDependsOnGovernorBeingAClass(false); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return PluralMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = PluralMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = PluralMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Plural"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final PluralAnnotationValues pluralAnnotationValues = new PluralAnnotationValues( + governorPhysicalTypeMetadata); + return new PluralMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, pluralAnnotationValues); + } + + public String getProvidesType() { + return PluralMetadata.getMetadataIdentiferType(); + } +} diff --git a/addon-plural/src/main/java/org/springframework/roo/addon/plural/RooPlural.java b/addon-plural/src/main/java/org/springframework/roo/addon/plural/RooPlural.java new file mode 100644 index 000000000..d6e6ffb1e --- /dev/null +++ b/addon-plural/src/main/java/org/springframework/roo/addon/plural/RooPlural.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.plural; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides the plural of a particular type or field. + * + * @author Ben Alex + * @since 1.0 + */ +@Target({ ElementType.TYPE, ElementType.FIELD }) +@Retention(RetentionPolicy.SOURCE) +public @interface RooPlural { + + /** + * @return the plural name to use when working with this type or field + * (defaults to an empty string, which means to compute dynamically) + */ + String value() default ""; +} diff --git a/addon-plural/src/test/java/org/springframework/roo/addon/plural/PluralAnnotationValuesTest.java b/addon-plural/src/test/java/org/springframework/roo/addon/plural/PluralAnnotationValuesTest.java new file mode 100644 index 000000000..ab7a4840a --- /dev/null +++ b/addon-plural/src/test/java/org/springframework/roo/addon/plural/PluralAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.plural; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link PluralAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PluralAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooPlural.class; + } + + @Override + protected Class getValuesClass() { + return PluralAnnotationValues.class; + } +} diff --git a/addon-property-editor/pom.xml b/addon-property-editor/pom.xml new file mode 100644 index 000000000..97ff81381 --- /dev/null +++ b/addon-property-editor/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.property.editor + bundle + Spring Roo - Addon - Property Editor + Support for the creation of standard Java property editors. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.jpa + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorAnnotationValues.java b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorAnnotationValues.java new file mode 100644 index 000000000..bcc33c2c5 --- /dev/null +++ b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorAnnotationValues.java @@ -0,0 +1,39 @@ +package org.springframework.roo.addon.property.editor; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooEditor} annotation. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class EditorAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private JavaType providePropertyEditorFor; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public EditorAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_EDITOR); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Returns the {@link JavaType} to which the property editor applies + * + * @return null if not set + */ + public JavaType getEditedType() { + return providePropertyEditorFor; + } +} diff --git a/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorMetadata.java b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorMetadata.java new file mode 100644 index 000000000..00fc2cdf9 --- /dev/null +++ b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorMetadata.java @@ -0,0 +1,220 @@ +package org.springframework.roo.addon.property.editor; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooEditor}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class EditorMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = EditorMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public EditorMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final JavaType javaType, final JavaType idType, + final MethodMetadata identifierAccessorMethod, + final MethodMetadata findMethod) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(javaType, "Java type required"); + Validate.notNull(idType, "Identifier field metadata required"); + Validate.notNull(identifierAccessorMethod, + "Identifier accessor metadata required"); + + if (!isValid() || findMethod == null) { + valid = false; + return; + } + + // Only make the ITD cause PropertyEditorSupport to be subclasses if the + // governor doesn't already subclass it + final JavaType requiredSuperclass = JdkJavaType.PROPERTY_EDITOR_SUPPORT; + if (!governorTypeDetails.extendsType(requiredSuperclass)) { + builder.addImplementsType(requiredSuperclass); + } + + builder.addField(getField()); + builder.addMethod(getGetAsTextMethod(javaType, identifierAccessorMethod)); + builder.addMethod(getSetAsTextMethod(javaType, idType, findMethod)); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private FieldMetadataBuilder getField() { + final JavaSymbolName fieldName = new JavaSymbolName("typeConverter"); + + // Locate user-defined field + final FieldMetadata userField = governorTypeDetails.getField(fieldName); + final JavaType fieldType = SpringJavaType.SIMPLE_TYPE_CONVERTER; + if (userField != null) { + Validate.isTrue(userField.getFieldType().equals(fieldType), + "Field '%s' on '%s' must be of type '%s'", fieldName, + destination, fieldType.getNameIncludingTypeParameters()); + return new FieldMetadataBuilder(userField); + } + + return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, fieldName, + fieldType, "new " + fieldType + "()"); + } + + private MethodMetadataBuilder getGetAsTextMethod(final JavaType javaType, + final MethodMetadata identifierAccessorMethod) { + final JavaType returnType = JavaType.STRING; + final JavaSymbolName methodName = new JavaSymbolName("getAsText"); + final JavaType[] parameterTypes = {}; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterTypes); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return new MethodMetadataBuilder(userMethod); + } + + final List parameterNames = new ArrayList(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("Object obj = getValue();"); + bodyBuilder.appendFormalLine("if (obj == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return null;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("return (String) typeConverter.convertIfNecessary(((" + + javaType.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()) + + ") obj)." + + identifierAccessorMethod.getMethodName() + + "(), String.class);"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getSetAsTextMethod(final JavaType javaType, + final JavaType idType, final MethodMetadata findMethod) { + final JavaType parameterType = JavaType.STRING; + final List parameterNames = Arrays + .asList(new JavaSymbolName("text")); + + final JavaSymbolName methodName = new JavaSymbolName("setAsText"); + final JavaType returnType = JavaType.VOID_PRIMITIVE; + + // Locate user-defined method + final MethodMetadata userMethod = getGovernorMethod(methodName, + parameterType); + if (userMethod != null) { + Validate.isTrue(userMethod.getReturnType().equals(returnType), + "Method '%s' on '%s' must return '%s'", methodName, + destination, returnType.getNameIncludingTypeParameters()); + return new MethodMetadataBuilder(userMethod); + } + + final String identifierTypeName = idType + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("if (text == null || 0 == text.length()) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("setValue(null);"); + bodyBuilder.appendFormalLine("return;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.newLine(); + bodyBuilder.appendFormalLine(identifierTypeName + " identifier = (" + + identifierTypeName + + ") typeConverter.convertIfNecessary(text, " + + identifierTypeName + ".class);"); + bodyBuilder.appendFormalLine("if (identifier == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("setValue(null);"); + bodyBuilder.appendFormalLine("return;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.newLine(); + bodyBuilder.appendFormalLine("setValue(" + + javaType.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()) + "." + + findMethod.getMethodName() + "(identifier));"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorMetadataProvider.java b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorMetadataProvider.java new file mode 100644 index 000000000..6a553c990 --- /dev/null +++ b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/EditorMetadataProvider.java @@ -0,0 +1,127 @@ +package org.springframework.roo.addon.property.editor; + +import static org.springframework.roo.model.RooJavaType.ROO_EDITOR; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Provides {@link EditorMetadata}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class EditorMetadataProvider extends AbstractItdMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_EDITOR); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return EditorMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_EDITOR); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = EditorMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = EditorMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Editor"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // We know governor type details are non-null and can be safely cast + + // We need to parse the annotation, which we expect to be present + final EditorAnnotationValues annotationValues = new EditorAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound() + || annotationValues.getEditedType() == null) { + return null; + } + + // Lookup the form backing object's metadata + final JavaType javaType = annotationValues.getEditedType(); + if (!typeLocationService.isInProject(javaType)) { + return null; + } + + final LogicalPath path = EditorMetadata + .getPath(metadataIdentificationString); + final String jpaActiveRecordMetadataKey = JpaActiveRecordMetadata + .createIdentifier(javaType, path); + + // We need to lookup the metadata we depend on + final JpaActiveRecordMetadata jpaActiveRecordMetadata = (JpaActiveRecordMetadata) metadataService + .get(jpaActiveRecordMetadataKey); + if (jpaActiveRecordMetadata == null + || !jpaActiveRecordMetadata.isValid()) { + return null; + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + jpaActiveRecordMetadataKey, metadataIdentificationString); + + // We do not need to monitor the parent, as any changes to the java type + // associated with the parent will trickle down to + // the governing java type + final JavaType identifierType = persistenceMemberLocator + .getIdentifierType(javaType); + if (identifierType == null) { + return null; + } + + final MethodMetadata identifierAccessor = persistenceMemberLocator + .getIdentifierAccessor(javaType); + if (identifierAccessor == null) { + return null; + } + + final MethodMetadata findMethod = jpaActiveRecordMetadata + .getFindMethod(); + + return new EditorMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, javaType, identifierType, + identifierAccessor, findMethod); + } + + public String getProvidesType() { + return EditorMetadata.getMetadataIdentiferType(); + } +} diff --git a/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/RooEditor.java b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/RooEditor.java new file mode 100644 index 000000000..5a5402bd6 --- /dev/null +++ b/addon-property-editor/src/main/java/org/springframework/roo/addon/property/editor/RooEditor.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.property.editor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires ROO property editor support. + *

    + * This annotation will cause ROO to produce code that would typically appear in + * a property editor which, in turn is required by MVC controllers. Importantly, + * such code does NOT depend on any singletons and is intended to safely + * serialise. In the current release this code will be emitted to an ITD. + *

    + * There are two cases in which ROO will not emit one or more of the above + * artifacts: + *

      + *
    • The user provides the equivalent object himself. ROO will check for this + * by naming convention where it will look for a class name with the 'Editor' + * suffix in the same directory for types that require editors.
    • + *
    + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooEditor { + + /** + * Every editor is responsible for a single java object. The editor will be + * created for the object defined here. + */ + Class providePropertyEditorFor(); +} diff --git a/addon-property-editor/src/test/java/org/springframework/roo/addon/property/editor/EditorAnnotationValuesTest.java b/addon-property-editor/src/test/java/org/springframework/roo/addon/property/editor/EditorAnnotationValuesTest.java new file mode 100644 index 000000000..5c64b7985 --- /dev/null +++ b/addon-property-editor/src/test/java/org/springframework/roo/addon/property/editor/EditorAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.property.editor; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link EditorAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class EditorAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooEditor.class; + } + + @Override + protected Class getValuesClass() { + return EditorAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-propfiles/pom.xml b/addon-propfiles/pom.xml new file mode 100644 index 000000000..bb981f55a --- /dev/null +++ b/addon-propfiles/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.propfiles + bundle + Spring Roo - Addon - Property Files + Support for the management of properties files in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileCommands.java b/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileCommands.java new file mode 100644 index 000000000..dd5dd810a --- /dev/null +++ b/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileCommands.java @@ -0,0 +1,58 @@ +package org.springframework.roo.addon.propfiles; + +import java.util.SortedSet; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'propfile' add-on to be used by the ROO shell. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class PropFileCommands implements CommandMarker { + + @Reference private PropFileOperations propFileOperations; + + @CliCommand(value = "properties remove", help = "Removes a particular properties file property") + public void databaseRemove( + @CliOption(key = "name", mandatory = true, help = "Property file name (including .properties suffix)") final String name, + @CliOption(key = "path", mandatory = true, help = "Source path to property file") final LogicalPath path, + @CliOption(key = { "", "key" }, mandatory = true, help = "The property key that should be removed") final String key) { + + propFileOperations.removeProperty(path, name, key); + } + + @CliCommand(value = "properties set", help = "Changes a particular properties file property") + public void databaseSet( + @CliOption(key = "name", mandatory = true, help = "Property file name (including .properties suffix)") final String name, + @CliOption(key = "path", mandatory = true, help = "Source path to property file") final LogicalPath path, + @CliOption(key = "key", mandatory = true, help = "The property key that should be changed") final String key, + @CliOption(key = "value", mandatory = true, help = "The new vale for this property key") final String value) { + + propFileOperations.changeProperty(path, name, key, value); + } + + @CliAvailabilityIndicator({ "properties list", "properties set", + "properties remove" }) + public boolean isInstallWebFlowAvailable() { + return propFileOperations.isPropertiesCommandAvailable(); + } + + @CliCommand(value = "properties list", help = "Shows the details of a particular properties file") + public SortedSet propertyFileKeys( + @CliOption(key = "name", mandatory = true, help = "Property file name (including .properties suffix)") final String name, + @CliOption(key = "path", mandatory = true, help = "Source path to property file") final LogicalPath path) { + + return propFileOperations.getPropertyKeys(path, name, true); + } +} \ No newline at end of file diff --git a/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileOperations.java b/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileOperations.java new file mode 100644 index 000000000..36e299569 --- /dev/null +++ b/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileOperations.java @@ -0,0 +1,163 @@ +package org.springframework.roo.addon.propfiles; + +import java.io.InputStream; +import java.util.Map; +import java.util.Properties; +import java.util.SortedSet; + +import org.springframework.roo.project.LogicalPath; + +/** + * Provides an interface to {@link PropFileOperationsImpl}. + * + * @author Ben Alex + * @author Alan Stewart + * @author Stefan Schmidt + */ +public interface PropFileOperations { + + /** + * Adds the contents of the properties map to the given properties file. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param properties the map of properties to add + * @param sorted indicates if the resulting properties should be sorted + * alphabetically + * @param changeExisting indicates if an existing value for a given key + * should be replaced or not + */ + void addProperties(LogicalPath propertyFilePath, String propertyFilename, + Map properties, boolean sorted, + boolean changeExisting); + + /** + * Adds a property only if the given key (and value) does not exist already. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param key the property key to update (required) + * @param value the property value to set into the property key (required) + */ + void addPropertyIfNotExists(LogicalPath propertyFilePath, + String propertyFilename, String key, String value); + + /** + * Adds a property only if the given key (and value) does not exist already. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param key the property key to update (required) + * @param value the property value to set into the property key (required) + * @param sorted indicates if the resulting properties should be sorted + * alphabetically + */ + void addPropertyIfNotExists(LogicalPath propertyFilePath, + String propertyFilename, String key, String value, boolean sorted); + + /** + * Changes the specified property, throwing an exception if the file does + * not exist. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param key the property key to update (required) + * @param value the property value to set into the property key (required) + */ + void changeProperty(LogicalPath propertyFilePath, String propertyFilename, + String key, String value); + + /** + * Changes the specified property, throwing an exception if the file does + * not exist. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param key the property key to update (required) + * @param sorted indicates if the resulting properties should be sorted + * alphabetically + * @param value the property value to set into the property key (required) + */ + void changeProperty(LogicalPath propertyFilePath, String propertyFilename, + String key, String value, boolean sorted); + + /** + * Retrieves all property key/value pairs from the specified property, + * throwing an exception if the file does not exist. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @return the key/value pairs (may return null if the property file does + * not exist) + */ + Map getProperties(LogicalPath propertyFilePath, + String propertyFilename); + + /** + * Retrieves the specified property, returning null if the property or file + * does not exist. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param key the property key to retrieve (required) + * @return the property value (may return null if the property file or + * requested property does not exist) + */ + String getProperty(LogicalPath propertyFilePath, String propertyFilename, + String key); + + /** + * Retrieves all property keys from the specified property, throwing an + * exception if the file does not exist. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param includeValues if true, appends (" = theValue") to each returned + * string + * @return the keys (may return null if the property file does not exist) + */ + SortedSet getPropertyKeys(LogicalPath propertyFilePath, + String propertyFilename, boolean includeValues); + + boolean isPropertiesCommandAvailable(); + + /** + * Loads the properties from the given stream, closing it on completion + * + * @param inputStream the stream from which to read (can be + * null) + * @return an empty {@link Properties} if a null stream is given + */ + Properties loadProperties(InputStream inputStream); + + /** + * Loads the properties from the given classpath resource + * + * @param filename the name of the properties file to load + * @param loadingClass the class in whose package to look for the file + * @return a non-null properties + * @throws IllegalArgumentException if the given file can't be loaded + * @since 1.2.0 + */ + Properties loadProperties(String filename, Class loadingClass); + + /** + * Removes the specified property, throwing an exception if the file does + * not exist. + * + * @param propertyFilePath the location of the property file (required) + * @param propertyFilename the name of the property file within the + * specified path (required) + * @param key the property key to remove (required) + */ + void removeProperty(LogicalPath propertyFilePath, String propertyFilename, + String key); +} \ No newline at end of file diff --git a/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileOperationsImpl.java b/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileOperationsImpl.java new file mode 100644 index 000000000..37d943c5f --- /dev/null +++ b/addon-propfiles/src/main/java/org/springframework/roo/addon/propfiles/PropFileOperationsImpl.java @@ -0,0 +1,307 @@ +package org.springframework.roo.addon.propfiles; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; + +/** + * Provides property file configuration operations. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class PropFileOperationsImpl implements PropFileOperations { + + private static final boolean CHANGE_EXISTING = true; + private static final boolean SORTED = true; + + @Reference private FileManager fileManager; + @Reference private ProjectOperations projectOperations; + + public void addProperties(final LogicalPath propertyFilePath, + final String propertyFilename, + final Map properties, final boolean sorted, + final boolean changeExisting) { + manageProperty(propertyFilePath, propertyFilename, properties, sorted, + changeExisting); + } + + public void addPropertyIfNotExists(final LogicalPath propertyFilePath, + final String propertyFilename, final String key, final String value) { + manageProperty(propertyFilePath, propertyFilename, asMap(key, value), + !SORTED, !CHANGE_EXISTING); + } + + public void addPropertyIfNotExists(final LogicalPath propertyFilePath, + final String propertyFilename, final String key, + final String value, final boolean sorted) { + manageProperty(propertyFilePath, propertyFilename, asMap(key, value), + sorted, !CHANGE_EXISTING); + } + + private Map asMap(final String key, final String value) { + final Map properties = new HashMap(); + properties.put(key, value); + return properties; + } + + public void changeProperty(final LogicalPath propertyFilePath, + final String propertyFilename, final String key, final String value) { + manageProperty(propertyFilePath, propertyFilename, asMap(key, value), + !SORTED, CHANGE_EXISTING); + } + + public void changeProperty(final LogicalPath propertyFilePath, + final String propertyFilename, final String key, + final String value, final boolean sorted) { + manageProperty(propertyFilePath, propertyFilename, asMap(key, value), + sorted, CHANGE_EXISTING); + } + + public Map getProperties( + final LogicalPath propertyFilePath, final String propertyFilename) { + Validate.notNull(propertyFilePath, "Property file path required"); + Validate.notBlank(propertyFilename, "Property filename required"); + + final String filePath = projectOperations.getPathResolver() + .getIdentifier(propertyFilePath, propertyFilename); + final Properties props = new Properties(); + + try { + if (fileManager.exists(filePath)) { + loadProperties(props, new BufferedInputStream( + new FileInputStream(filePath))); + } + else { + throw new IllegalStateException("Properties file not found"); + } + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + + final Map result = new HashMap(); + for (final Object key : props.keySet()) { + result.put(key.toString(), props.getProperty(key.toString())); + } + return Collections.unmodifiableMap(result); + } + + public String getProperty(final LogicalPath propertyFilePath, + final String propertyFilename, final String key) { + Validate.notNull(propertyFilePath, "Property file path required"); + Validate.notBlank(propertyFilename, "Property filename required"); + Validate.notBlank(key, "Key required"); + + final String filePath = projectOperations.getPathResolver() + .getIdentifier(propertyFilePath, propertyFilename); + MutableFile mutableFile = null; + final Properties props = new Properties(); + + if (fileManager.exists(filePath)) { + mutableFile = fileManager.updateFile(filePath); + loadProperties(props, mutableFile.getInputStream()); + } + else { + return null; + } + + return props.getProperty(key); + } + + public SortedSet getPropertyKeys( + final LogicalPath propertyFilePath, final String propertyFilename, + final boolean includeValues) { + Validate.notNull(propertyFilePath, "Property file path required"); + Validate.notBlank(propertyFilename, "Property filename required"); + + final String filePath = projectOperations.getPathResolver() + .getIdentifier(propertyFilePath, propertyFilename); + final Properties props = new Properties(); + + try { + if (fileManager.exists(filePath)) { + loadProperties(props, new BufferedInputStream( + new FileInputStream(filePath))); + } + else { + throw new IllegalStateException("Properties file not found"); + } + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + + final SortedSet result = new TreeSet(); + for (final Object key : props.keySet()) { + String info = key.toString(); + if (includeValues) { + info += " = " + props.getProperty(key.toString()); + } + result.add(info); + } + return result; + } + + public boolean isPropertiesCommandAvailable() { + return projectOperations.isFocusedProjectAvailable(); + } + + public Properties loadProperties(final InputStream inputStream) { + final Properties properties = new Properties(); + if (inputStream != null) { + loadProperties(properties, inputStream); + } + return properties; + } + + private void loadProperties(final Properties props, + final InputStream inputStream) { + try { + props.load(inputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Could not load properties", e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + public Properties loadProperties(final String filename, + final Class loadingClass) { + return loadProperties(FileUtils.getInputStream(loadingClass, filename)); + } + + private void manageProperty(final LogicalPath propertyFilePath, + final String propertyFilename, + final Map properties, final boolean sorted, + final boolean changeExisting) { + Validate.notNull(propertyFilePath, "Property file path required"); + Validate.notBlank(propertyFilename, "Property filename required"); + Validate.notNull(properties, "Property map required"); + + final String filePath = projectOperations.getPathResolver() + .getIdentifier(propertyFilePath, propertyFilename); + MutableFile mutableFile = null; + + Properties props; + if (sorted) { + props = new Properties() { + private static final long serialVersionUID = 1L; + + // Override the keys() method to order the keys alphabetically + @SuppressWarnings("all") + public synchronized Enumeration keys() { + final Object[] keys = keySet().toArray(); + Arrays.sort(keys); + return new Enumeration() { + int i = 0; + + public boolean hasMoreElements() { + return i < keys.length; + } + + public Object nextElement() { + return keys[i++]; + } + }; + } + }; + } + else { + props = new Properties(); + } + + if (fileManager.exists(filePath)) { + mutableFile = fileManager.updateFile(filePath); + loadProperties(props, mutableFile.getInputStream()); + } + else { + // Unable to find the file, so let's create it + mutableFile = fileManager.createFile(filePath); + } + + boolean saveNeeded = false; + for (final Entry entry : properties.entrySet()) { + final String key = entry.getKey(); + final String newValue = entry.getValue(); + final String existingValue = props.getProperty(key); + if (existingValue == null || !existingValue.equals(newValue) + && changeExisting) { + props.setProperty(key, newValue); + saveNeeded = true; + } + } + + if (saveNeeded) { + storeProps(props, mutableFile.getOutputStream(), "Updated at " + + new Date()); + } + } + + public void removeProperty(final LogicalPath propertyFilePath, + final String propertyFilename, final String key) { + Validate.notNull(propertyFilePath, "Property file path required"); + Validate.notBlank(propertyFilename, "Property filename required"); + Validate.notBlank(key, "Key required"); + + final String filePath = projectOperations.getPathResolver() + .getIdentifier(propertyFilePath, propertyFilename); + MutableFile mutableFile = null; + final Properties props = new Properties(); + + if (fileManager.exists(filePath)) { + mutableFile = fileManager.updateFile(filePath); + loadProperties(props, mutableFile.getInputStream()); + } + else { + throw new IllegalStateException("Properties file not found"); + } + + props.remove(key); + + storeProps(props, mutableFile.getOutputStream(), "Updated at " + + new Date()); + } + + private void storeProps(final Properties props, + final OutputStream outputStream, final String comment) { + Validate.notNull(outputStream, "OutputStream required"); + try { + props.store(outputStream, comment); + } + catch (final IOException e) { + throw new IllegalStateException("Could not store properties", e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } +} diff --git a/addon-propfiles/src/test/java/org/springframework/roo/addon/propfiles/PropFileOperationsImplTest.java b/addon-propfiles/src/test/java/org/springframework/roo/addon/propfiles/PropFileOperationsImplTest.java new file mode 100644 index 000000000..c0e3abd6d --- /dev/null +++ b/addon-propfiles/src/test/java/org/springframework/roo/addon/propfiles/PropFileOperationsImplTest.java @@ -0,0 +1,44 @@ +package org.springframework.roo.addon.propfiles; + +import static org.junit.Assert.assertEquals; + +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.roo.addon.propfiles.caller.PropertiesTestClient; + +/** + * Unit test of {@link PropFileOperationsImpl} N.B. for this test to pass, the + * following folder must be on the classpath: + * org.springframework.roo.addon.propfiles/src/test/resources This + * is automatically the case when run by Maven, but not in Eclipse/STS, where + * the first time you run this test, you need to add the above folder explicitly + * via the "Run As -> Run Configurations..." dialog (in the "Classpath" tab, + * click "Advanced" and add the above path as a "folder"). + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PropFileOperationsImplTest { + + // Fixture + private PropFileOperationsImpl propFileOperations; + + @Before + public void setUp() { + propFileOperations = new PropFileOperationsImpl(); + } + + @Test + public void testLoadPropertiesFromClasspathWhenItExists() { + // Invoke + final Properties properties = propFileOperations.loadProperties( + "bike.properties", PropertiesTestClient.class); + + // Check + assertEquals("Fondriest", properties.getProperty("frame")); + assertEquals("Shimano Ultegra", properties.getProperty("groupset")); + assertEquals("Rolf Vector", properties.getProperty("wheels")); + } +} diff --git a/addon-propfiles/src/test/java/org/springframework/roo/addon/propfiles/caller/PropertiesTestClient.java b/addon-propfiles/src/test/java/org/springframework/roo/addon/propfiles/caller/PropertiesTestClient.java new file mode 100644 index 000000000..7e9942080 --- /dev/null +++ b/addon-propfiles/src/test/java/org/springframework/roo/addon/propfiles/caller/PropertiesTestClient.java @@ -0,0 +1,10 @@ +package org.springframework.roo.addon.propfiles.caller; + +/** + * A class for testing the loading of properties from the classpath. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PropertiesTestClient { +} \ No newline at end of file diff --git a/addon-propfiles/src/test/resources/org/springframework/roo/addon/propfiles/caller/bike.properties b/addon-propfiles/src/test/resources/org/springframework/roo/addon/propfiles/caller/bike.properties new file mode 100644 index 000000000..69c59cb9f --- /dev/null +++ b/addon-propfiles/src/test/resources/org/springframework/roo/addon/propfiles/caller/bike.properties @@ -0,0 +1,3 @@ +frame=Fondriest +groupset=Shimano Ultegra +wheels=Rolf Vector \ No newline at end of file diff --git a/addon-roobot-client/pom.xml b/addon-roobot-client/pom.xml new file mode 100644 index 000000000..2c10b38d1 --- /dev/null +++ b/addon-roobot-client/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.roobot.client + bundle + Spring Roo - Addon - RooBot Client + Support for add-on management through the RooBot server. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.json-simple + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.bootstrap + + + org.springframework.roo + org.springframework.roo.shell.osgi + + + org.springframework.roo + org.springframework.roo.felix + + + org.springframework.roo + org.springframework.roo.uaa + + + \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnBundleSymbolicName.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnBundleSymbolicName.java new file mode 100644 index 000000000..30602a0a2 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnBundleSymbolicName.java @@ -0,0 +1,54 @@ +package org.springframework.roo.addon.roobot.client; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Display Addon symbolic name for command completion. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class AddOnBundleSymbolicName implements + Comparable { + + /** + * You can change this field name, but ensure getKey() returns a unique + * value + */ + private final String key; + + public AddOnBundleSymbolicName(final String key) { + Validate.notBlank(key, "bundle symbolic name required"); + this.key = key; + } + + public final int compareTo(final AddOnBundleSymbolicName o) { + if (o == null) { + return -1; + } + return key.compareTo(o.key); + } + + @Override + public final boolean equals(final Object obj) { + return obj instanceof AddOnBundleSymbolicName + && compareTo((AddOnBundleSymbolicName) obj) == 0; + } + + public String getKey() { + return key; + } + + @Override + public final int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("key", key); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnBundleSymbolicNameConverter.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnBundleSymbolicNameConverter.java new file mode 100644 index 000000000..5e0bac5b1 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnBundleSymbolicNameConverter.java @@ -0,0 +1,57 @@ +package org.springframework.roo.addon.roobot.client; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.roobot.client.model.Bundle; +import org.springframework.roo.addon.roobot.client.model.BundleVersion; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link AddOnBundleSymbolicName}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class AddOnBundleSymbolicNameConverter implements + Converter { + + @Reference private AddOnRooBotOperations addonManagerOperations; + + public AddOnBundleSymbolicName convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new AddOnBundleSymbolicName(value.trim()); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String originalUserInput, + final String optionContext, final MethodTarget target) { + final Map bundles = addonManagerOperations + .getAddOnCache(false); + for (final Entry entry : bundles.entrySet()) { + final String bsn = entry.getKey(); + final Bundle bundle = entry.getValue(); + if (bundle.getVersions().size() > 1) { + for (final BundleVersion bundleVersion : bundle.getVersions()) { + completions.add(new Completion(bsn + ";" + + bundleVersion.getVersion())); + } + } + completions.add(new Completion(bsn)); + } + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return AddOnBundleSymbolicName.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnCommands.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnCommands.java new file mode 100644 index 000000000..627b9b9dd --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnCommands.java @@ -0,0 +1,142 @@ +package org.springframework.roo.addon.roobot.client; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.roobot.client.model.Rating; +import org.springframework.roo.felix.BundleSymbolicName; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Commands for this add-on. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class AddOnCommands implements CommandMarker { + + @Reference private AddOnFeedbackOperations addOnfeedbackOperations; + @Reference private AddOnRooBotOperations addOnRooBotOperations; + @Reference private StaticFieldConverter staticFieldConverter; + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(Rating.class); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(Rating.class); + } + + @CliCommand(value = "addon feedback bundle", help = "Provide anonymous ratings and comments on a Spring Roo Add-on (your feedback will be published publicly)") + public void feedbackBundle( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The bundle symbolic name for the add-on of interest") final BundleSymbolicName bsn, + @CliOption(key = "rating", mandatory = true, help = "How much did you like this add-on?") final Rating rating, + @CliOption(key = "comment", mandatory = false, help = "Your comments on this add-on eg \"this is my comment!\"; limit of 140 characters") final String comment) { + + addOnfeedbackOperations.feedbackBundle(bsn, rating, comment); + } + + @CliCommand(value = "addon info bundle", help = "Provide information about a specific Spring Roo Add-on") + public void infoBundle( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The bundle symbolic name for the add-on of interest") final AddOnBundleSymbolicName bsn) { + + addOnRooBotOperations.addOnInfo(bsn); + } + + @CliCommand(value = "addon info id", help = "Provide information about a specific Spring Roo Add-on") + public void infoId( + @CliOption(key = { "", "searchResultId" }, mandatory = true, help = "The bundle ID as presented via the addon list or addon search command") final String bundleId) { + + addOnRooBotOperations.addOnInfo(bundleId); + } + + @CliCommand(value = "addon install bundle", help = "Install Spring Roo Add-on") + public void installBsn( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The bundle symbolic name for the add-on of interest") final AddOnBundleSymbolicName bsn) { + addOnRooBotOperations.installAddOn(bsn); + } + + @CliCommand(value = "addon install id", help = "Install Spring Roo Add-on") + public void installId( + @CliOption(key = { "", "searchResultId" }, mandatory = true, help = "The bundle ID as presented via the addon list or addon search command") final String bundleId) { + + addOnRooBotOperations.installAddOn(bundleId); + } + + @CliCommand(value = "addon list", help = "List all known Spring Roo Add-ons (up to the maximum number displayed on a single page)") + public void list( + @CliOption(key = "refresh", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Refresh the add-on index from the Internet") final boolean refresh, + @CliOption(key = "linesPerResult", mandatory = false, unspecifiedDefaultValue = "2", specifiedDefaultValue = "2", help = "The maximum number of lines displayed per add-on") final int linesPerResult, + @CliOption(key = "maxResults", mandatory = false, unspecifiedDefaultValue = "99", specifiedDefaultValue = "99", help = "The maximum number of add-ons to list") final int maxResults, + @CliOption(key = "trustedOnly", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Only display trusted add-ons in search results") final boolean trustedOnly, + @CliOption(key = "communityOnly", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Only display community provided add-ons in search results") final boolean communityOnly, + @CliOption(key = "compatibleOnly", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Only display compatible add-ons in search results") final boolean compatibleOnly) { + + // A list is really just a search without criteria. We keep some + // criteria to allow reasonable filtering and display logic to take + // place. + addOnRooBotOperations.searchAddOns(true, null, refresh, linesPerResult, + maxResults, trustedOnly, compatibleOnly, communityOnly, null); + } + + @CliCommand(value = "addon remove", help = "Remove Spring Roo Add-on") + public void remove( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The bundle symbolic name for the add-on of interest") final BundleSymbolicName bsn) { + + addOnRooBotOperations.removeAddOn(bsn); + } + + @CliCommand(value = "addon search", help = "Search all known Spring Roo Add-ons") + public void search( + @CliOption(key = { "", "requiresDescription" }, mandatory = false, specifiedDefaultValue = "*", unspecifiedDefaultValue = "*", help = "A comma separated list of search terms") final String searchTerms, + @CliOption(key = "refresh", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Refresh the add-on index from the Internet") final boolean refresh, + @CliOption(key = "linesPerResult", mandatory = false, unspecifiedDefaultValue = "2", specifiedDefaultValue = "2", help = "The maximum number of lines displayed per add-on") final int linesPerResult, + @CliOption(key = "maxResults", mandatory = false, unspecifiedDefaultValue = "20", specifiedDefaultValue = "20", help = "The maximum number of add-ons to list") final int maxResults, + @CliOption(key = "trustedOnly", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Only display trusted add-ons in search results") final boolean trustedOnly, + @CliOption(key = "compatibleOnly", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Only display compatible add-ons in search results") final boolean compatibleOnly, + @CliOption(key = "communityOnly", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Only display community provided add-ons in search results") final boolean communityOnly, + @CliOption(key = "requiresCommand", mandatory = false, help = "Only display add-ons in search results that offer this command") final String requiresCommand) { + + addOnRooBotOperations.searchAddOns(true, searchTerms, refresh, + linesPerResult, maxResults, trustedOnly, compatibleOnly, + communityOnly, requiresCommand); + } + + @CliCommand(value = "addon upgrade all", help = "Upgrade all relevant Spring Roo Add-ons / Components for the current stability level") + public void ugradeAll() { + addOnRooBotOperations.upgradeAddOns(); + } + + @CliCommand(value = "addon upgrade available", help = "List available Spring Roo Add-on / Component upgrades") + public void ugradeAvailable( + @CliOption(key = "addonStabilityLevel", mandatory = false, help = "The stability level of add-ons or components which are presented for upgrading (default: ANY)") final AddOnStabilityLevel level) { + + addOnRooBotOperations.upgradesAvailable(level); + } + + @CliCommand(value = "addon upgrade bundle", help = "Upgrade a specific Spring Roo Add-on / Component") + public void ugradeBundle( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The bundle symbolic name for the add-on to upgrade") final AddOnBundleSymbolicName bsn) { + + addOnRooBotOperations.upgradeAddOn(bsn); + } + + @CliCommand(value = "addon upgrade id", help = "Upgrade a specific Spring Roo Add-on / Component from a search result ID") + public void ugradeId( + @CliOption(key = { "", "searchResultId" }, mandatory = true, help = "The bundle ID as presented via the addon list or addon search command") final String bundleId) { + + addOnRooBotOperations.upgradeAddOn(bundleId); + } + + @CliCommand(value = "addon upgrade settings", help = "Settings for Add-on upgrade operations") + public void ugradeSettings( + @CliOption(key = "addonStabilityLevel", mandatory = false, help = "The stability level of add-ons or components which are presented for upgrading") final AddOnStabilityLevel level) { + addOnRooBotOperations.upgradeSettings(level); + } +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnFeedbackOperations.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnFeedbackOperations.java new file mode 100644 index 000000000..a830579a4 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnFeedbackOperations.java @@ -0,0 +1,25 @@ +package org.springframework.roo.addon.roobot.client; + +import org.springframework.roo.addon.roobot.client.model.Rating; +import org.springframework.roo.felix.BundleSymbolicName; + +/** + * Provides the operations support for add-on feedback. + *

    + * We need this in a separate interface to avoid a circular dependency between + * the UAA support mechanism and {@link AddOnFeedbackOperations}. + * + * @author Ben Alex + * @since 1.1.1 + */ +public interface AddOnFeedbackOperations { + + /** + * Provide feedback on the {@link BundleSymbolicName}. + * + * @param bsn the bundle symbolic name (required) + * @param rating the rating given (required) + * @param comment the comment (can be null if no comment was offered) + */ + void feedbackBundle(BundleSymbolicName bsn, Rating rating, String comment); +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnFeedbackOperationsImpl.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnFeedbackOperationsImpl.java new file mode 100644 index 000000000..d7ea6eb78 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnFeedbackOperationsImpl.java @@ -0,0 +1,105 @@ +package org.springframework.roo.addon.roobot.client; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.json.simple.JSONObject; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.roobot.client.model.Rating; +import org.springframework.roo.felix.BundleSymbolicName; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.osgi.BundleFindingUtils; +import org.springframework.roo.uaa.UaaRegistrationService; +import org.springframework.roo.url.stream.UrlInputStreamService; + +/** + * Implementation of {@link AddOnFeedbackOperations}. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class AddOnFeedbackOperationsImpl implements AddOnFeedbackOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(AddOnFeedbackOperationsImpl.class); + + @Reference private UaaRegistrationService registrationService; + @Reference private UrlInputStreamService urlInputStreamService; + + private BundleContext bundleContext; + + protected void activate(final ComponentContext context) { + bundleContext = context.getBundleContext(); + } + + protected void deactivate(final ComponentContext context) { + bundleContext = null; + } + + @SuppressWarnings("unchecked") + public void feedbackBundle(final BundleSymbolicName bsn, + final Rating rating, String comment) { + Validate.notNull(bsn, "Bundle symbolic name required"); + Validate.notNull(rating, "Rating required"); + Validate.isTrue(comment == null || comment.length() <= 140, + "Comment must be under 140 characters"); + if ("".equals(comment)) { + comment = null; + } + + // Figure out the HTTP URL we'll get "GET"ing in order to submit the + // user's feedback + URL httpUrl; + try { + httpUrl = new URL(UaaRegistrationService.EMPTY_FILE_URL); + } + catch (final MalformedURLException shouldNeverHappen) { + throw new IllegalStateException(shouldNeverHappen); + } + + // Fail early if we're not allowed GET this URL due to UAA restrictions + final String failureMessage = urlInputStreamService + .getUrlCannotBeOpenedMessage(httpUrl); + if (failureMessage != null) { + LOGGER.warning(failureMessage); + return; + } + + // To get this far, there is no reason we shouldn't be able to store + // this user's feedback + final JSONObject o = new JSONObject(); + o.put("version", UaaRegistrationService.SPRING_ROO.getMajorVersion() + + "." + UaaRegistrationService.SPRING_ROO.getMajorVersion() + + "." + UaaRegistrationService.SPRING_ROO.getPatchVersion()); + o.put("type", "bundle_feedback"); + + // A BSN shouldn't need escaping, but anyway... + o.put("bsn", JSONObject.escape(bsn.getKey())); + o.put("rating", rating.getKey()); + o.put("comment", comment == null ? "" : JSONObject.escape(comment)); + final String customJson = o.toJSONString(); + + // Register the feedback. Note we record all feedback against the BSN + // for the RooBot client add-on to assist simple server-side detection. + // We do NOT record feedback against the BSN that is receiving the + // feedback (as the BSN receiving the feedback is stored inside the + // custom JSON). + registrationService.registerBundleSymbolicNameUse(BundleFindingUtils + .findFirstBundleForTypeName(bundleContext, + AddOnRooBotOperations.class.getName()), customJson); + + // Push the feedback up to the server now if possible + registrationService.requestTransmission(); + + LOGGER.info("Thanks for sharing your feedback."); + } +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnIndexPublicFeatureResolver.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnIndexPublicFeatureResolver.java new file mode 100644 index 000000000..f4ff1c762 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnIndexPublicFeatureResolver.java @@ -0,0 +1,35 @@ +package org.springframework.roo.addon.roobot.client; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.uaa.PublicFeatureResolver; + +/** + * Resolves public features by reference to the RooBot add-on index. + *

    + * Any item in the RooBot add-on index is considered a public feature. In + * addition, any item starting with "org.springframework.roo" is considered a + * public feature. All other items are considered private features. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Component +@Service +public class AddOnIndexPublicFeatureResolver implements PublicFeatureResolver { + + @Reference AddOnRooBotOperations rooBotOperations; + + public boolean isPublic(final String bundleSymbolicNameOrTypeName) { + if (bundleSymbolicNameOrTypeName.startsWith("org.springframework.roo")) { + return true; + } + for (final String bsn : rooBotOperations.getAddOnCache(false).keySet()) { + if (bundleSymbolicNameOrTypeName.startsWith(bsn)) { + return true; + } + } + return false; + } +} diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnRooBotOperations.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnRooBotOperations.java new file mode 100644 index 000000000..a22411c48 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnRooBotOperations.java @@ -0,0 +1,132 @@ +package org.springframework.roo.addon.roobot.client; + +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.springframework.roo.addon.roobot.client.model.Bundle; +import org.springframework.roo.felix.BundleSymbolicName; +import org.springframework.roo.support.api.AddOnSearch; + +/** + * Interface for operations offered by this addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface AddOnRooBotOperations extends AddOnSearch { + + enum InstallOrUpgradeStatus { + FAILED, INVALID_OBR_URL, PGP_VERIFICATION_NEEDED, SHELL_RESTART_NEEDED, SUCCESS; + } + + /** + * Display information for a given ({@link AddOnBundleSymbolicName}. + * Information is piped to standard JDK {@link Logger#info} + * + * @param bsn the bundle symbolic name (required) + */ + void addOnInfo(AddOnBundleSymbolicName bsn); + + /** + * Display information for a given bundle ID. Information is piped to + * standard JDK {@link Logger#info} + * + * @param bundleId the bundle ID (required) + */ + void addOnInfo(String bundleId); + + /** + * Find all add-ons presently known to this Roo instance, including add-ons + * which have not been downloaded or installed by the user. + *

    + * Information is optionally emitted to the console via {@link Logger#info}. + * + * @param showFeedback if false will never output any messages to the + * console (required) + * @param searchTerms comma separated list of search terms (required) + * @param refresh attempt a fresh download of roobot.xml (optional) + * @param linesPerResult maximum number of lines per add-on (optional) + * @param maxResults maximum number of results to display (optional) + * @param trustedOnly display only trusted add-ons in search results + * (optional) + * @param compatibleOnly display only compatible add-ons in search results + * (optional) + * @param communityOnly display only community-provided add-ons in search + * results (optional) + * @param requiresCommand display only add-ons which offer the specified + * command (optional) + * @return the total number of matches found, even if only some of these are + * displayed due to maxResults (or null if the add-on list is + * unavailable for some reason, eg network problems etc) + * @since 1.2.0 + */ + List findAddons(boolean showFeedback, String searchTerms, + boolean refresh, int linesPerResult, int maxResults, + boolean trustedOnly, boolean compatibleOnly, boolean communityOnly, + String requiresCommand); + + /** + * Get a list of all cached addon bundles. + * + * @param refresh refresh attempt a fresh download of roobot.xml (optional) + * @return a set of addon bundles + */ + Map getAddOnCache(boolean refresh); + + /** + * Install addon with given {@link AddOnBundleSymbolicName}. + * + * @param bsn the bundle symbolic name (required) + */ + InstallOrUpgradeStatus installAddOn(AddOnBundleSymbolicName bsn); + + /** + * Install addon with given Add-On ID. + * + * @param bundleId the bundle id (required) + */ + InstallOrUpgradeStatus installAddOn(String bundleId); + + /** + * Remove addon with given {@link BundleSymbolicName}. + * + * @param bsn the bundle symbolic name (required) + */ + InstallOrUpgradeStatus removeAddOn(BundleSymbolicName bsn); + + /** + * Upgrade specific add-on only. + * + * @param bsn the bundle symbolic name (required) + */ + InstallOrUpgradeStatus upgradeAddOn(AddOnBundleSymbolicName bsn); + + /** + * Upgrade specific add-on only. + * + * @param bundleId the bundle id (required) + */ + InstallOrUpgradeStatus upgradeAddOn(String bundleId); + + /** + * Upgrade all add-ons according to the user defined add-on stability level. + */ + void upgradeAddOns(); + + /** + * Display information about the available upgrades + * + * @param addonStabilityLevel the add-on stability level taken into account + * for the upgrade + */ + void upgradesAvailable(AddOnStabilityLevel addonStabilityLevel); + + /** + * Define the stability level for add-on upgrades + * + * @param addOnStabilityLevel the stability level for add-on upgrades + * (required) + */ + void upgradeSettings(AddOnStabilityLevel addOnStabilityLevel); +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnRooBotOperationsImpl.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnRooBotOperationsImpl.java new file mode 100644 index 000000000..106ed9755 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnRooBotOperationsImpl.java @@ -0,0 +1,1078 @@ +package org.springframework.roo.addon.roobot.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Logger; +import java.util.zip.ZipInputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.roobot.client.model.Bundle; +import org.springframework.roo.addon.roobot.client.model.BundleVersion; +import org.springframework.roo.addon.roobot.client.model.Comment; +import org.springframework.roo.addon.roobot.client.model.Rating; +import org.springframework.roo.classpath.preferences.Preferences; +import org.springframework.roo.classpath.preferences.PreferencesService; +import org.springframework.roo.felix.BundleSymbolicName; +import org.springframework.roo.felix.pgp.PgpKeyId; +import org.springframework.roo.felix.pgp.PgpService; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.springframework.roo.uaa.UaaRegistrationService; +import org.springframework.roo.url.stream.UrlInputStreamService; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of commands that are available via the Roo shell. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class AddOnRooBotOperationsImpl implements AddOnRooBotOperations { + + public static final String ADDON_UPGRADE_STABILITY_LEVEL = "ADDON_UPGRADE_STABILITY_LEVEL"; + private static final Logger LOGGER = HandlerUtils + .getLogger(AddOnRooBotOperationsImpl.class); + private static final List NO_UPGRADE_BSN_LIST = Arrays.asList( + "org.springframework.uaa.client", + "org.springframework.roo.url.stream.jdk", + "org.springframework.roo.url.stream", + "org.springframework.roo.file.monitor", + "org.springframework.roo.file.monitor.polling", + "org.springframework.roo.file.monitor.polling.roo", + "org.springframework.roo.bootstrap", + "org.springframework.roo.classpath", + "org.springframework.roo.classpath.javaparser", + "org.springframework.roo.deployment.support", + "org.springframework.roo.felix", + "org.springframework.roo.file.undo", + "org.springframework.roo.metadata", + "org.springframework.roo.model", + "org.springframework.roo.osgi.bundle", + "org.springframework.roo.osgi.roo.bundle", + "org.springframework.roo.process.manager", + "org.springframework.roo.project", "org.springframework.roo.root", + "org.springframework.roo.shell", + "org.springframework.roo.shell.jline", + "org.springframework.roo.shell.jline.osgi", + "org.springframework.roo.shell.osgi", + "org.springframework.roo.startlevel", + "org.springframework.roo.support", + "org.springframework.roo.support.osgi", + "org.springframework.roo.uaa"); + + @Reference private PgpService pgpService; + @Reference private PreferencesService preferencesService; + @Reference private Shell shell; + @Reference private UrlInputStreamService urlInputStreamService; + + private Map bundleCache; + private ComponentContext context; + private final DateFormat dateFormat = new SimpleDateFormat( + "yyyy-MM-dd hh:mm:ss"); + private final Object mutex = new Object(); + private Preferences prefs; + private volatile Thread rooBotEagerDownload; + private boolean rooBotIndexDownload = true; + private String rooBotXmlUrl = "http://spring-roo-repository.springsource.org/roobot/roobot.xml.zip"; + private Map searchResultCache; + + protected void activate(final ComponentContext context) { + this.context = context; + prefs = preferencesService + .getPreferencesFor(AddOnRooBotOperationsImpl.class); + bundleCache = new HashMap(); + searchResultCache = new HashMap(); + final BundleContext bundleContext = context.getBundleContext(); + if (bundleContext != null) { + final String roobot = bundleContext.getProperty("roobot.url"); + if (roobot != null && roobot.length() > 0) { + rooBotXmlUrl = roobot; + } + rooBotIndexDownload = Boolean.valueOf(bundleContext + .getProperty("roobot.index.dowload")); + } + if (rooBotIndexDownload) { + rooBotEagerDownload = new Thread(new Runnable() { + public void run() { + synchronized (mutex) { + populateBundleCache(true); + } + } + }, "Spring Roo RooBot Add-In Index Eager Download"); + rooBotEagerDownload.start(); + } + } + + public void addOnInfo(final AddOnBundleSymbolicName bsn) { + Validate.notNull(bsn, "A valid add-on bundle symbolic name is required"); + synchronized (mutex) { + String bsnString = bsn.getKey(); + if (bsnString.contains(";")) { + bsnString = bsnString.split(";")[0]; + } + final Bundle bundle = bundleCache.get(bsnString); + if (bundle == null) { + LOGGER.warning("Unable to find specified bundle with symbolic name: " + + bsn.getKey()); + return; + } + addOnInfo(bundle, bundle.getBundleVersion(bsn.getKey())); + } + } + + private void addOnInfo(final Bundle bundle, + final BundleVersion bundleVersion) { + final StringBuilder sb = new StringBuilder(bundleVersion.getVersion()); + if (bundle.getVersions().size() > 1) { + sb.append(" [available versions: "); + for (final BundleVersion version : BundleVersion + .orderByVersion(new ArrayList(bundle + .getVersions()))) { + sb.append(version.getVersion()).append(", "); + } + sb.delete(sb.length() - 2, sb.length()).append("]"); + } + logInfo("Name", bundleVersion.getPresentationName()); + logInfo("BSN", bundle.getSymbolicName()); + logInfo("Version", sb.toString()); + logInfo("Roo Version", bundleVersion.getRooVersion()); + logInfo("Ranking", Float.toString(bundle.getRanking())); + logInfo("JAR Size", bundleVersion.getSize() + " bytes"); + logInfo("PGP Signature", bundleVersion.getPgpKey() + " signed by " + + bundleVersion.getPgpDescriptions()); + logInfo("OBR URL", bundleVersion.getObrUrl()); + logInfo("JAR URL", bundleVersion.getUri()); + for (final Entry entry : bundleVersion.getCommands() + .entrySet()) { + logInfo("Commands", "'" + entry.getKey() + "' [" + entry.getValue() + + "]"); + } + logInfo("Description", bundleVersion.getDescription()); + int cc = 0; + for (final Comment comment : bundle.getComments()) { + logInfo("Comment " + ++cc, + "Rating [" + + comment.getRating().name() + + "], grade [" + + DateFormat.getDateInstance(DateFormat.SHORT) + .format(comment.getDate()) + "], Comment [" + + comment.getComment() + "]"); + } + } + + public void addOnInfo(final String bundleKey) { + Validate.notBlank(bundleKey, "A valid bundle ID is required"); + synchronized (mutex) { + Bundle bundle = null; + if (searchResultCache != null) { + bundle = searchResultCache.get(String.format("%02d", + Integer.parseInt(bundleKey))); + } + if (bundle == null) { + LOGGER.warning("A valid bundle ID is required"); + return; + } + addOnInfo(bundle, bundle.getBundleVersion(bundleKey)); + } + } + + private AddOnStabilityLevel checkAddOnStabilityLevel( + AddOnStabilityLevel addOnStabilityLevel) { + if (addOnStabilityLevel == null) { + addOnStabilityLevel = AddOnStabilityLevel.fromLevel(prefs.getInt( + ADDON_UPGRADE_STABILITY_LEVEL, /* default */ + AddOnStabilityLevel.RELEASE.getLevel())); + } + return addOnStabilityLevel; + } + + private int countBundles() { + final BundleContext bc = context.getBundleContext(); + if (bc != null) { + final org.osgi.framework.Bundle[] bundles = bc.getBundles(); + if (bundles != null) { + return bundles.length; + } + } + return 0; + } + + protected void deactivate(final ComponentContext context) { + if (rooBotEagerDownload != null && rooBotEagerDownload.isAlive()) { + rooBotEagerDownload = null; + } + } + + private List filterList(final List bundles, + final boolean trustedOnly, final boolean compatibleOnly, + final boolean communityOnly, final String requiresCommand, + final boolean onlyRelevantBundles) { + final List filteredList = new ArrayList(); + List keys = null; + if (trustedOnly) { + keys = pgpService.getTrustedKeys(); + } + bundle_loop: for (final Bundle bundle : bundles) { + final BundleVersion latest = bundle.getLatestVersion(); + if (onlyRelevantBundles && !(bundle.getSearchRelevance() > 0)) { + continue bundle_loop; + } + if (trustedOnly && !isTrustedKey(keys, latest.getPgpKey())) { + continue bundle_loop; + } + if (communityOnly + && latest + .getObrUrl() + .equals("http://spring-roo-repository.springsource.org/repository.xml")) { + continue bundle_loop; + } + if (compatibleOnly && !isCompatible(latest.getRooVersion())) { + continue bundle_loop; + } + if (isBundleInstalled(bundle)) { + continue bundle_loop; + } + if (requiresCommand != null && requiresCommand.length() > 0) { + boolean matchingCommand = false; + for (final String cmd : latest.getCommands().keySet()) { + if (cmd.startsWith(requiresCommand) + || requiresCommand.startsWith(cmd)) { + matchingCommand = true; + break; + } + } + if (!matchingCommand) { + continue bundle_loop; + } + } + filteredList.add(bundle); + } + return filteredList; + } + + public List findAddons(final boolean showFeedback, + final String searchTerms, boolean refresh, + final int linesPerResult, int maxResults, + final boolean trustedOnly, final boolean compatibleOnly, + final boolean communityOnly, final String requiresCommand) { + synchronized (mutex) { + if (maxResults > 99) { + maxResults = 99; + } + if (maxResults < 1) { + maxResults = 10; + } + if (bundleCache.isEmpty()) { + // We should refresh regardless in this case + refresh = true; + } + if (refresh && populateBundleCache(false)) { + if (showFeedback) { + LOGGER.info("Successfully downloaded Roo add-on Data"); + } + } + if (bundleCache.size() != 0) { + boolean onlyRelevantBundles = false; + if (searchTerms != null && !"".equals(searchTerms)) { + onlyRelevantBundles = true; + final String[] terms = searchTerms.split(","); + for (final Bundle bundle : bundleCache.values()) { + // First set relevance of all bundles to zero + bundle.setSearchRelevance(0f); + int hits = 0; + final BundleVersion latest = bundle.getLatestVersion(); + for (final String term : terms) { + if ((bundle.getSymbolicName() + ";" + latest + .getSummary()).toLowerCase().contains( + term.trim().toLowerCase()) + || term.equals("*")) { + hits++; + } + } + bundle.setSearchRelevance(hits / terms.length); + } + } + final List bundles = Bundle + .orderBySearchRelevance(new ArrayList( + bundleCache.values())); + final List filteredSearchResults = filterList(bundles, + trustedOnly, compatibleOnly, communityOnly, + requiresCommand, onlyRelevantBundles); + if (showFeedback) { + printResultList(filteredSearchResults, maxResults, + linesPerResult); + } + return filteredSearchResults; + } + + // There is a problem with the add-on index + if (showFeedback) { + LOGGER.info("No add-ons known. Are you online? Try the 'download status' command"); + } + + return null; + } + } + + public Map getAddOnCache(final boolean refresh) { + synchronized (mutex) { + if (refresh) { + populateBundleCache(false); + } + return Collections.unmodifiableMap(bundleCache); + } + } + + private Map getUpgradableBundles( + final AddOnStabilityLevel asl) { + final Map bundles = new HashMap(); + if (context == null) { + return bundles; + } + final BundleContext bundleContext = context.getBundleContext(); + for (final org.osgi.framework.Bundle bundle : bundleContext + .getBundles()) { + final Bundle b = bundleCache.get(bundle.getSymbolicName()); + if (b == null) { + continue; + } + final BundleVersion bundleVersion = b.getLatestVersion(); + final String rooBotBundleVersion = bundleVersion.getVersion(); + final Object ebv = bundle.getHeaders().get("Bundle-Version"); + if (ebv == null) { + continue; + } + final String exisingBundleVersion = ebv.toString().trim(); + if (isCompatible(b.getLatestVersion().getRooVersion()) + && rooBotBundleVersion + .compareToIgnoreCase(exisingBundleVersion) > 0 + && asl.getLevel() > AddOnStabilityLevel + .getAddOnStabilityLevel(exisingBundleVersion)) { + + bundles.put(b.getSymbolicName() + ";" + exisingBundleVersion, b); + } + } + return bundles; + } + + private String getVersionForCompatibility() { + return UaaRegistrationService.SPRING_ROO.getMajorVersion() + "." + + UaaRegistrationService.SPRING_ROO.getMinorVersion(); + } + + private InstallOrUpgradeStatus installAddon( + final BundleVersion bundleVersion, final String bsn) { + final InstallOrUpgradeStatus status = installOrUpgradeAddOn( + bundleVersion, bsn, true); + switch (status) { + case SUCCESS: + LOGGER.info("Successfully installed add-on: " + + bundleVersion.getPresentationName() + " [version: " + + bundleVersion.getVersion() + "]"); + LOGGER.warning("[Hint] Please consider rating this add-on with the following command:"); + LOGGER.warning("[Hint] addon feedback bundle --bundleSymbolicName " + + bsn.substring( + 0, + bsn.indexOf(";") != -1 ? bsn.indexOf(";") : bsn + .length()) + + " --rating ... --comment \"...\""); + break; + case SHELL_RESTART_NEEDED: + LOGGER.warning("You have upgraded a Roo core addon. To complete this installation please restart the Roo shell."); + break; + case PGP_VERIFICATION_NEEDED: + LOGGER.warning("PGP verification of the bundle required"); + break; + default: + LOGGER.warning("Unable to install add-on: " + + bundleVersion.getPresentationName() + " [version: " + + bundleVersion.getVersion() + "]"); + break; + } + + return status; + } + + public InstallOrUpgradeStatus installAddOn(final AddOnBundleSymbolicName bsn) { + synchronized (mutex) { + Validate.notNull(bsn, + "A valid add-on bundle symbolic name is required"); + String bsnString = bsn.getKey(); + if (bsnString.contains(";")) { + bsnString = bsnString.split(";")[0]; + } + final Bundle bundle = bundleCache.get(bsnString); + if (bundle == null) { + LOGGER.warning("Could not find specified bundle with symbolic name: " + + bsn.getKey()); + return InstallOrUpgradeStatus.FAILED; + } + return installAddon(bundle.getBundleVersion(bsn.getKey()), + bsn.getKey()); + } + } + + public InstallOrUpgradeStatus installAddOn(final String bundleKey) { + synchronized (mutex) { + Validate.notBlank(bundleKey, "A valid bundle ID is required"); + Bundle bundle = null; + if (searchResultCache != null) { + bundle = searchResultCache.get(String.format("%02d", + Integer.parseInt(bundleKey))); + } + if (bundle == null) { + LOGGER.warning("To install an addon a valid bundle ID is required"); + return InstallOrUpgradeStatus.FAILED; + } + return installAddon(bundle.getBundleVersion(bundleKey), + bundle.getSymbolicName()); + } + } + + private InstallOrUpgradeStatus installOrUpgradeAddOn( + final BundleVersion bundleVersion, final String bsn, + final boolean install) { + if (!verifyRepository(bundleVersion.getObrUrl())) { + return InstallOrUpgradeStatus.INVALID_OBR_URL; + } + + final int count = countBundles(); + final boolean requiresWrappedCoreDependency = bundleVersion + .getDescription().contains("#wrappedCoreDependency"); + + boolean success = !(requiresWrappedCoreDependency && !shell + .executeCommand("osgi obr url add --url http://spring-roo-repository.springsource.org/repository.xml")); + success &= shell.executeCommand("osgi obr url add --url " + + bundleVersion.getObrUrl()); + success &= shell.executeCommand("osgi obr deploy --bundleSymbolicName " + + bsn); + success &= shell.executeCommand("osgi obr url remove --url " + + bundleVersion.getObrUrl()); + success &= !(requiresWrappedCoreDependency && !shell + .executeCommand("osgi obr url remove --url http://spring-roo-repository.springsource.org/repository.xml")); + + if (install && count == countBundles()) { + // Most likely PgP verification required before the bundle can be + // installed, no log needed + return InstallOrUpgradeStatus.PGP_VERIFICATION_NEEDED; + } + return success ? InstallOrUpgradeStatus.SUCCESS + : InstallOrUpgradeStatus.FAILED; + } + + private boolean isBundleInstalled(final Bundle search) { + final BundleContext bundleContext = context.getBundleContext(); + for (final org.osgi.framework.Bundle bundle : bundleContext + .getBundles()) { + final String bsn = (String) bundle.getHeaders().get( + "Bundle-SymbolicName"); + if (StringUtils.isNotBlank(bsn) + && bsn.equals(search.getSymbolicName())) { + return true; + } + } + return false; + } + + private boolean isCompatible(final String version) { + return version.equals(getVersionForCompatibility()); + } + + @SuppressWarnings("unchecked") + private boolean isTrustedKey(final List keys, + final String keyId) { + for (final PGPPublicKeyRing keyRing : keys) { + final Iterator it = keyRing.getPublicKeys(); + while (it.hasNext()) { + final PGPPublicKey pgpKey = it.next(); + if (new PgpKeyId(pgpKey).equals(new PgpKeyId(keyId))) { + return true; + } + } + } + return false; + } + + public void listAddOns(boolean refresh, final int linesPerResult, + final int maxResults, final boolean trustedOnly, + final boolean compatibleOnly, final boolean communityOnly, + final String requiresCommand) { + synchronized (mutex) { + if (bundleCache.isEmpty()) { + // We should refresh regardless in this case + refresh = true; + } + if (refresh && populateBundleCache(false)) { + LOGGER.info("Successfully downloaded Roo add-on Data"); + } + if (bundleCache.size() != 0) { + final List bundles = Bundle + .orderByRanking(new ArrayList(bundleCache + .values())); + final List filteredList = filterList(bundles, + trustedOnly, compatibleOnly, communityOnly, + requiresCommand, false); + printResultList(filteredList, maxResults, linesPerResult); + } + else { + LOGGER.info("No add-ons known. Are you online? Try the 'download status' command"); + } + } + } + + private void logInfo(final String label, String content) { + final StringBuilder sb = new StringBuilder(); + sb.append(label); + for (int i = 0; i < 13 - label.length(); i++) { + sb.append("."); + } + sb.append(": "); + if (content.length() < 65) { + sb.append(content); + LOGGER.info(sb.toString()); + } + else { + final List split = new ArrayList( + Arrays.asList(content.split("\\s"))); + if (split.size() == 1) { + while (content.length() > 65) { + sb.append(content.substring(0, 65)); + content = content.substring(65); + LOGGER.info(sb.toString()); + sb.setLength(0); + sb.append(" "); + } + if (content.length() > 0) { + LOGGER.info(sb.append(content).toString()); + } + } + else { + while (split.size() > 0) { + while (!split.isEmpty() + && split.get(0).length() + sb.length() < 79) { + sb.append(split.get(0)).append(" "); + split.remove(0); + } + LOGGER.info(sb.toString().substring(0, + sb.toString().length() - 1)); + sb.setLength(0); + sb.append(" "); + } + } + } + } + + private boolean populateBundleCache(final boolean startupTime) { + boolean success = false; + InputStream is = null; + ByteArrayInputStream bais = null; + ByteArrayOutputStream baos = null; + try { + final DocumentBuilderFactory dbf = DocumentBuilderFactory + .newInstance(); + final DocumentBuilder db = dbf.newDocumentBuilder(); + if (rooBotXmlUrl.startsWith("http://")) { + // Handle it as HTTP + final URL httpUrl = new URL(rooBotXmlUrl); + final String failureMessage = urlInputStreamService + .getUrlCannotBeOpenedMessage(httpUrl); + if (failureMessage != null) { + if (!startupTime) { + // This wasn't just an eager startup time attempt, so + // let's display the error reason + // (for startup time, we just fail quietly) + LOGGER.warning(failureMessage); + } + return false; + } + // It appears we can acquire the URL, so let's do it + is = urlInputStreamService.openConnection(httpUrl); + } + else { + // Fall back to normal protocol handler (likely in local + // development testing etc) + is = new URL(rooBotXmlUrl).openStream(); + } + if (is == null) { + LOGGER.warning("Could not connect to Roo Addon bundle repository index"); + return false; + } + + final ZipInputStream zip = new ZipInputStream(is); + zip.getNextEntry(); + + baos = new ByteArrayOutputStream(); + IOUtils.copy(zip, baos); + + bais = new ByteArrayInputStream(baos.toByteArray()); + final Document roobotXml = db.parse(bais); + if (roobotXml != null) { + populateBundleCache(roobotXml); + success = true; + } + zip.close(); + } + catch (final Throwable ignored) { + } + finally { + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(baos); + IOUtils.closeQuietly(bais); + } + if (success && startupTime) { + printAddonStats(); + } + return success; + } + + private void populateBundleCache(final Document roobotXml) + throws ParseException { + bundleCache.clear(); + for (final Element bundleElement : XmlUtils.findElements( + "/roobot/bundles/bundle", roobotXml.getDocumentElement())) { + final String bsn = bundleElement.getAttribute("bsn"); + if (NO_UPGRADE_BSN_LIST.contains(bsn)) { + // List only add-ons which are not core (see ROO-2190) + continue; + } + final List comments = new ArrayList(); + for (final Element commentElement : XmlUtils.findElements( + "comments/comment", bundleElement)) { + comments.add(new Comment(Rating.fromInt(new Integer( + commentElement.getAttribute("rating"))), commentElement + .getAttribute("comment"), dateFormat + .parse(commentElement.getAttribute("date")))); + } + final Bundle bundle = new Bundle(bundleElement.getAttribute("bsn"), + new Float(bundleElement.getAttribute("uaa-ranking")), + comments); + for (final Element versionElement : XmlUtils.findElements( + "versions/version", bundleElement)) { + if (bsn != null && bsn.length() > 0 && versionElement != null) { + String signedBy = ""; + final String pgpKey = versionElement + .getAttribute("pgp-key-id"); + if (pgpKey != null && pgpKey.length() > 0) { + final Element pgpSigned = XmlUtils.findFirstElement( + "/roobot/pgp-keys/pgp-key[@id='" + pgpKey + + "']/pgp-key-description", + roobotXml.getDocumentElement()); + if (pgpSigned != null) { + signedBy = pgpSigned.getAttribute("text"); + } + } + + final Map commands = new HashMap(); + for (final Element shell : XmlUtils.findElements( + "shell-commands/shell-command", versionElement)) { + commands.put(shell.getAttribute("command"), + shell.getAttribute("help")); + } + + final StringBuilder versionBuilder = new StringBuilder(); + versionBuilder.append(versionElement.getAttribute("major")) + .append(".") + .append(versionElement.getAttribute("minor")); + final String versionMicro = versionElement + .getAttribute("micro"); + if (versionMicro != null && versionMicro.length() > 0) { + versionBuilder.append(".").append(versionMicro); + } + final String versionQualifier = versionElement + .getAttribute("qualifier"); + if (versionQualifier != null + && versionQualifier.length() > 0) { + versionBuilder.append(".").append(versionQualifier); + } + + String rooVersion = versionElement + .getAttribute("roo-version"); + if (rooVersion.equals("*") || rooVersion.length() == 0) { + rooVersion = getVersionForCompatibility(); + } + else { + final String[] split = rooVersion.split("\\."); + if (split.length > 2) { + // Only interested in major.minor + rooVersion = split[0] + "." + split[1]; + } + } + final BundleVersion version = new BundleVersion( + versionElement.getAttribute("url"), + versionElement.getAttribute("obr-url"), + versionBuilder.toString(), + versionElement.getAttribute("name"), + new Long(versionElement.getAttribute("size")) + .longValue(), + versionElement.getAttribute("description"), pgpKey, + signedBy, rooVersion, commands); + // For security reasons we ONLY accept httppgp:// + // add-on versions + if (!version.getUri().startsWith("httppgp://")) { + continue; + } + bundle.addVersion(version); + } + bundleCache.put(bsn, bundle); + } + } + } + + private void printAddonStats() { + String msg = null; + final AddOnStabilityLevel currentLevel = AddOnStabilityLevel + .fromLevel(prefs.getInt(ADDON_UPGRADE_STABILITY_LEVEL, + AddOnStabilityLevel.RELEASE.getLevel())); + final Map currentLevelBundles = getUpgradableBundles(currentLevel); + if (currentLevelBundles.size() > 0) { + msg = currentLevelBundles.size() + " upgrade" + + (currentLevelBundles.size() > 1 ? "s" : "") + + " available"; + } + final Map anyLevelBundles = getUpgradableBundles(AddOnStabilityLevel.ANY); + if (anyLevelBundles.size() != 0) { + if (msg == null) { + msg = "0 upgrades available"; + } + final int plusSize = anyLevelBundles.size() + - currentLevelBundles.size(); + msg += " (plus " + plusSize + " upgrade" + + (plusSize > 1 ? "s" : "") + + " not visible due to your version stability setting of " + + currentLevel.name() + ")"; + } + if (msg != null) { + Thread.currentThread().setName(""); // Prevent thread name from + // being presented in Roo shell + LOGGER.info(msg); + } + } + + private void printResultList(final Collection bundles, + int maxResults, final int linesPerResult) { + int bundleId = 1; + searchResultCache.clear(); + final StringBuilder sb = new StringBuilder(); + final List keys = pgpService.getTrustedKeys(); + LOGGER.info(bundles.size() + + " found, sorted by rank; T = trusted developer; R = Roo " + + getVersionForCompatibility() + " compatible"); + LOGGER.warning("ID T R DESCRIPTION -------------------------------------------------------------"); + for (final Bundle bundle : bundles) { + if (maxResults-- == 0) { + break; + } + final BundleVersion latest = bundle.getLatestVersion(); + final String bundleKey = String.format("%02d", bundleId++); + searchResultCache.put(bundleKey, bundle); + sb.append(bundleKey); + sb.append(isTrustedKey(keys, latest.getPgpKey()) ? " Y " : " - "); + sb.append(isCompatible(latest.getRooVersion()) ? "Y " : "- "); + sb.append(latest.getVersion()); + sb.append(" "); + final List split = new ArrayList( + Arrays.asList(latest.getDescription().split("\\s"))); + int lpr = linesPerResult; + while (split.size() > 0 && --lpr >= 0) { + while (!split.isEmpty() + && split.get(0).length() + sb.length() < (lpr == 0 ? 77 + : 80)) { + sb.append(split.get(0)).append(" "); + split.remove(0); + } + String line = sb.toString().substring(0, + sb.toString().length() - 1); + if (lpr == 0 && split.size() > 0) { + line += "..."; + } + LOGGER.info(line); + sb.setLength(0); + sb.append(" "); + } + if (sb.toString().trim().length() > 0) { + LOGGER.info(sb.toString()); + } + sb.setLength(0); + } + printSeparator(); + LOGGER.info("[HINT] use 'addon info id --searchResultId ..' to see details about a search result"); + LOGGER.info("[HINT] use 'addon install id --searchResultId ..' to install a specific search result, or"); + LOGGER.info("[HINT] use 'addon install bundle --bundleSymbolicName TAB' to install a specific add-on version"); + } + + private void printSeparator() { + LOGGER.warning("--------------------------------------------------------------------------------"); + } + + public InstallOrUpgradeStatus removeAddOn(final BundleSymbolicName bsn) { + synchronized (mutex) { + Validate.notNull(bsn, "Bundle symbolic name required"); + boolean success = false; + final int count = countBundles(); + success = shell + .executeCommand("osgi uninstall --bundleSymbolicName " + + bsn.getKey()); + InstallOrUpgradeStatus status; + if (count == countBundles() || !success) { + LOGGER.warning("Unable to remove add-on: " + bsn.getKey()); + status = InstallOrUpgradeStatus.FAILED; + } + else { + LOGGER.info("Successfully removed add-on: " + bsn.getKey()); + status = InstallOrUpgradeStatus.SUCCESS; + } + return status; + } + } + + public Integer searchAddOns(final boolean showFeedback, + final String searchTerms, final boolean refresh, + final int linesPerResult, final int maxResults, + final boolean trustedOnly, final boolean compatibleOnly, + final boolean communityOnly, final String requiresCommand) { + final List result = findAddons(showFeedback, searchTerms, + refresh, linesPerResult, maxResults, trustedOnly, + compatibleOnly, communityOnly, requiresCommand); + return result != null ? result.size() : null; + } + + public InstallOrUpgradeStatus upgradeAddOn(final AddOnBundleSymbolicName bsn) { + synchronized (mutex) { + Validate.notNull(bsn, + "A valid add-on bundle symbolic name is required"); + String bsnString = bsn.getKey(); + if (bsnString.contains(";")) { + bsnString = bsnString.split(";")[0]; + } + final Bundle bundle = bundleCache.get(bsnString); + if (bundle == null) { + LOGGER.warning("Could not find specified bundle with symbolic name: " + + bsn.getKey()); + return InstallOrUpgradeStatus.FAILED; + } + final BundleVersion bundleVersion = bundle.getBundleVersion(bsn + .getKey()); + final InstallOrUpgradeStatus status = installOrUpgradeAddOn( + bundleVersion, bsn.getKey(), false); + if (status.equals(InstallOrUpgradeStatus.SUCCESS)) { + LOGGER.info("Successfully upgraded: " + + bundle.getSymbolicName() + " [version: " + + bundleVersion.getVersion() + "]"); + LOGGER.warning("Please restart the Roo shell to complete the upgrade"); + } + else if (status.equals(InstallOrUpgradeStatus.FAILED)) { + LOGGER.warning("Unable to upgrade: " + bundle.getSymbolicName() + + " [version: " + bundleVersion.getVersion() + "]"); + } + return status; + } + } + + public InstallOrUpgradeStatus upgradeAddOn(final String bundleId) { + synchronized (mutex) { + Validate.notBlank(bundleId, "A valid bundle ID is required"); + Bundle bundle = null; + if (searchResultCache != null) { + bundle = searchResultCache.get(String.format("%02d", + Integer.parseInt(bundleId))); + } + if (bundle == null) { + LOGGER.warning("A valid bundle ID is required"); + return InstallOrUpgradeStatus.FAILED; + } + final BundleVersion bundleVersion = bundle + .getBundleVersion(bundleId); + final InstallOrUpgradeStatus status = installOrUpgradeAddOn( + bundleVersion, bundle.getSymbolicName(), false); + if (status.equals(InstallOrUpgradeStatus.SUCCESS)) { + LOGGER.info("Successfully upgraded: " + + bundle.getSymbolicName() + " [version: " + + bundleVersion.getVersion() + "]"); + LOGGER.warning("Please restart the Roo shell to complete the upgrade"); + } + else if (status.equals(InstallOrUpgradeStatus.FAILED)) { + LOGGER.warning("Unable to upgrade: " + bundle.getSymbolicName() + + " [version: " + bundleVersion.getVersion() + "]"); + } + return status; + } + } + + public void upgradeAddOns() { + synchronized (mutex) { + final AddOnStabilityLevel addonStabilityLevel = checkAddOnStabilityLevel(null); + final Map bundles = getUpgradableBundles(addonStabilityLevel); + boolean upgraded = false; + for (final Bundle bundle : bundles.values()) { + final BundleVersion bundleVersion = bundle.getLatestVersion(); + final InstallOrUpgradeStatus status = installOrUpgradeAddOn( + bundleVersion, bundle.getSymbolicName(), false); + if (status.equals(InstallOrUpgradeStatus.SUCCESS)) { + LOGGER.info("Successfully upgraded: " + + bundle.getSymbolicName() + " [version: " + + bundleVersion.getVersion() + "]"); + upgraded = true; + } + else if (status.equals(InstallOrUpgradeStatus.FAILED)) { + LOGGER.warning("Unable to upgrade: " + + bundle.getSymbolicName() + " [version: " + + bundleVersion.getVersion() + "]"); + } + } + if (upgraded) { + LOGGER.warning("Please restart the Roo shell to complete the upgrade"); + } + else { + LOGGER.info("No add-ons / components are available for upgrade for level: " + + addonStabilityLevel.name()); + } + } + } + + public void upgradesAvailable(AddOnStabilityLevel addonStabilityLevel) { + synchronized (mutex) { + addonStabilityLevel = checkAddOnStabilityLevel(addonStabilityLevel); + final Map bundles = getUpgradableBundles(addonStabilityLevel); + if (bundles.isEmpty()) { + LOGGER.info("No add-ons / components are available for upgrade for level: " + + addonStabilityLevel.name()); + } + else { + LOGGER.info("The following add-ons / components are available for upgrade for level: " + + addonStabilityLevel.name()); + printSeparator(); + for (final Entry entry : bundles.entrySet()) { + final BundleVersion latest = entry.getValue() + .getLatestVersion(); + if (latest != null) { + LOGGER.info("[level: " + + AddOnStabilityLevel.fromLevel( + AddOnStabilityLevel + .getAddOnStabilityLevel(latest + .getVersion())).name() + + "] " + entry.getKey() + " > " + + latest.getVersion()); + } + } + printSeparator(); + } + } + } + + public void upgradeSettings(AddOnStabilityLevel addOnStabilityLevel) { + if (addOnStabilityLevel == null) { + addOnStabilityLevel = checkAddOnStabilityLevel(null); + LOGGER.info("Current Add-on Stability Level: " + + addOnStabilityLevel.name()); + } + else { + boolean success = true; + prefs.putInt(ADDON_UPGRADE_STABILITY_LEVEL, + addOnStabilityLevel.getLevel()); + try { + prefs.flush(); + } + catch (final IllegalStateException ignore) { + success = false; + } + if (success) { + LOGGER.info("Add-on Stability Level: " + + addOnStabilityLevel.name() + " stored"); + } + else { + LOGGER.warning("Unable to store add-on stability level at this time"); + } + } + } + + private boolean verifyRepository(final String repoUrl) { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + Document doc = null; + try { + URL obrUrl = null; + obrUrl = new URL(repoUrl); + final DocumentBuilder db = dbf.newDocumentBuilder(); + if (obrUrl.toExternalForm().endsWith(".zip")) { + ByteArrayInputStream bais = null; + ByteArrayOutputStream baos = null; + ZipInputStream zip = null; + try { + zip = new ZipInputStream(obrUrl.openStream()); + zip.getNextEntry(); + + baos = new ByteArrayOutputStream(); + final byte[] buffer = new byte[8192]; + int length = -1; + while (zip.available() > 0) { + length = zip.read(buffer, 0, 8192); + if (length > 0) { + baos.write(buffer, 0, length); + } + } + bais = new ByteArrayInputStream(baos.toByteArray()); + doc = db.parse(bais); + } + finally { + IOUtils.closeQuietly(zip); + IOUtils.closeQuietly(bais); + IOUtils.closeQuietly(baos); + } + } + else { + doc = db.parse(obrUrl.openStream()); + } + Validate.notNull(doc, + "RooBot was unable to parse the repository document of this add-on"); + for (final Element resource : XmlUtils.findElements("resource", + doc.getDocumentElement())) { + if (resource.hasAttribute("uri")) { + if (!resource.getAttribute("uri").startsWith("httppgp")) { + LOGGER.warning("Sorry, the resource " + + resource.getAttribute("uri") + + " does not follow HTTPPGP conventions mangraded by Spring Roo so the OBR file at " + + repoUrl + " is unacceptable at this time"); + return false; + } + } + } + doc = null; + } + catch (final Exception e) { + throw new IllegalStateException( + "RooBot was unable to parse the repository document of this add-on", + e); + } + return true; + } +} \ No newline at end of file diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnStabilityLevel.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnStabilityLevel.java new file mode 100644 index 000000000..573cb4a22 --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/AddOnStabilityLevel.java @@ -0,0 +1,51 @@ +package org.springframework.roo.addon.roobot.client; + +/** + * Indication of stability level for add-ons / components. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +public enum AddOnStabilityLevel { + ANY(3), MILESTONE(2), RELEASE(0), RELEASE_CANDIDATE(1); + + public static AddOnStabilityLevel fromLevel(final int level) { + if (level == ANY.getLevel()) { + return ANY; + } + else if (level == RELEASE_CANDIDATE.getLevel()) { + return RELEASE_CANDIDATE; + } + else if (level == MILESTONE.getLevel()) { + return MILESTONE; + } + else { + return RELEASE; // Default for all unknown inputs + } + } + + public static int getAddOnStabilityLevel(final String version) { + if (version.endsWith(".RELEASE")) { + return RELEASE.getLevel(); + } + else if (version.matches("\\.RC\\d")) { + return RELEASE_CANDIDATE.getLevel(); + } + else if (version.matches("\\.M\\d")) { + return MILESTONE.getLevel(); + } + else { + return ANY.getLevel(); + } + } + + private int level; + + private AddOnStabilityLevel(final int level) { + this.level = level; + } + + public int getLevel() { + return level; + } +} diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Bundle.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Bundle.java new file mode 100644 index 000000000..049fd1faa --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Bundle.java @@ -0,0 +1,166 @@ +package org.springframework.roo.addon.roobot.client.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.lang3.Validate; + +public class Bundle { + + public static List orderByRanking(final List bundles) { + Collections.sort(bundles, new Comparator() { + public int compare(final Bundle o1, final Bundle o2) { + if (o1.getRanking() == o2.getRanking()) { + return 0; + } + else if (o1.getRanking() < o2.getRanking()) { + return 1; + } + else { + return -1; + } + } + }); + return Collections.unmodifiableList(bundles); + } + + public static List orderBySearchRelevance(final List bundles) { + Collections.sort(bundles, new Comparator() { + public int compare(final Bundle o1, final Bundle o2) { + if (o1.getSearchRelevance() < o2.getSearchRelevance()) { + return -1; + } + else if (o1.getSearchRelevance() > o2.getSearchRelevance()) { + return 1; + } + else { + if (o1.getRanking() == o2.getRanking()) { + return 0; + } + else if (o1.getRanking() < o2.getRanking()) { + return 1; + } + else { + return -1; + } + } + } + }); + return Collections.unmodifiableList(bundles); + } + + private List comments; + private float ranking; + private float searchRelevance; + + private String symbolicName; + + private List versions; + + public Bundle(final String symbolicName, final float ranking, + final List inComments) { + super(); + this.symbolicName = symbolicName; + this.ranking = ranking; + Collections.sort(inComments, new Comparator() { + public int compare(final Comment o1, final Comment o2) { + return o1.getDate().compareTo(o2.getDate()); + } + }); + comments = inComments; + versions = new ArrayList(); + } + + public void addComment(final Comment comment) { + comments.add(comment); + } + + public void addVersion(final BundleVersion bundleVersion) { + versions.add(bundleVersion); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Bundle other = (Bundle) obj; + if (symbolicName == null) { + if (other.symbolicName != null) { + return false; + } + } + else if (!symbolicName.equals(other.symbolicName)) { + return false; + } + return true; + } + + public BundleVersion getBundleVersion(final String bundleKey) { + Validate.notBlank(bundleKey, "Bundle key required"); + if (bundleKey.contains(";")) { + final String[] split = bundleKey.split(";"); + Validate.isTrue(split.length == 2, + "Incorrect bundle identifier presented"); + final String remains = split[1]; + for (final BundleVersion version : versions) { + if (version.getVersion().equalsIgnoreCase(remains)) { + return version; + } + } + throw new IllegalStateException("Unable to find bundle with key " + + bundleKey); + } + return getLatestVersion(); + } + + public List getComments() { + return comments; + } + + public BundleVersion getLatestVersion() { + final List versions = BundleVersion + .orderByVersion(getVersions()); + if (versions.size() > 0) { + return versions.get(versions.size() - 1); + } + return null; + } + + public float getRanking() { + return ranking; + } + + public float getSearchRelevance() { + return searchRelevance; + } + + public String getSymbolicName() { + return symbolicName; + } + + public List getVersions() { + return versions; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (symbolicName == null ? 0 : symbolicName.hashCode()); + return result; + } + + public void setSearchRelevance(final float searchRelevance) { + this.searchRelevance = searchRelevance; + } +} diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/BundleVersion.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/BundleVersion.java new file mode 100644 index 000000000..3d025531b --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/BundleVersion.java @@ -0,0 +1,105 @@ +package org.springframework.roo.addon.roobot.client.model; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BundleVersion { + + /** + * Returns a {@link List} of {@link BundleVersion} objects in ascending + * version order, i.e. the object with the smallest version is in position 0 + * and the object with the highest version is in position n-1. This method + * does not take into account release types, that is a 0.1.0.GA is seen as + * an earlier version than a 0.1.0.M1 version. + * + * @param versions + * @return a {@link List} of {@link BundleVersion} objects in ascending + * order + */ + public static List orderByVersion( + final List versions) { + Collections.sort(versions, new Comparator() { + public int compare(final BundleVersion o1, final BundleVersion o2) { + return o1.getVersion().compareToIgnoreCase(o2.getVersion()); + } + }); + return Collections.unmodifiableList(versions); + } + + private Map commands = new HashMap(); + private final String description; + private final String obrUrl; + private final String pgpDescriptions; + private final String pgpKey; + private final String presentationName; + private final String rooVersion; + private final Long size; + private final String uri; + private final String version; + + public BundleVersion(final String uri, final String obrUrl, + final String version, final String presentationName, + final Long size, final String description, final String pgpKey, + final String pgpDescriptions, final String rooVersion, + final Map commands) { + super(); + this.uri = uri; + this.obrUrl = obrUrl; + this.version = version; + this.presentationName = presentationName; + this.size = size; + this.description = description; + this.pgpKey = pgpKey; + this.pgpDescriptions = pgpDescriptions; + this.commands = commands; + this.rooVersion = rooVersion; + } + + public Map getCommands() { + return commands; + } + + public String getDescription() { + return description; + } + + public String getObrUrl() { + return obrUrl; + } + + public String getPgpDescriptions() { + return pgpDescriptions; + } + + public String getPgpKey() { + return pgpKey; + } + + public String getPresentationName() { + return presentationName; + } + + public String getRooVersion() { + return rooVersion; + } + + public Long getSize() { + return size; + } + + public String getSummary() { + return presentationName + "; " + description + "; " + pgpDescriptions + + "; " + commands.toString(); + } + + public String getUri() { + return uri; + } + + public String getVersion() { + return version; + } +} diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Comment.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Comment.java new file mode 100644 index 000000000..17d165c7a --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Comment.java @@ -0,0 +1,28 @@ +package org.springframework.roo.addon.roobot.client.model; + +import java.util.Date; + +public class Comment { + private final String comment; + private final Date date; + private final Rating rating; + + public Comment(final Rating rating, final String comment, final Date date) { + super(); + this.rating = rating; + this.comment = comment; + this.date = date; + } + + public String getComment() { + return comment; + } + + public Date getDate() { + return date; + } + + public Rating getRating() { + return rating; + } +} diff --git a/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Rating.java b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Rating.java new file mode 100644 index 000000000..732aadaab --- /dev/null +++ b/addon-roobot-client/src/main/java/org/springframework/roo/addon/roobot/client/model/Rating.java @@ -0,0 +1,49 @@ +package org.springframework.roo.addon.roobot.client.model; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Star ratings for the "addon feedback bundle" command. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.1.1 + */ +public enum Rating { + BAD(2), GOOD(4), NEUTRAL(3), VERY_BAD(1), VERY_GOOD(5); + + public static Rating fromInt(final Integer rating) { + switch (rating) { + case 1: + return VERY_BAD; + case 2: + return BAD; + case 3: + return NEUTRAL; + case 4: + return GOOD; + case 5: + return VERY_GOOD; + default: + return NEUTRAL; + } + } + + private Integer key; + + private Rating(final Integer key) { + this.key = key; + } + + public Integer getKey() { + return key; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + builder.append("key", key); + return builder.toString(); + } +} diff --git a/addon-security/pom.xml b/addon-security/pom.xml new file mode 100644 index 000000000..5d78b98e9 --- /dev/null +++ b/addon-security/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.security + bundle + Spring Roo - Addon - Spring Security + Configuration and Integration of Spring Security features in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/Permission.java b/addon-security/src/main/java/org/springframework/roo/addon/security/Permission.java new file mode 100644 index 000000000..46071f60c --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/Permission.java @@ -0,0 +1,146 @@ +package org.springframework.roo.addon.security; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.model.JavaType; + +public enum Permission { + // The names of these enum constants are arbitrary; calling code refers to + // these methods by their String key. + + COUNT(CustomDataKeys.COUNT_ALL_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sIsAllowed", getBaseName(), plural); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.COUNT_ALL_PERMISSION; + } + }, + + DELETE(CustomDataKeys.REMOVE_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sIsAllowed", getBaseName(), entityType.getSimpleTypeName()); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.DELETE_PERMISSION; + } + }, + + FIND(CustomDataKeys.FIND_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sIsAllowed", getBaseName(), entityType.getSimpleTypeName()); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.FIND_PERMISSION; + } + }, + + FIND_ALL(CustomDataKeys.FIND_ALL_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sIsAllowed", getBaseName(), plural); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.FIND_ALL_PERMISSION; + } + }, + + FIND_ENTRIES(CustomDataKeys.FIND_ENTRIES_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sEntriesIsAllowed", getBaseName(), entityType.getSimpleTypeName()); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.FIND_ENTRIES_PERMISSION; + } + }, + + SAVE(CustomDataKeys.PERSIST_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sIsAllowed", getBaseName(), entityType.getSimpleTypeName()); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.SAVE_PERMISSION; + } + }, + + UPDATE(CustomDataKeys.MERGE_METHOD) { + @Override + public String getName(final JavaType entityType, final String plural) { + if (StringUtils.isNotBlank(getBaseName())) { + return String.format("%s%sIsAllowed", getBaseName(), entityType.getSimpleTypeName()); + } + return null; + } + + @Override + public String getBaseName() { + return RooPermissionEvaluator.UPDATE_PERMISSION; + } + }; + + private final MethodMetadataCustomDataKey key; + + public MethodMetadataCustomDataKey getKey() { + return key; + } + + /** + * Constructor + * + * @param key the internal key for this method (required) + */ + private Permission(final MethodMetadataCustomDataKey key) { + Validate.notNull(key, "Method key is required"); + this.key = key; + } + + public abstract String getBaseName(); + + /** + * Returns the name of this method, based on the given inputs + * + * @param annotationValues the values of the {@link RooService} annotation + * on the service + * @param entityType the type of domain entity managed by the service + * @param plural the plural form of the entity + * @return null if the method is not implemented + */ + public abstract String getName(JavaType entityType, String plural); +} diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorAnnotationValues.java b/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorAnnotationValues.java new file mode 100644 index 000000000..ed0d2c11f --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorAnnotationValues.java @@ -0,0 +1,26 @@ +package org.springframework.roo.addon.security; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.RooJavaType; + +public class PermissionEvaluatorAnnotationValues extends AbstractAnnotationValues { + @AutoPopulate private final boolean defaultReturnValue = false; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata to parse (required) + */ + public PermissionEvaluatorAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_PERMISSION_EVALUATOR); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public boolean getDefaultReturnValue() { + return defaultReturnValue; + } +} diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorMetadata.java b/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorMetadata.java new file mode 100644 index 000000000..8db3d3a0c --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorMetadata.java @@ -0,0 +1,208 @@ +package org.springframework.roo.addon.security; + +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.model.SpringJavaType.AUTHENTICATION; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +public class PermissionEvaluatorMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + private static final String PROVIDES_TYPE_STRING = PermissionEvaluatorMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + protected PermissionEvaluatorMetadata(String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final MemberDetails governorDetails, + final PermissionEvaluatorAnnotationValues annotationValues, + final Map domainTypeToPlurals) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + + //Creates method hasPermission(Authentication authentication, Object targetDomainObject, Object permission) + List hasPermissionParameterTypes = new ArrayList(); + hasPermissionParameterTypes.add(AUTHENTICATION); + hasPermissionParameterTypes.add(JavaType.OBJECT); + hasPermissionParameterTypes.add(JavaType.OBJECT); + + JavaSymbolName hasPermissionMethodName = new JavaSymbolName("hasPermission"); + if (!governorDetails.isMethodDeclaredByAnother(hasPermissionMethodName, + hasPermissionParameterTypes, getId())) { + + List hasPermissionParameterNames = new ArrayList(); + hasPermissionParameterNames.add(new JavaSymbolName("authentication")); + hasPermissionParameterNames.add(new JavaSymbolName("targetObject")); + hasPermissionParameterNames.add(new JavaSymbolName("permission")); + + final InvocableMemberBodyBuilder hasPermissionBodyBuilder = new InvocableMemberBodyBuilder(); + + hasPermissionBodyBuilder.append("\n\t\treturn checkManagedPermissions(authentication, targetObject, permission);"); + + MethodMetadataBuilder hasPermissionMethodMetadataBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, hasPermissionMethodName, JavaType.BOOLEAN_PRIMITIVE, + AnnotatedJavaType + .convertFromJavaTypes(hasPermissionParameterTypes), + hasPermissionParameterNames, hasPermissionBodyBuilder); + + builder.addMethod(hasPermissionMethodMetadataBuilder.build()); + } + + //Creates method hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) + hasPermissionParameterTypes = new ArrayList(); + hasPermissionParameterTypes.add(AUTHENTICATION); + hasPermissionParameterTypes.add(JavaType.SERIALIZABLE); + hasPermissionParameterTypes.add(JavaType.STRING); + hasPermissionParameterTypes.add(JavaType.OBJECT); + + if (!governorDetails.isMethodDeclaredByAnother(hasPermissionMethodName, + hasPermissionParameterTypes, getId())) { + final InvocableMemberBodyBuilder hasPermissionBodyBuilder2 = new InvocableMemberBodyBuilder(); + hasPermissionBodyBuilder2.append(String.format("\n\t\treturn %s;\n",annotationValues.getDefaultReturnValue())); + + List hasPermissionParameterNames2 = new ArrayList(); + hasPermissionParameterNames2.add(new JavaSymbolName( + "authentication")); + hasPermissionParameterNames2.add(new JavaSymbolName("targetId")); + hasPermissionParameterNames2.add(new JavaSymbolName("targetType")); + hasPermissionParameterNames2.add(new JavaSymbolName("permission")); + + MethodMetadataBuilder hasPermissionMethodMetadataBuilder2 = new MethodMetadataBuilder( + getId(), + PUBLIC, + new JavaSymbolName("hasPermission"), + JavaType.BOOLEAN_PRIMITIVE, + AnnotatedJavaType + .convertFromJavaTypes(hasPermissionParameterTypes), + hasPermissionParameterNames2, hasPermissionBodyBuilder2); + + builder.addMethod(hasPermissionMethodMetadataBuilder2.build()); + } + + List checkManagedPermissionsParameterTypes = new ArrayList(); + checkManagedPermissionsParameterTypes.add(AUTHENTICATION); + checkManagedPermissionsParameterTypes.add(JavaType.OBJECT); + checkManagedPermissionsParameterTypes.add(JavaType.OBJECT); + + JavaSymbolName checkManagedPermissionsMethodName = new JavaSymbolName("checkManagedPermissions"); + + if (!governorDetails.isMethodDeclaredByAnother(checkManagedPermissionsMethodName, + checkManagedPermissionsParameterTypes, getId())) { + + List checkManagedPermissionsParameterNames = new ArrayList(); + checkManagedPermissionsParameterNames + .add(new JavaSymbolName("authentication")); + checkManagedPermissionsParameterNames.add(new JavaSymbolName("targetObject")); + checkManagedPermissionsParameterNames.add(new JavaSymbolName("permission")); + + final InvocableMemberBodyBuilder checkManagedPermissionsBodyBuilder = new InvocableMemberBodyBuilder(); + boolean firstPass = true; + for (Entry entrySet : domainTypeToPlurals.entrySet()) { + for (Permission permission : Permission.values()){ + String permissionName = permission.getName(entrySet.getKey(), entrySet.getValue()); + if (permissionName == null) { + continue; + } + checkManagedPermissionsBodyBuilder.append(String.format("\n\t\t%sif(permission.equals(\"%s\")){",firstPass ? "" : "else ", permissionName)); + checkManagedPermissionsBodyBuilder.append(String.format("\n\t\t\treturn %s(authentication, (%s)targetObject);", permissionName, entrySet.getKey().getFullyQualifiedTypeName())); + checkManagedPermissionsBodyBuilder.append("\n\t\t}"); + } + firstPass = false; + } + checkManagedPermissionsBodyBuilder.append(String.format("\n\t\treturn %s;\n",annotationValues.getDefaultReturnValue())); + + MethodMetadataBuilder hasPermissionMethodMetadataBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, checkManagedPermissionsMethodName, JavaType.BOOLEAN_PRIMITIVE, + AnnotatedJavaType + .convertFromJavaTypes(checkManagedPermissionsParameterTypes), + checkManagedPermissionsParameterNames, checkManagedPermissionsBodyBuilder); + + builder.addMethod(hasPermissionMethodMetadataBuilder.build()); + + } + + for (Entry entrySet : domainTypeToPlurals.entrySet()) { + for (Permission permission : Permission.values()){ + String permissionName = permission.getName(entrySet.getKey(), entrySet.getValue()); + if (permissionName == null) { + continue; + } + JavaSymbolName isAllowedMethodName = new JavaSymbolName(permissionName); + List isAllowedParameterTypes = new ArrayList(); + isAllowedParameterTypes.add(AUTHENTICATION); + isAllowedParameterTypes.add(entrySet.getKey()); + if (!governorDetails.isMethodDeclaredByAnother(isAllowedMethodName, isAllowedParameterTypes, getId())) { + List isAllowedParameterNames = new ArrayList(); + isAllowedParameterNames.add(new JavaSymbolName("authentication")); + isAllowedParameterNames.add(JavaSymbolName.getReservedWordSafeName(entrySet.getKey())); + + final InvocableMemberBodyBuilder isAllowedBodyBuilder = new InvocableMemberBodyBuilder(); + isAllowedBodyBuilder.append(String.format("\n\t\treturn %s;\n",annotationValues.getDefaultReturnValue())); + + MethodMetadataBuilder isAllowedMethodMetadataBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, isAllowedMethodName, JavaType.BOOLEAN_PRIMITIVE, + AnnotatedJavaType + .convertFromJavaTypes(isAllowedParameterTypes), + isAllowedParameterNames, isAllowedBodyBuilder); + + builder.addMethod(isAllowedMethodMetadataBuilder.build()); + } + } + } + + itdTypeDetails = builder.build(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorMetadataProvider.java b/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorMetadataProvider.java new file mode 100644 index 000000000..1199d80a8 --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/PermissionEvaluatorMetadataProvider.java @@ -0,0 +1,195 @@ +package org.springframework.roo.addon.security; + +import static org.springframework.roo.model.SpringJavaType.PERMISSION_EVALUATOR; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +@Component +@Service +public class PermissionEvaluatorMetadataProvider extends + AbstractMemberDiscoveringItdMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(PermissionEvaluatorMetadataProvider.class); + + private TypeManagementService typeManagementService; + + private final Map managedEntityTypes = new HashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + setIgnoreTriggerAnnotations(true); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + } + + @Override + public String getItdUniquenessFilenameSuffix() { + return "PermissionEvaluator"; + } + + @Override + public String getProvidesType() { + return PermissionEvaluatorMetadata.getMetadataIdentiferType(); + } + + @Override + protected String getLocalMidToRequest(ItdTypeDetails itdTypeDetails) { + // Determine the governor for this ITD, and whether any metadata is even + // hoping to hear about changes to that JavaType and its ITDs + final JavaType governor = itdTypeDetails.getName(); + final String localMid = managedEntityTypes.get(governor); + if (localMid != null) { + return localMid; + } + + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + final String localMidType = managedEntityTypes.get(type); + if (localMidType != null) { + return localMidType; + } + } + } + return null; + } + + @Override + protected String createLocalIdentifier(JavaType javaType, LogicalPath path) { + return PermissionEvaluatorMetadata.createIdentifier(javaType, path); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + String metadataIdentificationString) { + final JavaType javaType = PermissionEvaluatorMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = PermissionEvaluatorMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + final ClassOrInterfaceTypeDetails permissionEvaluatorClass = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (permissionEvaluatorClass == null) { + return null; + } + + JavaType permissionEvaluatorInterface = null; + + for (final JavaType implementedType : permissionEvaluatorClass + .getImplementsTypes()) { + if (implementedType.equals(PERMISSION_EVALUATOR)) { + permissionEvaluatorInterface = implementedType; + break; + } + } + + //Checks to ensure the supposed permission evaluator class actually implements PermissionEvaluator + if (permissionEvaluatorInterface == null) { + return null; + } + + final PermissionEvaluatorAnnotationValues annotationValues = new PermissionEvaluatorAnnotationValues( + governorPhysicalTypeMetadata); + + //AnnotationMetadata annotationMetadata = MemberFindingUtils.getAnnotationOfType(permissionEvaluatorClass.getAnnotations(), RooJavaType.ROO_PERMISSION_EVALUATOR); + //Checks to ensure permission evaluator class includes the @RooPermissionEvaluator annotation + /*if (annotationValues == null) { + return null; + }*/ + + final MemberDetails permissionEvaluatorClassDetails = memberDetailsScanner + .getMemberDetails(getClass().getName(), + permissionEvaluatorClass); + + Map domainTypesToPlurals = getDomainTypesToPlurals(); + + //AnnotationAttributeValue defaultReturnValue = annotationMetadata.getAttribute("defaultReturnValue"); + + return new PermissionEvaluatorMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, + permissionEvaluatorClassDetails, + annotationValues, // == null ? false : defaultReturnValue.getValue(), + domainTypesToPlurals); + } + + private Map getDomainTypesToPlurals() { + + Map domainTypesToPlurals = new HashMap (); + for (ClassOrInterfaceTypeDetails cid : getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_SERVICE)) { + AnnotationMetadata annotationMetadata = MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), RooJavaType.ROO_SERVICE); + AnnotationAttributeValue usePermissionEvaluator = annotationMetadata.getAttribute("usePermissionEvaluator"); + if (usePermissionEvaluator == null || usePermissionEvaluator.getValue() == false){ + continue; + } + AnnotationAttributeValue> domainTypes = annotationMetadata.getAttribute("domainTypes"); + for (ClassAttributeValue domainType : domainTypes.getValue()) { + final ClassOrInterfaceTypeDetails domainTypeDetails = getTypeLocationService() + .getTypeDetails(domainType.getValue()); + if (domainTypeDetails == null) { + return null; + } + final LogicalPath path = PhysicalTypeIdentifier + .getPath(domainTypeDetails.getDeclaredByMetadataId()); + final String pluralId = PluralMetadata.createIdentifier(domainType.getValue(), + path); + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(pluralId); + if (pluralMetadata == null) { + continue; + } + domainTypesToPlurals.put(domainType.getValue(), pluralMetadata.getPlural()); + } + } + return domainTypesToPlurals; + } +} diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/RooPermissionEvaluator.java b/addon-security/src/main/java/org/springframework/roo/addon/security/RooPermissionEvaluator.java new file mode 100644 index 000000000..881214083 --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/RooPermissionEvaluator.java @@ -0,0 +1,45 @@ +package org.springframework.roo.addon.security; + +public @interface RooPermissionEvaluator { + /** + * The default prefix of the "count all" permission + */ + String COUNT_ALL_PERMISSION = null; + + /** + * The default name of the "delete" permission + */ + String DELETE_PERMISSION = "delete"; + + /** + * The default prefix of the "find all" permission + */ + String FIND_ALL_PERMISSION = null; + + /** + * The default prefix of the "find entries" permission + */ + String FIND_ENTRIES_PERMISSION = null; + + /** + * The default prefix of the "find" permission + */ + String FIND_PERMISSION = "find"; + + /** + * The default name of the "save" permission + */ + String SAVE_PERMISSION = "save"; + + /** + * The default name of the "update" permission + */ + String UPDATE_PERMISSION = "update"; + + /** + * Indicates the default return value for the permission evaluator + * + * @return see above + */ + boolean defaultReturnValue() default false; +} diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityCommands.java b/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityCommands.java new file mode 100644 index 000000000..afc8ad0b9 --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityCommands.java @@ -0,0 +1,47 @@ +package org.springframework.roo.addon.security; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the security add-on to be used by the ROO shell. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class SecurityCommands implements CommandMarker { + + @Reference private SecurityOperations securityOperations; + + @CliCommand(value = "security setup", help = "Install Spring Security into your project") + public void installSecurity() { + securityOperations.installSecurity(); + } + + @CliAvailabilityIndicator("security setup") + public boolean isInstallSecurityAvailable() { + return securityOperations.isSecurityInstallationPossible(); + } + + @CliAvailabilityIndicator("permissionEvaluator") + public boolean isPermissionEvaluatorCommandAvailable() { + return securityOperations + .isServicePermissionEvaluatorInstallationPossible(); + } + + @CliCommand(value = "permissionEvaluator", help = "Create a permission evaluator") + public void setupPermissionEvaluator( + @CliOption(key = "package", mandatory = true, optionContext = PROJECT, help = "The package to add the permission evaluator to") final JavaPackage evaluatorPackage) { + securityOperations.installPermissionEvaluator(evaluatorPackage); + } +} \ No newline at end of file diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityOperations.java b/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityOperations.java new file mode 100644 index 000000000..e22c58c81 --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityOperations.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.security; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.Feature; + +/** + * Interface for {@link SecurityOperationsImpl}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface SecurityOperations extends Feature { + + String SECURITY_FILTER_NAME = "springSecurityFilterChain"; + + void installSecurity(); + + void installPermissionEvaluator(JavaPackage permissionEvaluatorPackage); + + boolean isSecurityInstallationPossible(); + + boolean isServicePermissionEvaluatorInstallationPossible(); +} \ No newline at end of file diff --git a/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityOperationsImpl.java b/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityOperationsImpl.java new file mode 100644 index 000000000..5d920e799 --- /dev/null +++ b/addon-security/src/main/java/org/springframework/roo/addon/security/SecurityOperationsImpl.java @@ -0,0 +1,428 @@ +package org.springframework.roo.addon.security; + +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.classpath.PhysicalTypeCategory.CLASS; +import static org.springframework.roo.model.RooJavaType.ROO_PERMISSION_EVALUATOR; +import static org.springframework.roo.model.SpringJavaType.PERMISSION_EVALUATOR; +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.controller.WebMvcOperations; +import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperations; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.Property; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.WebXmlUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Provides security installation services. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class SecurityOperationsImpl implements SecurityOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(SecurityOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private static final Dependency SPRING_SECURITY = new Dependency( + "org.springframework.security", "spring-security-core", + "3.1.0.RELEASE"); + + private FileManager fileManager; + private PathResolver pathResolver; + private ProjectOperations projectOperations; + private TilesOperations tilesOperations; + private TypeManagementService typeManagementService; + private MetadataService metadataService; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + @Override + public void installSecurity() { + // Parse the configuration.xml file + final Element configuration = XmlUtils.getConfiguration(getClass()); + + // Add POM properties + updatePomProperties(configuration, + getProjectOperations().getFocusedModuleName()); + + // Add dependencies to POM + updateDependencies(configuration, + getProjectOperations().getFocusedModuleName()); + + // Copy the template across + final String destination = getPathResolver().getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, "applicationContext-security.xml"); + if (!getFileManager().exists(destination)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "applicationContext-security-template.xml"); + outputStream = getFileManager().createFile(destination) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + // Copy the template across + final String loginPage = getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/views/login.jspx"); + if (!getFileManager().exists(loginPage)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils + .getInputStream(getClass(), "login.jspx"); + outputStream = getFileManager().createFile(loginPage) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + if (getFileManager().exists(getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/views/views.xml"))) { + getTilesOperations().addViewDefinition("", + getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP), "login", + getTilesOperations().PUBLIC_TEMPLATE, + "/WEB-INF/views/login.jspx"); + } + + final String webXmlPath = getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/web.xml"); + final Document webXmlDocument = XmlUtils.readXml(getFileManager() + .getInputStream(webXmlPath)); + + WebXmlUtils.addFilterAtPosition(WebXmlUtils.FilterPosition.LAST, null, + null, SecurityOperations.SECURITY_FILTER_NAME, + "org.springframework.web.filter.DelegatingFilterProxy", "/*", + webXmlDocument, null); + getFileManager().createOrUpdateTextFileIfRequired(webXmlPath, + XmlUtils.nodeToString(webXmlDocument), false); + + // Include static view controller handler to webmvc-config.xml + final String webConfigPath = getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/spring/webmvc-config.xml"); + final Document webConfigDocument = XmlUtils.readXml(getFileManager() + .getInputStream(webConfigPath)); + final Element webConfig = webConfigDocument.getDocumentElement(); + final Element viewController = DomUtils.findFirstElementByName( + "mvc:view-controller", webConfig); + Validate.notNull(viewController, + "Could not find mvc:view-controller in %s", webConfig); + viewController.getParentNode() + .insertBefore( + new XmlElementBuilder("mvc:view-controller", + webConfigDocument).addAttribute("path", + "/login").build(), viewController); + getFileManager().createOrUpdateTextFileIfRequired(webConfigPath, + XmlUtils.nodeToString(webConfigDocument), false); + } + + private void createPermissionEvaluator( + final JavaPackage permissionEvaluatorPackage) { + installPermissionEvaluatorTemplate(permissionEvaluatorPackage); + final LogicalPath focusedSrcMainJava = LogicalPath.getInstance( + SRC_MAIN_JAVA, getProjectOperations().getFocusedModuleName()); + JavaType permissionEvaluatorClass = new JavaType( + permissionEvaluatorPackage.getFullyQualifiedPackageName() + + ".ApplicationPermissionEvaluator"); + final String identifier = getPathResolver().getFocusedCanonicalPath( + Path.SRC_MAIN_JAVA, permissionEvaluatorClass); + if (getFileManager().exists(identifier)) { + return; // Type already exists - nothing to do + } + + final AnnotationMetadataBuilder classAnnotationMetadata = new AnnotationMetadataBuilder( + ROO_PERMISSION_EVALUATOR); + final String classMid = PhysicalTypeIdentifier.createIdentifier( + permissionEvaluatorClass, getPathResolver().getPath(identifier)); + final ClassOrInterfaceTypeDetailsBuilder classBuilder = new ClassOrInterfaceTypeDetailsBuilder( + classMid, PUBLIC, permissionEvaluatorClass, CLASS); + classBuilder.addAnnotation(classAnnotationMetadata.build()); + classBuilder.addImplementsType(PERMISSION_EVALUATOR); + getTypeManagementService().createOrUpdateTypeOnDisk(classBuilder.build()); + + getMetadataService().get(PermissionEvaluatorMetadata.createIdentifier( + permissionEvaluatorClass, focusedSrcMainJava)); + } + + private void installPermissionEvaluatorTemplate( + JavaPackage permissionEvaluatorPackage) { + // Copy the template across + final String destination = getPathResolver().getFocusedIdentifier( + Path.SPRING_CONFIG_ROOT, + "applicationContext-security-permissionEvaluator.xml"); + if (!getFileManager().exists(destination)) { + try { + InputStream inputStream = FileUtils + .getInputStream(getClass(), + "applicationContext-security-permissionEvaluator-template.xml"); + String content = IOUtils.toString(inputStream); + content = content.replace("__PERMISSION_EVALUATOR_PACKAGE__", + permissionEvaluatorPackage + .getFullyQualifiedPackageName()); + + getFileManager().createOrUpdateTextFileIfRequired(destination, + content, true); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + } + } + + @Override + public void installPermissionEvaluator( + final JavaPackage permissionEvaluatorPackage) { + Validate.isTrue( + getProjectOperations().isFeatureInstalled(FeatureNames.SECURITY), + "Security must first be setup before securing a method"); + Validate.notNull(permissionEvaluatorPackage, "Package required"); + createPermissionEvaluator(permissionEvaluatorPackage); + } + + @Override + public boolean isSecurityInstallationPossible() { + // Permit installation if they have a web project (as per ROO-342) and + // no version of Spring Security is already installed. + return getProjectOperations().isFocusedProjectAvailable() + && getFileManager().exists(getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/web.xml")) + && !getProjectOperations().getFocusedModule() + .hasDependencyExcludingVersion(SPRING_SECURITY) + && !getProjectOperations() + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + @Override + public boolean isServicePermissionEvaluatorInstallationPossible() { + return getProjectOperations().isFocusedProjectAvailable() + && getProjectOperations().isFeatureInstalled(FeatureNames.SECURITY); + } + + private void updateDependencies(final Element configuration, + final String moduleName) { + final List dependencies = new ArrayList(); + final List securityDependencies = XmlUtils.findElements( + "/configuration/spring-security/dependencies/dependency", + configuration); + for (final Element dependencyElement : securityDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + getProjectOperations().addDependencies(moduleName, dependencies); + } + + private void updatePomProperties(final Element configuration, + final String moduleName) { + final List databaseProperties = XmlUtils.findElements( + "/configuration/spring-security/properties/*", configuration); + for (final Element property : databaseProperties) { + getProjectOperations().addProperty(moduleName, new Property(property)); + } + } + + @Override + public String getName() { + return FeatureNames.SECURITY; + } + + @Override + public boolean isInstalledInModule(String moduleName) { + final Pom pom = getProjectOperations().getPomFromModuleName(moduleName); + if (pom == null) { + return false; + } + for (final Dependency dependency : pom.getDependencies()) { + if ("spring-security-core".equals(dependency.getArtifactId())) { + return true; + } + } + return false; + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on SecurityOperationsImpl."); + return null; + } + }else{ + return fileManager; + } + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on SecurityOperationsImpl."); + return null; + } + }else{ + return pathResolver; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on SecurityOperationsImpl."); + return null; + } + }else{ + return projectOperations; + } + } + + public TilesOperations getTilesOperations(){ + if(tilesOperations == null){ + // Get all Services implement TilesOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TilesOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (TilesOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TilesOperations on SecurityOperationsImpl."); + return null; + } + }else{ + return tilesOperations; + } + } + + public TypeManagementService getTypeManagementService(){ + if(typeManagementService == null){ + // Get all Services implement TypeManagementService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeManagementService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeManagementService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeManagementService on SecurityOperationsImpl."); + return null; + } + }else{ + return typeManagementService; + } + } + + public MetadataService getMetadataService(){ + if(metadataService == null){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on SecurityOperationsImpl."); + return null; + } + }else{ + return metadataService; + } + } +} diff --git a/addon-security/src/main/resources/org/springframework/roo/addon/security/applicationContext-security-permissionEvaluator-template.xml b/addon-security/src/main/resources/org/springframework/roo/addon/security/applicationContext-security-permissionEvaluator-template.xml new file mode 100644 index 000000000..eb3400965 --- /dev/null +++ b/addon-security/src/main/resources/org/springframework/roo/addon/security/applicationContext-security-permissionEvaluator-template.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-security/src/main/resources/org/springframework/roo/addon/security/applicationContext-security-template.xml b/addon-security/src/main/resources/org/springframework/roo/addon/security/applicationContext-security-template.xml new file mode 100644 index 000000000..cf4bea4bd --- /dev/null +++ b/addon-security/src/main/resources/org/springframework/roo/addon/security/applicationContext-security-template.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-security/src/main/resources/org/springframework/roo/addon/security/configuration.xml b/addon-security/src/main/resources/org/springframework/roo/addon/security/configuration.xml new file mode 100644 index 000000000..a08122faa --- /dev/null +++ b/addon-security/src/main/resources/org/springframework/roo/addon/security/configuration.xml @@ -0,0 +1,42 @@ + + + + + 3.2.5.RELEASE + + + + org.springframework.security + spring-security-core + ${spring-security.version} + + + commons-logging + commons-logging + + + + + org.springframework.security + spring-security-config + ${spring-security.version} + + + commons-logging + commons-logging + + + + + org.springframework.security + spring-security-web + ${spring-security.version} + + + org.springframework.security + spring-security-taglibs + ${spring-security.version} + + + + \ No newline at end of file diff --git a/addon-security/src/main/resources/org/springframework/roo/addon/security/login.jspx b/addon-security/src/main/resources/org/springframework/roo/addon/security/login.jspx new file mode 100644 index 000000000..a4a04fb63 --- /dev/null +++ b/addon-security/src/main/resources/org/springframework/roo/addon/security/login.jspx @@ -0,0 +1,60 @@ +

    + + + + + +
    +

    + + + . +

    +
    +
    + +

    + +

    +
    + +
    +
    + + + + +
    +
    +
    + + + + +
    +
    +
    + + + + + +
    +
    +
    +
    + diff --git a/addon-serializable/pom.xml b/addon-serializable/pom.xml new file mode 100644 index 000000000..017679780 --- /dev/null +++ b/addon-serializable/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.serializable + bundle + Spring Roo - Addon - java.io.Serializable + Integration of Java serialization support through an AspectJ ITD. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/RooSerializable.java b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/RooSerializable.java new file mode 100644 index 000000000..08c0fb416 --- /dev/null +++ b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/RooSerializable.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.serializable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a class should implement the {@link java.io.Serializable} + * interface. Generates and maintains a static final long serialVersionUID. + * + * @author Alan Stewart + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooSerializable { +} diff --git a/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadata.java b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadata.java new file mode 100644 index 000000000..e6e8fe067 --- /dev/null +++ b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadata.java @@ -0,0 +1,130 @@ +package org.springframework.roo.addon.serializable; + +import static java.lang.reflect.Modifier.FINAL; +import static java.lang.reflect.Modifier.PRIVATE; +import static java.lang.reflect.Modifier.STATIC; +import static org.springframework.roo.model.JavaType.LONG_PRIMITIVE; +import static org.springframework.roo.model.JdkJavaType.SERIALIZABLE; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetailsBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooSerializable}. + * + * @author Alan Stewart + * @since 1.1 + */ +public class SerializableMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String DEFAULT_SERIAL_VERSION = "1L"; + private static final String PROVIDES_TYPE_STRING = SerializableMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + static final JavaSymbolName SERIAL_VERSION_FIELD = new JavaSymbolName( + "serialVersionUID"); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + /** + * Constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalTypeMetadata + */ + public SerializableMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), "Metadata id '%s' is invalid", + identifier); + + if (isValid()) { + ensureGovernorImplements(SERIALIZABLE); + addSerialVersionUIDFieldIfRequired(); + buildItd(); + } + } + + /** + * Adds a "serialVersionUID" field to the {@link ItdTypeDetailsBuilder} if + * the governor doesn't already contain it. + */ + private void addSerialVersionUIDFieldIfRequired() { + if (!governorTypeDetails.declaresField(SERIAL_VERSION_FIELD)) { + builder.addField(createSerialVersionField()); + } + } + + /** + * Generates a field to store the serialization ID + * + * @return a non-null field + */ + private FieldMetadataBuilder createSerialVersionField() { + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId(), PRIVATE | STATIC | FINAL, SERIAL_VERSION_FIELD, + LONG_PRIMITIVE, DEFAULT_SERIAL_VERSION); + fieldBuilder.getCustomData().put( + CustomDataKeys.SERIAL_VERSION_UUID_FIELD, true); + return fieldBuilder; + } + + /** + * For unit testing + * + * @return + */ + ItdTypeDetails getItdTypeDetails() { + return itdTypeDetails; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadataProvider.java b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadataProvider.java new file mode 100644 index 000000000..84029dd3d --- /dev/null +++ b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.serializable; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link SerializableMetadata}. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface SerializableMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadataProviderImpl.java b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadataProviderImpl.java new file mode 100644 index 000000000..d5109eeb8 --- /dev/null +++ b/addon-serializable/src/main/java/org/springframework/roo/addon/serializable/SerializableMetadataProviderImpl.java @@ -0,0 +1,74 @@ +package org.springframework.roo.addon.serializable; + +import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Implementation of {@link SerializableMetadataProvider}. + * + * @author Alan Stewart + * @since 1.1 + */ +@Component +@Service +public class SerializableMetadataProviderImpl extends + AbstractItdMetadataProvider implements SerializableMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_SERIALIZABLE); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return SerializableMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_SERIALIZABLE); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = SerializableMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = SerializableMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Serializable"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + return new SerializableMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata); + } + + public String getProvidesType() { + return SerializableMetadata.getMetadataIdentiferType(); + } +} diff --git a/addon-serializable/src/test/java/org/springframework/roo/addon/serializable/SerializableMetadataTest.java b/addon-serializable/src/test/java/org/springframework/roo/addon/serializable/SerializableMetadataTest.java new file mode 100644 index 000000000..77d77cb6c --- /dev/null +++ b/addon-serializable/src/test/java/org/springframework/roo/addon/serializable/SerializableMetadataTest.java @@ -0,0 +1,91 @@ +package org.springframework.roo.addon.serializable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; + +/** + * Unit test of {@link SerializableMetadata} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class SerializableMetadataTest { + + private static final String METADATA_ID = "MID:org.springframework.roo.addon.serializable.SerializableMetadata#SRC_MAIN_JAVA?com.example.Person"; + + @Mock private JavaType mockAspectName; + // Fixture + @Mock private ClassOrInterfaceTypeDetails mockClassDetails; + @Mock private PhysicalTypeMetadata mockGovernor; + @Mock private JavaPackage mockPackage; + @Mock private JavaType mockTargetType; + + /** + * Asserts that the ITD has the expected contents when the governor does or + * does not contain the required members + * + * @param alreadySerializable + * @param alreadyHasVersionField + */ + private void assertItdContents(final boolean alreadySerializable, + final boolean alreadyHasVersionField) { + // Set up + when(mockClassDetails.implementsType(JdkJavaType.SERIALIZABLE)) + .thenReturn(alreadySerializable); + when( + mockClassDetails + .declaresField(SerializableMetadata.SERIAL_VERSION_FIELD)) + .thenReturn(alreadyHasVersionField); + final SerializableMetadata metadata = new SerializableMetadata( + METADATA_ID, mockAspectName, mockGovernor); + + // Invoke + final ItdTypeDetails itd = metadata.getItdTypeDetails(); + + // Check + assertEquals(alreadySerializable ? 0 : 1, itd.getImplementsTypes() + .size()); + assertEquals(alreadyHasVersionField ? 0 : 1, itd.getDeclaredFields() + .size()); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockAspectName.getPackage()).thenReturn(mockPackage); + when(mockClassDetails.getName()).thenReturn(mockTargetType); + when(mockGovernor.getMemberHoldingTypeDetails()).thenReturn( + mockClassDetails); + } + + @Test + public void testWhenGovernorAlreadyHasSerialVersionField() { + assertItdContents(false, true); + } + + @Test + public void testWhenGovernorAlreadyImplementsSerializable() { + assertItdContents(true, false); + } + + @Test + public void testWhenGovernorIsAlreadyFullySerializable() { + assertItdContents(true, true); + } + + @Test + public void testWhenGovernorIsNotAtAllSerializable() { + assertItdContents(false, false); + } +} diff --git a/addon-solr/pom.xml b/addon-solr/pom.xml new file mode 100644 index 000000000..a33c466af --- /dev/null +++ b/addon-solr/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.solr + bundle + Spring Roo - Addon - Solr + Configuration and Integration of Apache Solr features in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.addon.jpa + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + + + org.springframework.roo + org.springframework.roo.addon.plural + + + \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/RooSolrSearchable.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/RooSolrSearchable.java new file mode 100644 index 000000000..1fa1b8d4e --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/RooSolrSearchable.java @@ -0,0 +1,72 @@ +package org.springframework.roo.addon.solr; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Adds a Pojo to a Solr managed search index + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooSolrSearchable { + + /** + * Specify name of the "deleteIndex" methods to generate. Use a value of "" + * to avoid the generation of the deleteIndex method. + * + * @return the name of the "deleteIndex" method to generate (defaults to + * "deleteIndex"; mandatory) + */ + String deleteIndexMethod() default "deleteIndex"; + + /** + * Specify name of the "index" methods to generate. Use a value of "" to + * avoid the generation of both index methods. The method name will be + * concatenated by the simple name of the entity type (ie: indexOwner) + * + * @return the name of the "index" method to generate (defaults to "index"; + * mandatory) + */ + String indexMethod() default "index"; + + /** + * Specify name of the "postPersistOrUpdate" method to generate. Use a value + * of "" to avoid the generation of a postPersistOrUpdate method. + * + * @return the name of the "postPersistOrUpdate" method to generate + * (defaults to "postPersistOrUpdate"; mandatory) + */ + String postPersistOrUpdateMethod() default "postPersistOrUpdate"; + + /** + * Specify name of the "preRemove" method to generate. Use a value of "" to + * avoid the generation of a preRemove method. + * + * @return the name of the "preRemove" method to generate (defaults to + * "preRemove"; mandatory) + */ + String preRemoveMethod() default "preRemove"; + + /** + * Specify name of the "search" method to generate. Use a value of "" to + * avoid the generation of a search method. + * + * @return the name of the "search" method to generate (defaults to + * "search"; mandatory) + */ + String searchMethod() default "search"; + + /** + * Specify name of the "search" method to generate. Use a value of "" to + * avoid the generation of a search method. + * + * @return the name of the "search" method to generate (defaults to + * "search"; mandatory) + */ + String simpleSearchMethod() default "search"; +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/RooSolrWebSearchable.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/RooSolrWebSearchable.java new file mode 100644 index 000000000..b4bd4552a --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/RooSolrWebSearchable.java @@ -0,0 +1,35 @@ +package org.springframework.roo.addon.solr; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Adds a Pojo to expose controller method 'search' and 'autoComplete' + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooSolrWebSearchable { + + /** + * Specify name of the "autoComplete" method to generate. Use a value of "" + * to avoid the generation of a autoComplete method. + * + * @return the name of the "search" method to generate (defaults to + * "search"; mandatory) + */ + String autoCompleteMethod() default "autoComplete"; + + /** + * Specify name of the "search" method to generate. Use a value of "" to + * avoid the generation of a search method. + * + * @return the name of the "search" method to generate (defaults to + * "search"; mandatory) + */ + String searchMethod() default "search"; +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrCommands.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrCommands.java new file mode 100644 index 000000000..d1cf779d4 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrCommands.java @@ -0,0 +1,53 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'solr search' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SolrCommands implements CommandMarker { + + @Reference private SolrOperations solrOperations; + + @CliAvailabilityIndicator({ "solr setup" }) + public boolean setupCommandAvailable() { + return solrOperations.isSolrInstallationPossible(); + } + + @CliCommand(value = "solr add", help = "Make target type searchable") + public void solrAdd( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The type to be made searchable") final JavaType javaType) { + + solrOperations.addSearch(javaType); + } + + @CliCommand(value = "solr all", help = "Make all eligible project types searchable") + public void solrAll() { + solrOperations.addAll(); + } + + @CliAvailabilityIndicator({ "solr add", "solr all" }) + public boolean solrCommandAvailable() { + return solrOperations.isSearchAvailable(); + } + + @CliCommand(value = "solr setup", help = "Install support for Solr search integration") + public void solrSetup( + @CliOption(key = { "searchServerUrl" }, mandatory = false, unspecifiedDefaultValue = "http://localhost:8983/solr", specifiedDefaultValue = "http://localhost:8983/solr", help = "The URL of the Solr search server") final String searchServerUrl) { + solrOperations.setupConfig(searchServerUrl); + } +} \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrJspMetadata.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrJspMetadata.java new file mode 100644 index 000000000..6b6461860 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrJspMetadata.java @@ -0,0 +1,78 @@ +package org.springframework.roo.addon.solr; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; + +/** + * Metadata built from {@link SolrWebSearchMetadata}. A single + * {@link SolrJspMetadata} represents all Solr JSPs for an associated + * controller. The metadata identifier for a {@link SolrJspMetadata} is the + * fully qualifier name of the controller, and the source {@link Path} of the + * controller. This can be created using + * {@link #createIdentifier(JavaType, Path)}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class SolrJspMetadata extends AbstractMetadataItem { + + private static final String PROVIDES_TYPE_STRING = SolrJspMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final SolrWebSearchMetadata solrWebSearchMetadata; + + public SolrJspMetadata(final String identifier, + final SolrWebSearchMetadata solrWebSearchMetadata) { + super(identifier); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(solrWebSearchMetadata, + "Solr web search metadata required"); + this.solrWebSearchMetadata = solrWebSearchMetadata; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("solr jsp scaffold metadata id", + solrWebSearchMetadata.getId()); + return builder.toString(); + } +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrJspMetadataListener.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrJspMetadataListener.java new file mode 100644 index 000000000..199b76870 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrJspMetadataListener.java @@ -0,0 +1,614 @@ +package org.springframework.roo.addon.solr; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.logging.Logger; + +import javax.xml.parsers.DocumentBuilder; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadata; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations; +import org.springframework.roo.addon.web.mvc.jsp.roundtrip.XmlRoundTripFileManager; +import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperations; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Metadata listener responsible for installing Web MVC JSP artifacts for the + * Solr search addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SolrJspMetadataListener implements MetadataProvider, + MetadataNotificationListener { + + protected final static Logger LOGGER = HandlerUtils.getLogger(SolrJspMetadataListener.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private FileManager fileManager; + private JavaType formbackingObject; + private JavaType javaType; + private JpaActiveRecordMetadata jpaActiveRecordMetadata; + private MemberDetailsScanner memberDetailsScanner; + private MenuOperations menuOperations; + private MetadataDependencyRegistry metadataDependencyRegistry; + private MetadataService metadataService; + private PathResolver pathResolver; + private PersistenceMemberLocator persistenceMemberLocator; + + private TilesOperations tilesOperations; + private TypeLocationService typeLocationService; + private WebScaffoldMetadata webScaffoldMetadata; + private XmlRoundTripFileManager xmlRoundTripFileManager; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + SolrWebSearchMetadata.getMetadataIdentiferType(), + getProvidesType()); + } + + private void copyArtifacts(final String relativeTemplateLocation, + final String relativeProjectFileLocation) { + // First install search.tagx + final String projectFileLocation = getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, relativeProjectFileLocation); + if (!getFileManager().exists(projectFileLocation)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + relativeTemplateLocation); + outputStream = getFileManager().createFile(projectFileLocation) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Could not copy " + + relativeProjectFileLocation + " into project", e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + public MetadataItem get(final String metadataIdentificationString) { + javaType = SolrJspMetadata.getJavaType(metadataIdentificationString); + final LogicalPath path = SolrJspMetadata + .getPath(metadataIdentificationString); + final String solrWebSearchMetadataKeyString = SolrWebSearchMetadata + .createIdentifier(javaType, path); + final SolrWebSearchMetadata webSearchMetadata = (SolrWebSearchMetadata) getMetadataService() + .get(solrWebSearchMetadataKeyString); + if (webSearchMetadata == null || !webSearchMetadata.isValid()) { + return null; + } + + webScaffoldMetadata = (WebScaffoldMetadata) getMetadataService() + .get(WebScaffoldMetadata.createIdentifier(javaType, path)); + Validate.notNull(webScaffoldMetadata, "Web scaffold metadata required"); + + formbackingObject = webScaffoldMetadata.getAnnotationValues() + .getFormBackingObject(); + jpaActiveRecordMetadata = (JpaActiveRecordMetadata) getMetadataService() + .get(JpaActiveRecordMetadata.createIdentifier( + formbackingObject, path)); + Validate.notNull(jpaActiveRecordMetadata, + "Could not determine entity metadata for type: %s", + javaType.getFullyQualifiedTypeName()); + + installMvcArtifacts(webScaffoldMetadata); + + return new SolrJspMetadata(metadataIdentificationString, + webSearchMetadata); + } + + public String getProvidesType() { + return SolrJspMetadata.getMetadataIdentiferType(); + } + + private Document getSearchDocument( + final WebScaffoldMetadata webScaffoldMetadata) { + // Next install search.jspx + Validate.notNull(webScaffoldMetadata, "Web scaffold metadata required"); + + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + + // Add document namespaces + final Element div = new XmlElementBuilder("div", document) + .addAttribute("xmlns:page", "urn:jsptagdir:/WEB-INF/tags/form") + .addAttribute("xmlns:fields", + "urn:jsptagdir:/WEB-INF/tags/form/fields") + .addAttribute("xmlns:jsp", "http://java.sun.com/JSP/Page") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", "yes") + .build()).build(); + document.appendChild(div); + + final Element pageSearch = new XmlElementBuilder("page:search", + document) + .addAttribute( + "id", + XmlUtils.convertId("ps:" + + webScaffoldMetadata.getAnnotationValues() + .getFormBackingObject() + .getFullyQualifiedTypeName())) + .addAttribute("path", + webScaffoldMetadata.getAnnotationValues().getPath()) + .build(); + pageSearch.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(pageSearch)); + + final List idFields = getPersistenceMemberLocator() + .getIdentifierFields(formbackingObject); + if (idFields.isEmpty()) { + return null; + } + final Element resultTable = new XmlElementBuilder("fields:table", + document) + .addAttribute( + "id", + XmlUtils.convertId("rt:" + + webScaffoldMetadata.getAnnotationValues() + .getFormBackingObject() + .getFullyQualifiedTypeName())) + .addAttribute("data", "${searchResults}") + .addAttribute("delete", "false") + .addAttribute("update", "false") + .addAttribute("path", + webScaffoldMetadata.getAnnotationValues().getPath()) + .addAttribute( + "typeIdFieldName", + formbackingObject.getSimpleTypeName().toLowerCase() + + "." + + idFields.get(0).getFieldName() + .getSymbolName().toLowerCase() + + SolrUtils.getSolrDynamicFieldPostFix(idFields + .get(0).getFieldType())).build(); + resultTable.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(resultTable)); + + final StringBuilder facetFields = new StringBuilder(); + int fieldCounter = 0; + + final ClassOrInterfaceTypeDetails formbackingClassOrInterfaceDetails = getTypeLocationService() + .getTypeDetails(formbackingObject); + Validate.notNull(formbackingClassOrInterfaceDetails, + "Unable to obtain physical type metadata for type %s", + formbackingObject.getFullyQualifiedTypeName()); + final MemberDetails memberDetails = getMemberDetailsScanner() + .getMemberDetails(getClass().getName(), + formbackingClassOrInterfaceDetails); + final MethodMetadata identifierAccessor = getPersistenceMemberLocator() + .getIdentifierAccessor(formbackingObject); + final MethodMetadata versionAccessor = getPersistenceMemberLocator() + .getVersionAccessor(formbackingObject); + + for (final MethodMetadata method : memberDetails.getMethods()) { + // Only interested in accessors + if (!BeanInfoUtils.isAccessorMethod(method)) { + continue; + } + if (++fieldCounter < 7) { + if (method.getMethodName().equals( + identifierAccessor.getMethodName()) + || method.getMethodName().equals( + versionAccessor.getMethodName())) { + continue; + } + if (method.hasSameName(identifierAccessor, versionAccessor)) { + continue; + } + + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field == null) { + continue; + } + + facetFields + .append(formbackingObject.getSimpleTypeName() + .toLowerCase()) + .append(".") + .append(field.getFieldName()) + .append(SolrUtils.getSolrDynamicFieldPostFix(field + .getFieldType())).append(","); + + final Element columnElement = new XmlElementBuilder( + "fields:column", document) + .addAttribute( + "id", + XmlUtils.convertId("c:" + + formbackingObject + .getFullyQualifiedTypeName() + + "." + + field.getFieldName().getSymbolName())) + .addAttribute( + "property", + formbackingObject.getSimpleTypeName() + .toLowerCase() + + "." + + field.getFieldName().getSymbolName() + .toLowerCase() + + SolrUtils + .getSolrDynamicFieldPostFix(field + .getFieldType())) + .build(); + columnElement.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(columnElement)); + resultTable.appendChild(columnElement); + } + } + + final Element searchFacet = new XmlElementBuilder( + "fields:search-facet", document) + .addAttribute( + "id", + XmlUtils.convertId("sfacet:" + + webScaffoldMetadata.getAnnotationValues() + .getFormBackingObject() + .getFullyQualifiedTypeName())) + .addAttribute("facetFields", facetFields.toString()).build(); + searchFacet.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(searchFacet)); + pageSearch.appendChild(searchFacet); + + final Element searchField = new XmlElementBuilder( + "fields:search-field", document).addAttribute( + "id", + XmlUtils.convertId("sfield:" + + webScaffoldMetadata.getAnnotationValues() + .getFormBackingObject() + .getFullyQualifiedTypeName())).build(); + searchField.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(searchField)); + + pageSearch.appendChild(searchFacet); + pageSearch.appendChild(searchField); + pageSearch.appendChild(resultTable); + + div.appendChild(pageSearch); + + return document; + } + + public void installMvcArtifacts( + final WebScaffoldMetadata webScaffoldMetadata) { + copyArtifacts("form/search.tagx", "WEB-INF/tags/form/search.tagx"); + copyArtifacts("form/fields/search-facet.tagx", + "WEB-INF/tags/form/fields/search-facet.tagx"); + copyArtifacts("form/fields/search-field.tagx", + "WEB-INF/tags/form/fields/search-field.tagx"); + + final LogicalPath path = WebScaffoldMetadata + .getPath(webScaffoldMetadata.getId()); + getXmlRoundTripFileManager().writeToDiskIfNecessary(getPathResolver() + .getIdentifier( + Path.SRC_MAIN_WEBAPP.getModulePathId(path.getModule()), + "WEB-INF/views/" + + webScaffoldMetadata.getAnnotationValues() + .getPath() + "/search.jspx"), + getSearchDocument(webScaffoldMetadata)); + + final String folderName = webScaffoldMetadata.getAnnotationValues() + .getPath(); + getTilesOperations().addViewDefinition(folderName, path, folderName + + "/search", TilesOperations.DEFAULT_TEMPLATE, "WEB-INF/views/" + + webScaffoldMetadata.getAnnotationValues().getPath() + + "/search.jspx"); + getMenuOperations().addMenuItem( + new JavaSymbolName(formbackingObject.getSimpleTypeName()), + new JavaSymbolName("solr"), new JavaSymbolName( + jpaActiveRecordMetadata.getPlural()) + .getReadableSymbolName(), "global.menu.find", "/" + + webScaffoldMetadata.getAnnotationValues().getPath() + + "?search", "s:", path); + } + + public void notify(final String upstreamDependency, + String downstreamDependency) { + if (MetadataIdentificationUtils + .isIdentifyingClass(downstreamDependency)) { + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + upstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(SolrWebSearchMetadata + .getMetadataIdentiferType())), + "Expected class-level notifications only for Solr web search metadata (not '%s')", + upstreamDependency); + + // A physical Java type has changed, and determine what the + // corresponding local metadata identification string would have + // been + final JavaType javaType = SolrWebSearchMetadata + .getJavaType(upstreamDependency); + final LogicalPath path = SolrWebSearchMetadata + .getPath(upstreamDependency); + downstreamDependency = SolrJspMetadata.createIdentifier(javaType, + path); + + // We only need to proceed if the downstream dependency relationship + // is not already registered + // (if it's already registered, the event will be delivered directly + // later on) + if (getMetadataDependencyRegistry().getDownstream(upstreamDependency) + .contains(downstreamDependency)) { + return; + } + } + + // We should now have an instance-specific "downstream dependency" that + // can be processed by this class + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + downstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Unexpected downstream notification for '%s' to this provider (which uses '%s')", + downstreamDependency, getProvidesType()); + + getMetadataService().evict(downstreamDependency); + if (get(downstreamDependency) != null) { + getMetadataDependencyRegistry().notifyDownstream(downstreamDependency); + } + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on SolrJspMetadataListener."); + return null; + } + }else{ + return fileManager; + } + } + + public MemberDetailsScanner getMemberDetailsScanner(){ + if(memberDetailsScanner == null){ + // Get all Services implement MemberDetailsScanner interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MemberDetailsScanner.class.getName(), null); + + for(ServiceReference ref : references){ + return (MemberDetailsScanner) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MemberDetailsScanner on SolrJspMetadataListener."); + return null; + } + }else{ + return memberDetailsScanner; + } + } + + public MenuOperations getMenuOperations(){ + if(menuOperations == null){ + // Get all Services implement MenuOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MenuOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (MenuOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MenuOperations on SolrJspMetadataListener."); + return null; + } + }else{ + return menuOperations; + } + } + + public MetadataDependencyRegistry getMetadataDependencyRegistry(){ + if(metadataDependencyRegistry == null){ + // Get all Services implement MetadataDependencyRegistry interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataDependencyRegistry.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataDependencyRegistry) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataDependencyRegistry on SolrJspMetadataListener."); + return null; + } + }else{ + return metadataDependencyRegistry; + } + } + + public MetadataService getMetadataService(){ + if(metadataService == null){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on SolrJspMetadataListener."); + return null; + } + }else{ + return metadataService; + } + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on SolrJspMetadataListener."); + return null; + } + }else{ + return pathResolver; + } + } + + public PersistenceMemberLocator getPersistenceMemberLocator(){ + if(persistenceMemberLocator == null){ + // Get all Services implement PersistenceMemberLocator interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PersistenceMemberLocator.class.getName(), null); + + for(ServiceReference ref : references){ + return (PersistenceMemberLocator) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PersistenceMemberLocator on SolrJspMetadataListener."); + return null; + } + }else{ + return persistenceMemberLocator; + } + } + + public TilesOperations getTilesOperations(){ + if(tilesOperations == null){ + // Get all Services implement TilesOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TilesOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (TilesOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TilesOperations on SolrJspMetadataListener."); + return null; + } + }else{ + return tilesOperations; + } + } + + public TypeLocationService getTypeLocationService(){ + if(typeLocationService == null){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on SolrJspMetadataListener."); + return null; + } + }else{ + return typeLocationService; + } + } + + public XmlRoundTripFileManager getXmlRoundTripFileManager(){ + if(xmlRoundTripFileManager == null){ + // Get all Services implement XmlRoundTripFileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(XmlRoundTripFileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (XmlRoundTripFileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load XmlRoundTripFileManager on SolrJspMetadataListener."); + return null; + } + }else{ + return xmlRoundTripFileManager; + } + } +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrMetadata.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrMetadata.java new file mode 100644 index 000000000..162f91c19 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrMetadata.java @@ -0,0 +1,493 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.COLLECTION; +import static org.springframework.roo.model.JpaJavaType.POST_PERSIST; +import static org.springframework.roo.model.JpaJavaType.POST_UPDATE; +import static org.springframework.roo.model.JpaJavaType.PRE_REMOVE; +import static org.springframework.roo.model.SpringJavaType.ASYNC; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooSolrSearchable}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class SolrMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = SolrMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final JavaType SOLR_INPUT_DOCUMENT = new JavaType( + "org.apache.solr.common.SolrInputDocument"); + private static final JavaType SOLR_QUERY = new JavaType( + "org.apache.solr.client.solrj.SolrQuery"); + + private static final JavaType SOLR_QUERY_RESPONSE = new JavaType( + "org.apache.solr.client.solrj.response.QueryResponse"); + private static final JavaType SOLR_SERVER = new JavaType( + "org.apache.solr.client.solrj.SolrServer"); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private SolrSearchAnnotationValues annotationValues; + private String beanPlural; + private String javaBeanFieldName; + + public SolrMetadata(final String identifier, final JavaType aspectName, + final SolrSearchAnnotationValues annotationValues, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final MethodMetadata identifierAccessor, + final FieldMetadata versionField, + final Map accessorDetails, + final String javaTypePlural) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(annotationValues, + "Solr search annotation values required"); + Validate.isTrue(isValid(identifier), + "Metadata identification string '%s' is invalid", identifier); + Validate.notNull(identifierAccessor, + "Persistence identifier method required"); + Validate.notNull(accessorDetails, "Public accessors requred"); + Validate.notBlank(javaTypePlural, + "Plural representation of java type required"); + + if (!isValid()) { + return; + } + + javaBeanFieldName = JavaSymbolName.getReservedWordSafeName(destination) + .getSymbolName(); + this.annotationValues = annotationValues; + beanPlural = javaTypePlural; + + if (Modifier.isAbstract(governorTypeDetails.getModifier())) { + valid = false; + return; + } + + builder.addField(getSolrServerField()); + if (StringUtils.isNotBlank(annotationValues.getSimpleSearchMethod())) { + builder.addMethod(getSimpleSearchMethod()); + } + if (StringUtils.isNotBlank(annotationValues.getSearchMethod())) { + builder.addMethod(getSearchMethod()); + } + if (StringUtils.isNotBlank(annotationValues.getIndexMethod())) { + builder.addMethod(getIndexEntityMethod()); + builder.addMethod(getIndexEntitiesMethod(accessorDetails, + identifierAccessor, versionField)); + } + if (StringUtils.isNotBlank(annotationValues.getDeleteIndexMethod())) { + builder.addMethod(getDeleteIndexMethod(identifierAccessor)); + } + if (StringUtils.isNotBlank(annotationValues + .getPostPersistOrUpdateMethod())) { + builder.addMethod(getPostPersistOrUpdateMethod()); + } + if (StringUtils.isNotBlank(annotationValues.getPreRemoveMethod())) { + builder.addMethod(getPreRemoveMethod()); + } + + builder.addMethod(getSolrServerMethod()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + public SolrSearchAnnotationValues getAnnotationValues() { + return annotationValues; + } + + private MethodMetadataBuilder getDeleteIndexMethod( + final MethodMetadata identifierAccessor) { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getDeleteIndexMethod()); + final JavaType parameterType = destination; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final List parameterNames = Arrays + .asList(new JavaSymbolName(javaBeanFieldName)); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(getSimpleName(SOLR_SERVER) + + " solrServer = solrServer();"); + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("solrServer.deleteById(\"" + + destination.getSimpleTypeName().toLowerCase() + "_\" + " + + javaBeanFieldName + "." + + identifierAccessor.getMethodName().getSymbolName() + "());"); + bodyBuilder.appendFormalLine("solrServer.commit();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} catch (Exception e) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("e.printStackTrace();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + methodBuilder.addAnnotation(new AnnotationMetadataBuilder(ASYNC)); + return methodBuilder; + } + + private MethodMetadataBuilder getIndexEntitiesMethod( + final Map accessorDetails, + final MethodMetadata identifierAccessor, + final FieldMetadata versionField) { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getIndexMethod() + beanPlural); + final JavaType parameterType = new JavaType( + COLLECTION.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, + Arrays.asList(destination)); + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final List parameterNames = Arrays + .asList(new JavaSymbolName(beanPlural.toLowerCase())); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String sid = getSimpleName(SOLR_INPUT_DOCUMENT); + final List sidTypeParams = Arrays.asList(SOLR_INPUT_DOCUMENT); + + String listVar = "documents"; + if (listVar.equals(beanPlural.toLowerCase())) { + listVar += "_"; + } + bodyBuilder.appendFormalLine(getSimpleName(new JavaType(List.class + .getName(), 0, DataType.TYPE, null, sidTypeParams)) + + " " + + listVar + + " = new " + + getSimpleName(new JavaType(ArrayList.class.getName(), 0, + DataType.TYPE, null, sidTypeParams)) + "();"); + bodyBuilder.appendFormalLine("for (" + destination.getSimpleTypeName() + + " " + javaBeanFieldName + " : " + beanPlural.toLowerCase() + + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(sid + " sid = new " + sid + "();"); + bodyBuilder.appendFormalLine("sid.addField(\"id\", \"" + + destination.getSimpleTypeName().toLowerCase() + "_\" + " + + javaBeanFieldName + "." + identifierAccessor.getMethodName() + + "());"); + final StringBuilder textField = new StringBuilder("new StringBuilder()"); + + for (final Entry entry : accessorDetails + .entrySet()) { + final FieldMetadata field = entry.getValue(); + if (versionField != null + && field.getFieldName().equals(versionField.getFieldName())) { + continue; + } + if (field.getFieldType().isCommonCollectionType()) { + continue; + } + if (!textField.toString().endsWith("StringBuilder()")) { + textField.append(".append(\" \")"); + } + final JavaSymbolName accessorMethod = entry.getKey() + .getMethodName(); + if (field.getFieldType().equals(CALENDAR)) { + textField.append(".append(").append(javaBeanFieldName) + .append(".").append(accessorMethod) + .append("().getTime()").append(")"); + } + else { + textField.append(".append(").append(javaBeanFieldName) + .append(".").append(accessorMethod).append("()") + .append(")"); + } + String fieldName = javaBeanFieldName + + "." + + field.getFieldName().getSymbolName().toLowerCase() + + SolrUtils + .getSolrDynamicFieldPostFix(field.getFieldType()); + for (final AnnotationMetadata annotation : field.getAnnotations()) { + if (annotation.getAnnotationType() + .equals(new JavaType( + "org.apache.solr.client.solrj.beans.Field"))) { + final AnnotationAttributeValue value = annotation + .getAttribute(new JavaSymbolName("value")); + if (value != null) { + fieldName = value.getValue().toString(); + } + } + } + if (field.getFieldType().equals(CALENDAR)) { + bodyBuilder.appendFormalLine("sid.addField(\"" + fieldName + + "\", " + javaBeanFieldName + "." + + accessorMethod.getSymbolName() + "().getTime());"); + } + else { + bodyBuilder.appendFormalLine("sid.addField(\"" + fieldName + + "\", " + javaBeanFieldName + "." + + accessorMethod.getSymbolName() + "());"); + } + } + bodyBuilder + .appendFormalLine("// Add summary field to allow searching documents for objects of this type"); + bodyBuilder.appendFormalLine("sid.addField(\"" + + destination.getSimpleTypeName().toLowerCase() + + "_solrsummary_t\", " + textField.toString() + ");"); + bodyBuilder.appendFormalLine(listVar + ".add(sid);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(getSimpleName(SOLR_SERVER) + + " solrServer = solrServer();"); + bodyBuilder.appendFormalLine("solrServer.add(" + listVar + ");"); + bodyBuilder.appendFormalLine("solrServer.commit();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} catch (Exception e) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("e.printStackTrace();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, methodName, + JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + methodBuilder.addAnnotation(new AnnotationMetadataBuilder(ASYNC)); + return methodBuilder; + } + + private MethodMetadataBuilder getIndexEntityMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getIndexMethod() + + destination.getSimpleTypeName()); + final JavaType parameterType = destination; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final JavaType listType = JavaType.getInstance(List.class.getName(), 0, + DataType.TYPE, null, parameterType); + final JavaType arrayListType = JavaType.getInstance( + ArrayList.class.getName(), 0, DataType.TYPE, null, + parameterType); + bodyBuilder.appendFormalLine(getSimpleName(listType) + " " + + beanPlural.toLowerCase() + " = new " + + getSimpleName(arrayListType) + "();"); + bodyBuilder.appendFormalLine(beanPlural.toLowerCase() + ".add(" + + javaBeanFieldName + ");"); + bodyBuilder.appendFormalLine(annotationValues.getIndexMethod() + + beanPlural + "(" + beanPlural.toLowerCase() + ");"); + + final List parameterNames = Arrays + .asList(new JavaSymbolName(javaBeanFieldName)); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC + | Modifier.STATIC, methodName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getPostPersistOrUpdateMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getPostPersistOrUpdateMethod()); + if (governorHasMethod(methodName)) { + return null; + } + + final List annotations = Arrays.asList( + new AnnotationMetadataBuilder(POST_UPDATE), + new AnnotationMetadataBuilder(POST_PERSIST)); + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(annotationValues.getIndexMethod() + + destination.getSimpleTypeName() + "(this);"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PRIVATE, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getPreRemoveMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getPreRemoveMethod()); + if (governorHasMethod(methodName)) { + return null; + } + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(PRE_REMOVE)); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(annotationValues.getDeleteIndexMethod() + + "(this);"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PRIVATE, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getSearchMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getSearchMethod()); + final JavaType parameterType = SOLR_QUERY; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final JavaType queryResponse = SOLR_QUERY_RESPONSE; + final List parameterNames = Arrays + .asList(new JavaSymbolName("query")); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return solrServer().query(query);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} catch (Exception e) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("e.printStackTrace();"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return new " + + getSimpleName(queryResponse) + "();"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC + | Modifier.STATIC, methodName, queryResponse, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private String getSimpleName(final JavaType type) { + return type.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + } + + private MethodMetadataBuilder getSimpleSearchMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getSimpleSearchMethod()); + final JavaType parameterType = JavaType.STRING; + if (governorHasMethod(methodName, parameterType)) { + return null; + } + + final JavaType queryResponse = SOLR_QUERY_RESPONSE; + final List parameterNames = Arrays + .asList(new JavaSymbolName("queryString")); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("String searchString = \"" + + destination.getSimpleTypeName() + + "_solrsummary_t:\" + queryString;"); + bodyBuilder + .appendFormalLine("return search(new SolrQuery(searchString.toLowerCase()));"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC + | Modifier.STATIC, methodName, queryResponse, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private FieldMetadataBuilder getSolrServerField() { + final JavaSymbolName fieldName = new JavaSymbolName("solrServer"); + if (governorTypeDetails.getDeclaredField(fieldName) != null) { + return null; + } + + return new FieldMetadataBuilder(getId(), Modifier.TRANSIENT, + Arrays.asList(new AnnotationMetadataBuilder(AUTOWIRED)), + fieldName, SOLR_SERVER); + } + + private MethodMetadataBuilder getSolrServerMethod() { + final JavaSymbolName methodName = new JavaSymbolName("solrServer"); + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType returnType = SOLR_SERVER; + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(getSimpleName(returnType) + + " _solrServer = new " + destination.getSimpleTypeName() + + "().solrServer;"); + bodyBuilder + .appendFormalLine("if (_solrServer == null) throw new IllegalStateException(\"Solr server has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)\");"); + bodyBuilder.appendFormalLine("return _solrServer;"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC + | Modifier.STATIC, methodName, returnType, bodyBuilder); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrMetadataProvider.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrMetadataProvider.java new file mode 100644 index 000000000..7fb2e7e8f --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrMetadataProvider.java @@ -0,0 +1,212 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.model.RooJavaType.ROO_SOLR_SEARCHABLE; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadata; +import org.springframework.roo.addon.jpa.activerecord.JpaActiveRecordMetadataProvider; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Provides {@link SolrMetadata}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SolrMetadataProvider extends + AbstractMemberDiscoveringItdMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(SolrMetadataProvider.class); + + private JpaActiveRecordMetadataProvider jpaActiveRecordMetadataProvider; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getJpaActiveRecordMetadataProvider().addMetadataTrigger(ROO_SOLR_SEARCHABLE); + addMetadataTrigger(ROO_SOLR_SEARCHABLE); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return SolrMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getJpaActiveRecordMetadataProvider() + .removeMetadataTrigger(ROO_SOLR_SEARCHABLE); + removeMetadataTrigger(ROO_SOLR_SEARCHABLE); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = SolrMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = SolrMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "SolrSearch"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + // Determine if this ITD presents a method we're interested in (namely + // accessors) + for (final MethodMetadata method : itdTypeDetails.getDeclaredMethods()) { + if (BeanInfoUtils.isAccessorMethod(method) + && !method.getMethodName().getSymbolName().startsWith("is")) { + // We care about this ITD, so formally request an update so we + // can scan for it and process it + + // Determine the governor for this ITD, and the Path the ITD is + // stored within + final JavaType governorType = itdTypeDetails.getName(); + final String providesType = MetadataIdentificationUtils + .getMetadataClass(itdTypeDetails + .getDeclaredByMetadataId()); + final LogicalPath itdPath = PhysicalTypeIdentifierNamingUtils + .getPath(providesType, + itdTypeDetails.getDeclaredByMetadataId()); + + // Produce the local MID we're going to use and make the request + return createLocalIdentifier(governorType, itdPath); + } + } + + return null; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // We need to parse the annotation, which we expect to be present + final SolrSearchAnnotationValues annotationValues = new SolrSearchAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound() + || annotationValues.searchMethod == null) { + return null; + } + + // Acquire bean info (we need getters details, specifically) + final JavaType javaType = SolrMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = SolrMetadata + .getPath(metadataIdentificationString); + final String jpaActiveRecordMetadataKey = JpaActiveRecordMetadata + .createIdentifier(javaType, path); + + // We want to be notified if the getter info changes in any way + getMetadataDependencyRegistry().registerDependency( + jpaActiveRecordMetadataKey, metadataIdentificationString); + final JpaActiveRecordMetadata jpaActiveRecordMetadata = (JpaActiveRecordMetadata) metadataService + .get(jpaActiveRecordMetadataKey); + + // Abort if we don't have getter information available + if (jpaActiveRecordMetadata == null + || !jpaActiveRecordMetadata.isValid()) { + return null; + } + + // Otherwise go off and create the Solr metadata + String beanPlural = javaType.getSimpleTypeName() + "s"; + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(PluralMetadata.createIdentifier(javaType, path)); + if (pluralMetadata != null && pluralMetadata.isValid()) { + beanPlural = pluralMetadata.getPlural(); + } + + final MemberDetails memberDetails = getMemberDetails(governorPhysicalTypeMetadata); + final Map accessorDetails = new LinkedHashMap(); + for (final MethodMetadata method : memberDetails.getMethods()) { + if (BeanInfoUtils.isAccessorMethod(method) + && !method.getMethodName().getSymbolName().startsWith("is")) { + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field != null) { + accessorDetails.put(method, field); + } + // Track any changes to that method (eg it goes away) + getMetadataDependencyRegistry().registerDependency( + method.getDeclaredByMetadataId(), + metadataIdentificationString); + } + } + final MethodMetadata identifierAccessor = persistenceMemberLocator + .getIdentifierAccessor(javaType); + if (identifierAccessor == null) { + return null; + } + + final FieldMetadata versionField = persistenceMemberLocator + .getVersionField(javaType); + + return new SolrMetadata(metadataIdentificationString, aspectName, + annotationValues, governorPhysicalTypeMetadata, + identifierAccessor, versionField, accessorDetails, beanPlural); + } + + public String getProvidesType() { + return SolrMetadata.getMetadataIdentiferType(); + } + + public JpaActiveRecordMetadataProvider getJpaActiveRecordMetadataProvider(){ + if(jpaActiveRecordMetadataProvider == null){ + // Get all Services implement JpaActiveRecordMetadataProvider interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(JpaActiveRecordMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (JpaActiveRecordMetadataProvider) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load JpaActiveRecordMetadataProvider on SolrMetadataProvider."); + return null; + } + }else{ + return jpaActiveRecordMetadataProvider; + } + } +} \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrOperations.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrOperations.java new file mode 100644 index 000000000..94b27b78d --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrOperations.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.solr; + +import org.springframework.roo.model.JavaType; + +/** + * Provides Solr Search configuration operations. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface SolrOperations { + + void addAll(); + + void addSearch(JavaType javaType); + + boolean isSearchAvailable(); + + boolean isSolrInstallationPossible(); + + void setupConfig(String solrServerUrl); +} \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrOperationsImpl.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrOperationsImpl.java new file mode 100644 index 000000000..2890cbd95 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrOperationsImpl.java @@ -0,0 +1,206 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.model.RooJavaType.ROO_SOLR_SEARCHABLE; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Provides Search configuration operations. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SolrOperationsImpl implements SolrOperations { + + @Reference private FileManager fileManager; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void addAll() { + final Set cids = typeLocationService + .findClassesOrInterfaceDetailsWithTag(CustomDataKeys.PERSISTENT_TYPE); + for (final ClassOrInterfaceTypeDetails cid : cids) { + if (!Modifier.isAbstract(cid.getModifier())) { + addSolrSearchableAnnotation(cid); + } + } + } + + public void addSearch(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(javaType); + if (cid == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + javaType.getFullyQualifiedTypeName() + "'"); + } + + if (Modifier.isAbstract(cid.getModifier())) { + throw new IllegalStateException( + "The class specified is an abstract type. Can only add solr search for concrete types."); + } + addSolrSearchableAnnotation(cid); + } + + private void addSolrSearchableAnnotation( + final ClassOrInterfaceTypeDetails cid) { + if (cid.getTypeAnnotation(ROO_SOLR_SEARCHABLE) == null) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + ROO_SOLR_SEARCHABLE)); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + } + + public boolean isSearchAvailable() { + return solrPropsInstalled(); + } + + public boolean isSolrInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && !solrPropsInstalled() + && projectOperations.isFeatureInstalled(FeatureNames.JPA); + } + + public void setupConfig(final String solrServerUrl) { + updateConfiguration(projectOperations.getFocusedModuleName()); + updateSolrProperties(solrServerUrl); + + final String contextPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "applicationContext.xml"); + final Document appCtx = XmlUtils.readXml(fileManager + .getInputStream(contextPath)); + final Element root = appCtx.getDocumentElement(); + + if (DomUtils.findFirstElementByName("task:annotation-driven", root) == null) { + if (root.getAttribute("xmlns:task").length() == 0) { + root.setAttribute("xmlns:task", + "http://www.springframework.org/schema/task"); + root.setAttribute( + "xsi:schemaLocation", + root.getAttribute("xsi:schemaLocation") + + " http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"); + } + root.appendChild(new XmlElementBuilder("task:annotation-driven", + appCtx).addAttribute("executor", "asyncExecutor") + .addAttribute("mode", "aspectj").build()); + root.appendChild(new XmlElementBuilder("task:executor", appCtx) + .addAttribute("id", "asyncExecutor") + .addAttribute("pool-size", "${executor.poolSize}").build()); + } + + final Element solrServer = XmlUtils.findFirstElement( + "/beans/bean[@id='solrServer']", root); + if (solrServer != null) { + return; + } + + root.appendChild(new XmlElementBuilder("bean", appCtx) + .addAttribute("id", "solrServer") + .addAttribute("class", + "org.apache.solr.client.solrj.impl.CommonsHttpSolrServer") + .addChild( + new XmlElementBuilder("constructor-arg", appCtx) + .addAttribute("value", "${solr.serverUrl}") + .build()).build()); + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(contextPath, + XmlUtils.nodeToString(appCtx), false); + } + + private boolean solrPropsInstalled() { + return fileManager.exists(projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "solr.properties")); + } + + private void updateSolrProperties(final String solrServerUrl) { + final String solrPath = projectOperations.getPathResolver() + .getFocusedIdentifier(Path.SPRING_CONFIG_ROOT, + "solr.properties"); + final boolean solrExists = fileManager.exists(solrPath); + + final Properties props = new Properties(); + InputStream inputStream = null; + try { + if (fileManager.exists(solrPath)) { + inputStream = fileManager.getInputStream(solrPath); + props.load(inputStream); + } + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + finally { + IOUtils.closeQuietly(inputStream); + } + + props.put("solr.serverUrl", solrServerUrl); + props.put("executor.poolSize", "10"); + + OutputStream outputStream = null; + try { + final MutableFile mutableFile = solrExists ? fileManager + .updateFile(solrPath) : fileManager.createFile(solrPath); + outputStream = mutableFile.getOutputStream(); + props.store(outputStream, "Updated at " + new Date()); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + + private void updateConfiguration(final String moduleName) { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List emailDependencies = XmlUtils.findElements( + "/configuration/solr/dependencies/dependency", configuration); + for (final Element dependencyElement : emailDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + projectOperations.addDependencies(moduleName, dependencies); + } +} \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrSearchAnnotationValues.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrSearchAnnotationValues.java new file mode 100644 index 000000000..14365ff69 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrSearchAnnotationValues.java @@ -0,0 +1,58 @@ +package org.springframework.roo.addon.solr; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooSolrSearchable} annotation. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class SolrSearchAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate String deleteIndexMethod = "deleteIndex"; + @AutoPopulate String indexMethod = "index"; + @AutoPopulate String postPersistOrUpdateMethod = "postPersistOrUpdate"; + @AutoPopulate String preRemoveMethod = "preRemove"; + @AutoPopulate String searchMethod = "search"; + @AutoPopulate String simpleSearchMethod = "search"; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public SolrSearchAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_SOLR_SEARCHABLE); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getDeleteIndexMethod() { + return deleteIndexMethod; + } + + public String getIndexMethod() { + return indexMethod; + } + + public String getPostPersistOrUpdateMethod() { + return postPersistOrUpdateMethod; + } + + public String getPreRemoveMethod() { + return preRemoveMethod; + } + + public String getSearchMethod() { + return searchMethod; + } + + public String getSimpleSearchMethod() { + return simpleSearchMethod; + } +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrUtils.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrUtils.java new file mode 100644 index 000000000..ce72e4633 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrUtils.java @@ -0,0 +1,55 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.model.JavaType.BOOLEAN_OBJECT; +import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; +import static org.springframework.roo.model.JavaType.DOUBLE_OBJECT; +import static org.springframework.roo.model.JavaType.DOUBLE_PRIMITIVE; +import static org.springframework.roo.model.JavaType.FLOAT_OBJECT; +import static org.springframework.roo.model.JavaType.FLOAT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JavaType.LONG_PRIMITIVE; +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.DATE; + +import org.springframework.roo.model.JavaType; + +/** + * Utils class for solr addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public final class SolrUtils { + + public static String getSolrDynamicFieldPostFix(final JavaType type) { + if (type.equals(INT_OBJECT) || type.equals(INT_PRIMITIVE)) { + return "_i"; + } + else if (type.equals(JavaType.STRING)) { + return "_s"; + } + else if (type.equals(LONG_OBJECT) || type.equals(LONG_PRIMITIVE)) { + return "_l"; + } + else if (type.equals(BOOLEAN_OBJECT) || type.equals(BOOLEAN_PRIMITIVE)) { + return "_b"; + } + else if (type.equals(FLOAT_OBJECT) || type.equals(FLOAT_PRIMITIVE)) { + return "_f"; + } + else if (type.equals(DOUBLE_OBJECT) || type.equals(DOUBLE_PRIMITIVE)) { + return "_d"; + } + else if (type.equals(DATE) || type.equals(CALENDAR)) { + return "_dt"; + } + else { + return "_t"; + } + } + + private SolrUtils() { + } +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchAnnotationValues.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchAnnotationValues.java new file mode 100644 index 000000000..acf919132 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchAnnotationValues.java @@ -0,0 +1,38 @@ +package org.springframework.roo.addon.solr; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooSolrWebSearchable} annotation. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class SolrWebSearchAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String autoCompleteMethod = "autoComplete"; + @AutoPopulate private String searchMethod = "search"; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public SolrWebSearchAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_SOLR_WEB_SEARCHABLE); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getAutoCompleteMethod() { + return autoCompleteMethod; + } + + public String getSearchMethod() { + return searchMethod; + } +} diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchMetadata.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchMetadata.java new file mode 100644 index 000000000..1f2af23b5 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchMetadata.java @@ -0,0 +1,308 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.SpringJavaType.MODEL_MAP; +import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING; +import static org.springframework.roo.model.SpringJavaType.REQUEST_PARAM; +import static org.springframework.roo.model.SpringJavaType.RESPONSE_BODY; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooSolrWebSearchable}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class SolrWebSearchMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = SolrWebSearchMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public SolrWebSearchMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final SolrWebSearchAnnotationValues annotationValues, + final WebScaffoldAnnotationValues webScaffoldAnnotationValues, + final SolrSearchAnnotationValues solrSearchAnnotationValues) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(webScaffoldAnnotationValues, + "Web scaffold annotation values required"); + Validate.notNull(annotationValues, + "Solr web searchable annotation values required"); + Validate.notNull(solrSearchAnnotationValues, + "Solr search annotation values required"); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + + if (!isValid()) { + return; + } + + if (annotationValues.getSearchMethod() != null + && annotationValues.getSearchMethod().length() > 0) { + builder.addMethod(getSearchMethod(annotationValues, + solrSearchAnnotationValues, webScaffoldAnnotationValues)); + } + if (annotationValues.getAutoCompleteMethod() != null + && annotationValues.getAutoCompleteMethod().length() > 0) { + builder.addMethod(getAutocompleteMethod(annotationValues, + solrSearchAnnotationValues, webScaffoldAnnotationValues)); + } + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + private MethodMetadataBuilder getAutocompleteMethod( + final SolrWebSearchAnnotationValues solrWebSearchAnnotationValues, + final SolrSearchAnnotationValues searchAnnotationValues, + final WebScaffoldAnnotationValues webScaffoldAnnotationValues) { + final JavaSymbolName methodName = new JavaSymbolName( + solrWebSearchAnnotationValues.getAutoCompleteMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List> reqMapAttributes = new ArrayList>(); + reqMapAttributes.add(new StringAttributeValue(new JavaSymbolName( + "params"), "autocomplete")); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(REQUEST_MAPPING, + reqMapAttributes)); + annotations.add(new AnnotationMetadataBuilder(RESPONSE_BODY)); + + final List parameterTypes = new ArrayList(); + final List parameterNames = new ArrayList(); + + parameterTypes.add(new AnnotatedJavaType(JavaType.STRING, + getRequestParamAnnotation("q", true))); + parameterNames.add(new JavaSymbolName("q")); + + parameterTypes.add(new AnnotatedJavaType(JavaType.STRING, + getRequestParamAnnotation("facetFields", true))); + parameterNames.add(new JavaSymbolName("facetFields")); + + parameterTypes.add(new AnnotatedJavaType(INT_OBJECT, + getRequestParamAnnotation("rows", false))); + parameterNames.add(new JavaSymbolName("rows")); + + final String solrQuerySimpleName = new JavaType( + "org.apache.solr.client.solrj.SolrQuery") + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final String facetFieldSimpleName = new JavaType( + "org.apache.solr.client.solrj.response.FacetField") + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + + final String queryResponseSimpleName = new JavaType( + "org.apache.solr.client.solrj.response.QueryResponse") + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("StringBuilder dojo = new StringBuilder(\"{identifier:'id',label:'label',items:[\");"); + bodyBuilder.appendFormalLine(solrQuerySimpleName + + " solrQuery = new SolrQuery(q.toLowerCase());"); + bodyBuilder + .appendFormalLine("solrQuery.setRows(rows == null ? 10 : rows);"); + bodyBuilder.appendFormalLine("solrQuery.setFacetMinCount(1);"); + bodyBuilder + .appendFormalLine("solrQuery.addFacetField(facetFields.split(\",\"));"); + bodyBuilder.appendFormalLine(queryResponseSimpleName + + " response = " + + webScaffoldAnnotationValues.getFormBackingObject() + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()) + "." + + searchAnnotationValues.getSearchMethod() + "(solrQuery);"); + bodyBuilder.appendFormalLine("for (" + facetFieldSimpleName + + " field: response.getFacetFields()) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("if (response.getResults().get(0) != null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("Object fieldValue = response.getResults().get(0).getFieldValue(field.getName());"); + bodyBuilder.appendFormalLine("if (fieldValue != null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("dojo.append(\"{label:'\").append(fieldValue).append(\" (\").append(field.getValueCount()).append(\")\").append(\"',\").append(\"id:'\").append(field.getName()).append(\"'},\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("dojo.append(\"]}\");"); + bodyBuilder.appendFormalLine("return dojo.toString();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private AnnotationMetadata getRequestParamAnnotation( + final String paramName, final boolean required) { + final List> attributeValue = new ArrayList>(); + if (!required) { + attributeValue.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + } + attributeValue.add(new StringAttributeValue( + new JavaSymbolName("value"), paramName)); + return new AnnotationMetadataBuilder(REQUEST_PARAM, attributeValue) + .build(); + } + + private MethodMetadataBuilder getSearchMethod( + final SolrWebSearchAnnotationValues solrWebSearchAnnotationValues, + final SolrSearchAnnotationValues searchAnnotationValues, + final WebScaffoldAnnotationValues webScaffoldAnnotationValues) { + final JavaType targetObject = webScaffoldAnnotationValues + .getFormBackingObject(); + Validate.notNull(targetObject, + "Could not aquire form backing object for the '%s' controller", + webScaffoldAnnotationValues.getGovernorTypeDetails().getName() + .getFullyQualifiedTypeName()); + + final JavaSymbolName methodName = new JavaSymbolName( + solrWebSearchAnnotationValues.getSearchMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List parameterTypes = new ArrayList(); + final List parameterNames = new ArrayList(); + + parameterTypes.add(new AnnotatedJavaType(new JavaType("String"), + getRequestParamAnnotation("q", false))); + parameterNames.add(new JavaSymbolName("q")); + + parameterTypes.add(new AnnotatedJavaType(new JavaType("String"), + getRequestParamAnnotation("fq", false))); + parameterNames.add(new JavaSymbolName("facetQuery")); + + parameterTypes.add(new AnnotatedJavaType(new JavaType("Integer"), + getRequestParamAnnotation("page", false))); + parameterNames.add(new JavaSymbolName("page")); + + parameterTypes.add(new AnnotatedJavaType(new JavaType("Integer"), + getRequestParamAnnotation("size", false))); + parameterNames.add(new JavaSymbolName("size")); + + parameterTypes.add(new AnnotatedJavaType(MODEL_MAP)); + parameterNames.add(new JavaSymbolName("modelMap")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("params"), "search")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final String solrQuerySimpleName = new JavaType( + "org.apache.solr.client.solrj.SolrQuery") + .getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (q != null && q.length() != 0) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(solrQuerySimpleName + + " solrQuery = new " + + solrQuerySimpleName + + "(\"" + + webScaffoldAnnotationValues.getFormBackingObject() + .getSimpleTypeName().toLowerCase() + + "_solrsummary_t:\" + q.toLowerCase());"); + + bodyBuilder + .appendFormalLine("if (page != null) solrQuery.setStart(page);"); + bodyBuilder + .appendFormalLine("if (size != null) solrQuery.setRows(size);"); + bodyBuilder + .appendFormalLine("modelMap.addAttribute(\"searchResults\", " + + targetObject.getFullyQualifiedTypeName() + "." + + searchAnnotationValues.getSearchMethod() + + "(solrQuery).getResults());"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return \"" + + webScaffoldAnnotationValues.getPath() + "/search\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchMetadataProvider.java b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchMetadataProvider.java new file mode 100644 index 000000000..453433ec9 --- /dev/null +++ b/addon-solr/src/main/java/org/springframework/roo/addon/solr/SolrWebSearchMetadataProvider.java @@ -0,0 +1,167 @@ +package org.springframework.roo.addon.solr; + +import static org.springframework.roo.model.RooJavaType.ROO_SOLR_WEB_SEARCHABLE; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadataProvider; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Provides {@link SolrWebSearchMetadata}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SolrWebSearchMetadataProvider extends AbstractItdMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(SolrWebSearchMetadataProvider.class); + + private WebScaffoldMetadataProvider webScaffoldMetadataProvider; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getWebScaffoldMetadataProvider().addMetadataTrigger(ROO_SOLR_WEB_SEARCHABLE); + addMetadataTrigger(ROO_SOLR_WEB_SEARCHABLE); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return SolrWebSearchMetadata.createIdentifier(javaType, path); + } + + /** + * OSGi bundle deactivation callback + * + * @param context + * @since 1.2.0 + */ + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getWebScaffoldMetadataProvider() + .removeMetadataTrigger(ROO_SOLR_WEB_SEARCHABLE); + removeMetadataTrigger(ROO_SOLR_WEB_SEARCHABLE); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = SolrWebSearchMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = SolrWebSearchMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "SolrWebSearch"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + // We need to parse the annotation, which we expect to be present + final SolrWebSearchAnnotationValues annotationValues = new SolrWebSearchAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound() + || annotationValues.getSearchMethod() == null) { + return null; + } + + // Acquire bean info (we need getters details, specifically) + final JavaType javaType = SolrWebSearchMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = SolrWebSearchMetadata + .getPath(metadataIdentificationString); + final String webScaffoldMetadataKey = WebScaffoldMetadata + .createIdentifier(javaType, path); + + // We want to be notified if the getter info changes in any way + getMetadataDependencyRegistry().registerDependency(webScaffoldMetadataKey, + metadataIdentificationString); + final WebScaffoldMetadata webScaffoldMetadata = (WebScaffoldMetadata) metadataService + .get(webScaffoldMetadataKey); + + // Abort if we don't have getter information available + if (webScaffoldMetadata == null || !webScaffoldMetadata.isValid()) { + return null; + } + + final JavaType targetObject = webScaffoldMetadata.getAnnotationValues() + .getFormBackingObject(); + Validate.notNull( + targetObject, + "Could not acquire form backing object for the '%s' controller", + WebScaffoldMetadata.getJavaType(webScaffoldMetadata.getId()) + .getFullyQualifiedTypeName()); + + final String targetObjectMid = typeLocationService + .getPhysicalTypeIdentifier(targetObject); + final LogicalPath targetObjectPath = PhysicalTypeIdentifier + .getPath(targetObjectMid); + + final SolrMetadata solrMetadata = (SolrMetadata) metadataService + .get(SolrMetadata.createIdentifier(targetObject, + targetObjectPath)); + Validate.notNull(solrMetadata, + "Could not determine SolrMetadata for type '%s'", + targetObject.getFullyQualifiedTypeName()); + + // Otherwise go off and create the to String metadata + return new SolrWebSearchMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, annotationValues, + webScaffoldMetadata.getAnnotationValues(), + solrMetadata.getAnnotationValues()); + } + + public String getProvidesType() { + return SolrWebSearchMetadata.getMetadataIdentiferType(); + } + + public WebScaffoldMetadataProvider getWebScaffoldMetadataProvider(){ + if(webScaffoldMetadataProvider == null){ + // Get all Services implement WebScaffoldMetadataProvider interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(WebScaffoldMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (WebScaffoldMetadataProvider) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load WebScaffoldMetadataProvider on SolrWebSearchMetadataProvider."); + return null; + } + }else{ + return webScaffoldMetadataProvider; + } + } +} \ No newline at end of file diff --git a/addon-solr/src/main/resources/org/springframework/roo/addon/solr/configuration.xml b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/configuration.xml new file mode 100644 index 000000000..b5c826038 --- /dev/null +++ b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/configuration.xml @@ -0,0 +1,12 @@ + + + + + + org.apache.solr + solr-solrj + 3.6.1 + + + + \ No newline at end of file diff --git a/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/fields/search-facet.tagx b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/fields/search-facet.tagx new file mode 100644 index 000000000..da5084eaa --- /dev/null +++ b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/fields/search-facet.tagx @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/fields/search-field.tagx b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/fields/search-field.tagx new file mode 100644 index 000000000..7fc4214aa --- /dev/null +++ b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/fields/search-field.tagx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + +
    + + +
    +
    +
    \ No newline at end of file diff --git a/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/search.tagx b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/search.tagx new file mode 100644 index 000000000..b6d63bc36 --- /dev/null +++ b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/form/search.tagx @@ -0,0 +1,28 @@ + + + + + + + + + + + + ${id} + + + + + + +
    + + + ${title_msg} + + + +
    + +
    \ No newline at end of file diff --git a/addon-solr/src/main/resources/org/springframework/roo/addon/solr/schema-template.xml b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/schema-template.xml new file mode 100644 index 000000000..067105b11 --- /dev/null +++ b/addon-solr/src/main/resources/org/springframework/roo/addon/solr/schema-template.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrSearchAnnotationValuesTest.java b/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrSearchAnnotationValuesTest.java new file mode 100644 index 000000000..7da230127 --- /dev/null +++ b/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrSearchAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.solr; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link SolrSearchAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class SolrSearchAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooSolrSearchable.class; + } + + @Override + protected Class getValuesClass() { + return SolrSearchAnnotationValues.class; + } +} diff --git a/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrUtilsTest.java b/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrUtilsTest.java new file mode 100644 index 000000000..ab6c7ce08 --- /dev/null +++ b/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrUtilsTest.java @@ -0,0 +1,21 @@ +package org.springframework.roo.addon.solr; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.model.JdkJavaType; + +/** + * Unit test of {@link SolrUtils} + * + * @author Andrew Swan + * @since 1.1.5 + */ +public class SolrUtilsTest { + + @Test + public void testGetSolrDynamicFieldPostFixForJavaUtilCalendar() { + assertEquals("_dt", + SolrUtils.getSolrDynamicFieldPostFix(JdkJavaType.CALENDAR)); + } +} diff --git a/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrWebSearchAnnotationValuesTest.java b/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrWebSearchAnnotationValuesTest.java new file mode 100644 index 000000000..41a53b23b --- /dev/null +++ b/addon-solr/src/test/java/org/springframework/roo/addon/solr/SolrWebSearchAnnotationValuesTest.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.solr; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link SolrWebSearchAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class SolrWebSearchAnnotationValuesTest + extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooSolrWebSearchable.class; + } + + @Override + protected Class getValuesClass() { + return SolrWebSearchAnnotationValues.class; + } +} diff --git a/addon-tailor/pom.xml b/addon-tailor/pom.xml new file mode 100644 index 000000000..7746a0654 --- /dev/null +++ b/addon-tailor/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.tailor + bundle + Spring Roo - Addon - Tailor + TODO + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/CommandTransformation.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/CommandTransformation.java new file mode 100644 index 000000000..5666bb7f6 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/CommandTransformation.java @@ -0,0 +1,79 @@ +package org.springframework.roo.addon.tailor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.roo.shell.ParserUtils; + +/** + * Data container to transport an input command and its arguments through all + * configured actions, while those actions fill up a list of outputCommands. + * + * @author Vladimir Tihomirov + * @author Birgitta Boeckeler + */ +public class CommandTransformation { + + /** + * The full input command string, incl. arguments + */ + private String inputCommand; + + /** + * A list of output commands, result of the transformation the inputCommand + * goes through by action executions. + */ + private final List outputCommands = new ArrayList(); + + /** + * Parsed tokens of the command
    + * - Arguments will be represented with key=argumentname without "--", + * value=argumentvalue
    + * - The command elements before the actual "--" arguments will be in an + * entry without a key + */ + private Map arguments; + + public CommandTransformation(final String command) { + setInputCommand(command.trim()); + // ParserUtils.tokenize expects single blanks to split the command: + // Make sure that there are no obsolete blanks in the command string + while (inputCommand.contains(" ")) { + inputCommand = inputCommand.replace(" ", " "); + } + setArguments(ParserUtils.tokenize(inputCommand)); + } + + public void addOutputCommand(final String... commandFragments) { + String outputCommand = ""; + for (final String arg : commandFragments) { + outputCommand = outputCommand.concat(arg); + } + outputCommands.add(outputCommand); + } + + public void clearCommands() { + outputCommands.clear(); + } + + public Map getArguments() { + return arguments; + } + + public String getInputCommand() { + return inputCommand; + } + + public List getOutputCommands() { + return outputCommands; + } + + public void setArguments(final Map options) { + arguments = options; + } + + public void setInputCommand(final String command) { + inputCommand = command; + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/DefaultTailorImpl.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/DefaultTailorImpl.java new file mode 100644 index 000000000..753649acd --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/DefaultTailorImpl.java @@ -0,0 +1,125 @@ +package org.springframework.roo.addon.tailor; + +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.tailor.actions.Action; +import org.springframework.roo.addon.tailor.actions.ActionConfig; +import org.springframework.roo.addon.tailor.config.CommandConfiguration; +import org.springframework.roo.addon.tailor.config.TailorConfiguration; +import org.springframework.roo.addon.tailor.service.ActionLocator; +import org.springframework.roo.addon.tailor.service.ConfigurationLocator; +import org.springframework.roo.addon.tailor.util.CommentedLine; +import org.springframework.roo.addon.tailor.util.TailorHelper; +import org.springframework.roo.shell.AbstractShell; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.Tailor; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Executed by {@link AbstractShell}. Triggers execution of configured actions + * + * @author Vladimir Tihomirov + */ +@Service +@Component +public class DefaultTailorImpl implements Tailor { + @Reference protected ActionLocator actionLocator; + @Reference protected ConfigurationLocator configLocator; + @Reference protected Shell shell; + + private static final Logger LOGGER = HandlerUtils + .getLogger(DefaultTailorImpl.class); + protected boolean inBlockComment = false; + + /** + * @Inheritdoc + */ + public List sew(String command) { + if (StringUtils.isBlank(command)) { + return Collections.emptyList(); + } + try { + // validate if it is commented + final CommentedLine comment = new CommentedLine(command, + inBlockComment); + TailorHelper.removeComment(comment); + inBlockComment = comment.getInBlockComment(); + command = comment.getLine(); + if (StringUtils.isBlank(command)) { + return Collections.emptyList(); + } + // parse and tailor + final CommandTransformation commandTrafo = new CommandTransformation( + command); + execute(commandTrafo); + return commandTrafo.getOutputCommands(); + } + catch (final Exception e) { + // Do nothing if exception happened + LOGGER.log( + Level.WARNING, + "Error tailoring, cancelled command execution: " + + e.getMessage()); + return Collections.emptyList(); + } + } + + // We have to done explicit injection to support API compatibility with STS + // shell + protected void activate(final ComponentContext context) { + if (shell != null) { + shell.setTailor(this); + } + } + + protected void deactivate(final ComponentContext context) { + if (shell != null) { + shell.setTailor(null); + } + } + + protected void logInDevelopmentMode(final Level level, final String logMsg) { + if (shell.isDevelopmentMode()) { + LOGGER.log(level, logMsg); + } + } + + private void execute(final CommandTransformation commandTrafo) { + final TailorConfiguration configuration = configLocator + .getActiveTailorConfiguration(); + if (configuration == null) { + return; + } + final CommandConfiguration commandConfig = configuration + .getCommandConfigFor(commandTrafo.getInputCommand()); + if (commandConfig == null) { + return; + } + logInDevelopmentMode(Level.INFO, + "Tailor: detected " + commandTrafo.getInputCommand()); + + for (final ActionConfig config : commandConfig.getActions()) { + final Action component = actionLocator.getAction(config + .getActionTypeId()); + if (component != null) { + logInDevelopmentMode(Level.INFO, + "\tTailoring: " + component.getDescription(config)); + component.execute(commandTrafo, config); + } + else { + logInDevelopmentMode( + Level.WARNING, + "\tTailoring: Couldn't find action '" + + config.getActionTypeId()); + } + } + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/TailorCommands.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/TailorCommands.java new file mode 100644 index 000000000..808e95707 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/TailorCommands.java @@ -0,0 +1,72 @@ +package org.springframework.roo.addon.tailor; + +import java.util.Iterator; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.tailor.config.TailorConfiguration; +import org.springframework.roo.addon.tailor.service.ConfigurationLocator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Commands to list, activate and deactivate tailor configurations + * + * @author Birgitta Boeckeler + */ +@Component +@Service +public class TailorCommands implements CommandMarker { + + private static final Logger LOGGER = HandlerUtils + .getLogger(TailorCommands.class); + @Reference ConfigurationLocator configLocator; + + /** + * This method activates a tailor configuration by its name (Name needs to + * be listed with "tailor list" command + */ + @CliCommand(value = "tailor activate", help = "Activate a tailor configuration.") + public void tailorActivate( + @CliOption(key = { "name" }, mandatory = true, help = "The name of the tailor configuration") final String tailorName) { + configLocator.setActiveTailorConfiguration(tailorName); + } + + /** + * This method deactivates the current tailor + */ + @CliCommand(value = "tailor deactivate", help = "Deactivate the tailor.") + public void tailorDeactivate() { + configLocator.setActiveTailorConfiguration(null); + } + + /** + * This method lists all available tailor configurations in the the Roo + * shell. + * + * @param type + */ + @CliCommand(value = "tailor list", help = "List available tailor configurations.") + public void tailorList() { + LOGGER.info("Available tailor configurations: "); + final Map configs = configLocator + .getAvailableConfigurations(); + final TailorConfiguration activeConfig = configLocator + .getActiveTailorConfiguration(); + final Iterator iterator = configs.keySet().iterator(); + while (iterator.hasNext()) { + final String configName = iterator.next(); + final String isActive = activeConfig != null + && configName.equals(activeConfig.getName()) ? " [ ACTIVE ] " + : ""; + LOGGER.info("\to " + configName + isActive + " - " + + configs.get(configName).getDescription()); + } + } + +} \ No newline at end of file diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/AbstractAction.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/AbstractAction.java new file mode 100644 index 000000000..1e4d2a2d6 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/AbstractAction.java @@ -0,0 +1,55 @@ +package org.springframework.roo.addon.tailor.actions; + +import java.util.Map; +import java.util.logging.Logger; + +import org.springframework.roo.addon.tailor.CommandTransformation; +import org.springframework.roo.addon.tailor.util.TailorHelper; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Performs actual action in application logic. + * + * @author Vladimir Tihomirov + */ +public abstract class AbstractAction implements Action { + + private static final Logger LOGGER = HandlerUtils + .getLogger(AbstractAction.class); + + /** + * {@inheritDoc} + */ + public void execute(final CommandTransformation command, + final ActionConfig config) { + if (isValid(config)) { + final ActionConfig processedConfig = processConfigAttributes( + command, config); + executeImpl(command, processedConfig); + } + else { + LOGGER.warning("Invalid configuration for tailor action: " + config); + } + } + + /* + * @see #execute(RooCommand, ActionConfig) + */ + protected abstract void executeImpl(CommandTransformation command, + ActionConfig config); + + private ActionConfig processConfigAttributes( + final CommandTransformation command, final ActionConfig config) { + // Process variables in config + final ActionConfig processedConfig = new ActionConfig( + config.getActionTypeId()); + final Map attributes = config.getAttributes(); + for (final Map.Entry entry : attributes.entrySet()) { + final String processedValue = TailorHelper.replaceVars(command, + entry.getValue()); + processedConfig.setAttribute(entry.getKey(), processedValue); + } + return processedConfig; + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Action.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Action.java new file mode 100644 index 000000000..47532c37d --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Action.java @@ -0,0 +1,46 @@ +package org.springframework.roo.addon.tailor.actions; + +import org.springframework.roo.addon.tailor.CommandTransformation; + +/** + * Base interface of action hierarchy. Used for dynamic binding of available + * actions. + * + *
    + * To implement a new Action:
    + * - Create a Component Service that extend AbstractAction
    + * - Create a static method in there that creates an ActionConfig for the new Action.
    + *   This method defines the "interface" for this action: What data does the execute method
    + *   need in addition to the data {@link CommandTransformation#getInputCommand()}?
    + * - Implement the execute method: Read the attributes created with the static factory
    + *   method, execute the action.
    + * 
    + * + * @author Vladimir Tihomirov + */ +public interface Action { + + /** + * Triggers action execution + * + * @param command - resource to be processed + * @param config - configuration of action + */ + void execute(CommandTransformation command, ActionConfig config); + + /** + * Action info + * + * @param config - configuration of action + * @return description of actual action + */ + String getDescription(ActionConfig config); + + /** + * Checks if an ActionConfig is valid for an action execution. + * + * @param config will be checked + * @return true if valid, otherwise false + */ + boolean isValid(ActionConfig config); +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionConfig.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionConfig.java new file mode 100644 index 000000000..ce01b7fee --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionConfig.java @@ -0,0 +1,189 @@ +package org.springframework.roo.addon.tailor.actions; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.addon.tailor.config.CommandConfiguration; +import org.springframework.roo.addon.tailor.service.ActionLocator; + +/** + * Configuration object for an action that can be executed by the a + * {@link CommandConfiguration}. Provides some default attributes with getters + * and setters that are used by the default actions provided by tailor-core. + * Additional custom attributes for new actions can be added via + * {@link #setAttribute(String, String)}. + * + * @author Birgitta Boeckeler + * @author Vladimir Tihomirov + * @since 1.2.0 + */ +public class ActionConfig { + + private final String actionTypeId; + + /* A set of attributes for the default actions delivered with tailor.core */ + private static final String ATTR_MODULE = "module"; + private static final String ATTR_ARGUMENT = "argument"; + private static final String ATTR_VALUE = "value"; + private static final String ATTR_FORCE = "force"; + private static final String ATTR_COMMAND = "command"; + + /** + * A map to flexibly define additional attributes for actions not delivered + * with tailor-core. + */ + private final Map attributes = new LinkedHashMap(); + + /** + * Constructor + * + * @param actionClass Action class to be executed + */ + public ActionConfig(final Class actionClass) { + actionTypeId = actionClass.getSimpleName(); + } + + /** + * Constructor + * + * @param actionTypeId ID of the action's type This should be + * actionClass#getSimpleName(), as the actions are bound to the + * {@link ActionLocator} by class name. + */ + public ActionConfig(final String actionTypeId) { + this.actionTypeId = actionTypeId; + } + + public String getActionTypeId() { + return actionTypeId; + } + + public String getArgument() { + return attributes.get(ATTR_ARGUMENT); + } + + /** + * Get an attribute from this configuration + * + * @param key Key of the attribute + * @return Value of the attribute + */ + public String getAttribute(final String key) { + return attributes.get(key); + } + + /** + * @return A map to flexibly define additional attributes for actions not + * delivered with tailor-core. + */ + public Map getAttributes() { + return attributes; + } + + public String getCommand() { + return attributes.get(ATTR_COMMAND); + } + + public String getDefaultValue() { + return attributes.get(ATTR_VALUE); + } + + public String getModule() { + return attributes.get(ATTR_MODULE); + } + + public boolean isForced() { + final String isForced = attributes.get(ATTR_FORCE); + return "true".equals(isForced) || "yes".equals(isForced); + } + + /** + * Sets an argument value. This method will throw an exception if a value + * with that key is already set, thus making them immutable. This is to + * avoid unexpected behaviour, because ActionConfigs in a + * TailorConfiguration must stay the same once that configuration is + * initiated at Roo startup. + * + * @param argument Argument name (e.g. used for DefaultValue action to + * define a default value for an argument with this name) + * @see org.springframework.roo.addon.tailor.actions.DefaultValue + */ + public void setArgument(final String argument) { + // Don't allow overriding of arguments! + // The ActionConfig will be reused for all action executions, + // so it should stay the same after instantiation. + if (StringUtils.isNotBlank(attributes.get(argument))) { + throw new IllegalStateException( + "ActionConfig.setArgument: ActionConfig attributes are immutable once instantiated!"); + } + attributes.put(ATTR_ARGUMENT, argument); + } + + /** + * Add an additional attribute to this configuration + * + * @param key Key for the attribute + * @param value Value to set for the attribute + */ + public void setAttribute(final String key, final String value) { + attributes.put(key, value); + } + + /** + * @param command An additional command to be executed. String can contain + * placeholders in the format ${placeholder}, referencing + * arguments of the original command. + * @see org.springframework.roo.addon.tailor.actions.Execute + */ + public void setCommand(final String command) { + attributes.put(ATTR_COMMAND, command); + } + + /** + * @param isForced Sets an attribute called force, e.g. used for + * DefaultValue action to determine if a default value is forced + * or only applies when not specified. Default is false. + * @see org.springframework.roo.addon.tailor.actions.DefaultValue + */ + public void setForce(final boolean isForced) { + if (isForced) { + attributes.put(ATTR_FORCE, "true"); + } + else { + attributes.put(ATTR_FORCE, "false"); + } + } + + /** + * @param module Name of a module (e.g. used for Focus) + * @see org.springframework.roo.addon.tailor.actions.Focus + */ + public void setModule(final String module) { + attributes.put(ATTR_MODULE, module); + } + + /** + * @param value Sets an attribute called value, e.g. used for DefaultValue + * action as default value + * @see org.springframework.roo.addon.tailor.actions.DefaultValue + */ + public void setValue(final String value) { + attributes.put(ATTR_VALUE, value); + } + + @Override + public String toString() { + final StringBuffer result = new StringBuffer(); + result.append("Type: " + actionTypeId); + final Iterator iterator = attributes.keySet().iterator(); + while (iterator.hasNext()) { + final String attribute = iterator.next(); + result.append(" | ").append(attribute).append(" = ") + .append(attributes.get(attribute)); + } + return result.toString(); + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionConfigFactory.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionConfigFactory.java new file mode 100644 index 000000000..5a01ae901 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionConfigFactory.java @@ -0,0 +1,61 @@ +package org.springframework.roo.addon.tailor.actions; + +/** + * A set of static methods to create ActionConfig objects for each of the + * actions delivered with tailor-core. + * + * @author Birgitta Boeckeler + * @author Vladimir Tihomirov + * @since 1.2.0 + */ +public class ActionConfigFactory { + + /** + * @see ActionConfig#setArgument(String) + * @see ActionConfig#setValue(String) + */ + public static ActionConfig defaultArgumentAction(final String argument, + final String defaultValue) { + return defaultArgumentAction(argument, defaultValue, false); + } + + public static ActionConfig defaultArgumentAction(final String argument, + final String defaultValue, final boolean force) { + final ActionConfig config = new ActionConfig( + ActionType.DEFAULTVALUE.getActionId()); + config.setArgument(argument); + config.setValue(defaultValue); + config.setForce(force); + return config; + } + + /** + * Creates an empty execute commmand (if left unchanged, this action will + * lead to execution of the original input command) + * + * @return new ActionConfig + */ + public static ActionConfig executeAction() { + final ActionConfig config = new ActionConfig( + ActionType.EXECUTE.getActionId()); + return config; + } + + public static ActionConfig executeAction(final String command) { + final ActionConfig config = new ActionConfig( + ActionType.EXECUTE.getActionId()); + config.setCommand(command); + return config; + } + + /** + * @see Focus + */ + public static ActionConfig focusModuleAction(final String module) { + final ActionConfig config = new ActionConfig( + ActionType.FOCUS.getActionId()); + config.setModule(module); + return config; + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionType.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionType.java new file mode 100644 index 000000000..e92bfddd5 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/ActionType.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.tailor.actions; + +/** + * Predefined action types. Can be used to avoid spelling mistakes for the + * action types when creating a TailorConfiguration with Java. + * + * @author Birgitta Boeckeler + */ +public enum ActionType { + + DEFAULTVALUE(DefaultValue.class), FOCUS(Focus.class), EXECUTE(Execute.class); + + private Class actionClass; + + ActionType(final Class actionClass) { + this.actionClass = actionClass; + } + + public String getActionId() { + return actionClass.getSimpleName(); + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/DefaultValue.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/DefaultValue.java new file mode 100644 index 000000000..ba767dc1d --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/DefaultValue.java @@ -0,0 +1,57 @@ +package org.springframework.roo.addon.tailor.actions; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.tailor.CommandTransformation; + +/** + * Adds default argument to the command If default argument is forced it will be + * always replaced + * + * @author Vladimir Tihomirov + */ +@Component +@Service +public class DefaultValue extends AbstractAction { + + @Override + public void executeImpl(final CommandTransformation arg, + final ActionConfig config) { + // Allow argument name with and without "--" in config + String argumentName = config.getArgument(); + if (argumentName.startsWith("--")) { + argumentName = argumentName.substring(2); + } + // Change both the command string and update the arguments + if (!arg.getInputCommand().contains("--" + argumentName)) { + arg.setInputCommand(arg.getInputCommand().concat(" --") + .concat(argumentName).concat(" ") + .concat(config.getDefaultValue())); + // Update the arguments, so that subsequent actions will be based on + // this default value + arg.getArguments().put(argumentName, config.getDefaultValue()); + } + else if (config.isForced()) { + final String oldValue = arg.getArguments().get(argumentName); + if (StringUtils.isNotBlank(oldValue)) { + // Replace the old value with the default one + arg.setInputCommand(arg.getInputCommand().replace( + "--" + argumentName + " " + oldValue, + "--" + argumentName + " " + config.getDefaultValue())); + arg.getArguments().put(argumentName, config.getDefaultValue()); + } + } + + } + + public String getDescription(final ActionConfig config) { + return "Setting default argument: " + config.getArgument() + " = " + + config.getDefaultValue(); + } + + public boolean isValid(final ActionConfig config) { + return config != null && StringUtils.isNotBlank(config.getArgument()) + && StringUtils.isNotBlank(config.getDefaultValue()); + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Execute.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Execute.java new file mode 100644 index 000000000..3663fdd9d --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Execute.java @@ -0,0 +1,95 @@ +package org.springframework.roo.addon.tailor.actions; + +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.tailor.CommandTransformation; + +/** + * Schedules command for execution. + * + * @author Vladimir Tihomirov + * @author Birgitta Boeckeler + * @since 1.3.0 + */ +@Component +@Service +public class Execute extends AbstractAction { + + public static final String ACTIONATTR_REMOVEARGS = "exclude"; + + @Override + public void executeImpl(final CommandTransformation trafo, + final ActionConfig config) { + if (StringUtils.isBlank(config.getCommand())) { + // If no command specified, this will execute the original command + final String processedCommand = removeArgumentsFromInputCmd(trafo, + config); + trafo.addOutputCommand(processedCommand); + } + else { + trafo.addOutputCommand(config.getCommand()); + } + } + + public String getDescription(final ActionConfig config) { + if (StringUtils.isEmpty(config.getCommand())) { + return "Executing original command"; + } + return "Executing command: " + config.getCommand(); + } + + public boolean isValid(final ActionConfig config) { + return config != null + // "excludes" option only valid if "command" is empty + && !(StringUtils.isNotBlank(config + .getAttribute(ACTIONATTR_REMOVEARGS)) && StringUtils + .isNotBlank(config.getCommand())); + } + + /** + * Based on the value of ({@value #ACTIONATTR_REMOVEARGS}) in the action + * configuration, this method removes arguments from the input command in + * trafo. + * + * @param trafo Transformation object + * @param config Action configuration + * @return Processed input command + */ + private String removeArgumentsFromInputCmd( + final CommandTransformation trafo, final ActionConfig config) { + final String removeArgumentsAttribute = config + .getAttribute(ACTIONATTR_REMOVEARGS); + if (StringUtils.isBlank(removeArgumentsAttribute)) { + return trafo.getInputCommand(); + } + + String inputCommandString = trafo.getInputCommand(); + + final String[] removeArgumentsList = removeArgumentsAttribute + .split(","); + for (final String element : removeArgumentsList) { + String argToRemove = element; + + if (argToRemove.startsWith("--")) { + argToRemove = argToRemove.substring(2); + } + + final Map cmdArguments = trafo.getArguments(); + final Iterator keyIterator = cmdArguments.keySet() + .iterator(); + while (keyIterator.hasNext()) { + final String argName = keyIterator.next(); + if (argName.equals(argToRemove)) { + inputCommandString = inputCommandString.replace("--" + + argName + " " + cmdArguments.get(argName), ""); + } + } + } + + return inputCommandString; + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Focus.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Focus.java new file mode 100644 index 000000000..ec48d52a6 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/actions/Focus.java @@ -0,0 +1,88 @@ +package org.springframework.roo.addon.tailor.actions; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.tailor.CommandTransformation; +import org.springframework.roo.project.ProjectOperations; + +/** + * Focuses on a module with a given name. This action does not check for the + * EXACT name, but looks for a module that contains that string. This makes + * tailor configurations portable over projects with different names that have + * naming conventions for their modules and can match for certain patterns in + * modules. + *

    + * Advanced feature:
    + * Imagine the case that there are 2 modules named "projectname-domain" and + * "projectname-domain-test".
    + * For these cases, the match string can also be a comma-separated list, e.g. + * "domain,test".
    + * Each of the members of that list can start with a "/" to indicate that this + * member must NOT be present in the module to match, e.g. "domain,/test". + * + * @author Vladimir Tihomirov + * @author Birgitta Boeckeler + */ +@Component +@Service +public class Focus extends AbstractAction { + + @Reference protected ProjectOperations projectOperations; + + private final String baseCommand = "module focus --moduleName "; + + @Override + public void executeImpl(final CommandTransformation trafo, + final ActionConfig config) { + if ("~".equals(config.getModule())) { + trafo.addOutputCommand(baseCommand, "~"); + return; + } + + // If a command is tailored right after the shell was started, sometimes + // the module names are not yet loaded + if (projectOperations.getModuleNames().isEmpty()) { + throw new IllegalStateException( + "Module names not loaded, please try again."); + } + + // If comma-separated list: Module name will be checked against both + // those values + final String[] matches = config.getModule().split(","); + + // If not root: Check if module name actually exists + for (final String moduleName : projectOperations.getModuleNames()) { + // if (StringUtils.isEmpty(moduleName)) { + // continue; + // } + boolean matchesAll = true; + for (final String matche : matches) { + final String match = matche; + if (match.startsWith("/") + && moduleName.contains(match.substring(1))) { + matchesAll = false; + break; + } + else if (!match.startsWith("/") && !moduleName.contains(match)) { + matchesAll = false; + break; + } + } + if (matchesAll) { + trafo.addOutputCommand(baseCommand, moduleName); + return; + } + } + } + + public String getDescription(final ActionConfig config) { + return "Focusing: " + config.getModule(); + } + + public boolean isValid(final ActionConfig config) { + return config != null && StringUtils.isNotBlank(config.getModule()); + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/CommandConfiguration.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/CommandConfiguration.java new file mode 100644 index 000000000..4134f88e0 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/CommandConfiguration.java @@ -0,0 +1,37 @@ +package org.springframework.roo.addon.tailor.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.addon.tailor.actions.ActionConfig; + +/** + * Contains configuration (list of actions) for certain roo command + * + * @author Birgitta Boeckeler + */ +public class CommandConfiguration { + + /** + * Name of the command that will trigger this configuration + */ + private String commandName; + + private final List actions = new ArrayList(); + + public void addAction(final ActionConfig actionConfig) { + actions.add(actionConfig); + } + + public List getActions() { + return actions; + } + + public String getCommandName() { + return commandName; + } + + public void setCommandName(final String commandName) { + this.commandName = commandName; + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/TailorConfiguration.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/TailorConfiguration.java new file mode 100644 index 000000000..a99e099c8 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/TailorConfiguration.java @@ -0,0 +1,80 @@ +package org.springframework.roo.addon.tailor.config; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data container for a tailor configuration. Defines a set of + * {@link CommandConfiguration} objects that define which actions should be + * triggered by which commands when this configuration is activated + * + * @author Birgitta Boeckeler + * @since 1.2.0 + */ +public class TailorConfiguration { + + private final List commandConfigs = new ArrayList(); + + private final String name; + + private String description; + + private boolean isActive = false; + + /** + * Constructor + * + * @param name Name of the configuration. Should be unique over all + * TailorConfiguration instances in the container + */ + public TailorConfiguration(final String name) { + this.name = name; + } + + public TailorConfiguration(final String name, final String description) { + this.name = name; + this.description = description; + } + + public void addCommandConfig(final CommandConfiguration newConfig) { + commandConfigs.add(newConfig); + } + + /** + * Looks up the CommandConfiguration for a specific command. + * + * @param fullCommandString The command string to check + * @return CommandConfiguration for the command in this TailorConfiguration; + * null if no configuration present for the command + */ + public CommandConfiguration getCommandConfigFor( + final String fullCommandString) { + for (final CommandConfiguration config : commandConfigs) { + if (fullCommandString.startsWith(config.getCommandName())) { + return config; + } + } + return null; + } + + public List getCommandConfigs() { + return commandConfigs; + } + + public String getDescription() { + return description; + } + + public String getName() { + return name; + } + + public boolean isActive() { + return isActive; + } + + public void setActive(final boolean isActive) { + this.isActive = isActive; + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/TailorConfigurationFactory.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/TailorConfigurationFactory.java new file mode 100644 index 000000000..1d3f5da02 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/TailorConfigurationFactory.java @@ -0,0 +1,20 @@ +package org.springframework.roo.addon.tailor.config; + +import java.util.List; + +/** + * Creates a Tailor configuration. + * + * @author Birgitta Boeckeler + */ +public interface TailorConfigurationFactory { + + /** + * Creates a tailor configuration. + * + * @param name - configuration name + * @return - tailor configuration + */ + List createTailorConfiguration(); + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/xml/TailorParser.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/xml/TailorParser.java new file mode 100644 index 000000000..0a3fc66a2 --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/xml/TailorParser.java @@ -0,0 +1,119 @@ +package org.springframework.roo.addon.tailor.config.xml; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.addon.tailor.actions.ActionConfig; +import org.springframework.roo.addon.tailor.config.CommandConfiguration; +import org.springframework.roo.addon.tailor.config.TailorConfiguration; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +public class TailorParser { + private static final Logger LOGGER = HandlerUtils + .getLogger(TailorParser.class); + + /** + * Maps the XML file contents to a TailorConfiguration object. It is + * possible to have multiple configurations in tailor.xml + * + * @param root + * @return list of tailor configurations + */ + public static List mapXmlToTailorConfiguration( + final Element root) { + final List elTailors = XmlUtils.findElements( + "/tailorconfiguration/tailor", root); + if (elTailors.isEmpty()) { + logTailorXMLInvalid("no definitions found in root element"); + return null; + } + final List configs = new ArrayList(); + for (final Element eTailor : elTailors) { + final TailorConfiguration config = parseTailorConfiguration(eTailor); + if (config != null) { + configs.add(config); + } + } + return configs; + } + + /** + * Maps the single XML tailor configuration to a TailorConfiguration object. + * + * @param tailor element + * @return tailor configurations + */ + public static TailorConfiguration parseTailorConfiguration( + final Element elTailor) { + + if (StringUtils.isBlank(elTailor.getAttribute("name"))) { + logTailorXMLInvalid(" must have a name attribute"); + return null; + } + + final TailorConfiguration result = new TailorConfiguration( + elTailor.getAttribute("name"), + elTailor.getAttribute("description")); + + final String activeAttribute = elTailor.getAttribute("activate"); + if (StringUtils.isNotBlank(activeAttribute)) { + final boolean isActive = "true".equalsIgnoreCase(activeAttribute) + || "yes".equalsIgnoreCase(activeAttribute); + result.setActive(isActive); + } + + final List elConfigs = XmlUtils.findElements("config", + elTailor); + if (elConfigs.isEmpty()) { + logTailorXMLInvalid(" must have child elements"); + return null; + } + + for (final Element elConfig : elConfigs) { + final String command = elConfig.getAttribute("command"); + if (StringUtils.isBlank(command)) { + logTailorXMLInvalid("found without command attribute"); + return null; + } + + final CommandConfiguration newCmdConfig = new CommandConfiguration(); + newCmdConfig.setCommandName(command); + final List elActions = XmlUtils.findElements("action", + elConfig); + for (final Element elAction : elActions) { + // Determine the action type + if (StringUtils.isBlank(elAction.getAttribute("type"))) { + logTailorXMLInvalid("found without type attribute"); + return null; + } + final ActionConfig newAction = new ActionConfig( + elAction.getAttribute("type")); + final NamedNodeMap attributes = elAction.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + final Node item = attributes.item(i); + final String attributeKey = item.getNodeName(); + if (!"type".equals(attributeKey)) { + newAction.setAttribute(attributeKey, + item.getNodeValue()); + } + } + newCmdConfig.addAction(newAction); + } + result.addCommandConfig(newCmdConfig); + + } + return result; + } + + private static void logTailorXMLInvalid(final String msg) { + LOGGER.warning("Invalid tailor.xml - please correct and restart the shell to use this configuration (" + + msg + ")"); + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/xml/XMLTailorConfigurationFactory.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/xml/XMLTailorConfigurationFactory.java new file mode 100644 index 000000000..902e2f7be --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/config/xml/XMLTailorConfigurationFactory.java @@ -0,0 +1,99 @@ +package org.springframework.roo.addon.tailor.config.xml; + +import static org.springframework.roo.support.util.FileUtils.CURRENT_DIRECTORY; + +import java.io.File; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.tailor.config.TailorConfiguration; +import org.springframework.roo.addon.tailor.config.TailorConfigurationFactory; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Factory to create a TailorConfiguration from an XML configuration file named + * "tailor.xml" in the shell root. + * + * @author Birgitta Boeckeler + * @since 1.2.0 + */ +@Component +@Service +public class XMLTailorConfigurationFactory implements + TailorConfigurationFactory { + + private static final Logger LOGGER = HandlerUtils + .getLogger(XMLTailorConfigurationFactory.class); + + @Reference FileManager fileManager; + + String shellRootPath = null; + + /** + * Sample file: + * + *

    +     * 
    +     * 	
    +     * 		
    +     * 			
    +     * 		
    +     * 
    + */ + public List createTailorConfiguration() { + String configFileIdentifier = shellRootPath + "/tailor.xml"; + if (!fileManager.exists(configFileIdentifier)) { + configFileIdentifier = System.getProperty("user.home") + + "/tailor.xml"; + if (!fileManager.exists(configFileIdentifier)) { + return null; + } + } + + try { + final Document readXml = XmlUtils.readXml(fileManager + .getInputStream(configFileIdentifier)); + final Element root = readXml.getDocumentElement(); + return TailorParser.mapXmlToTailorConfiguration(root); + } + catch (final Exception e) { + // Make sure that an invalid tailor.xml file does not crash the + // whole shell + logTailorXMLInvalid("Error reading file (" + + e.getLocalizedMessage()); + } + + return null; + + } + + // ------------ OSGi component methods ---------------- + protected void activate(final ComponentContext context) { + // Load the root project directory at startup + // Use this instead of the shell object, because shell.getHome() + // sometimes + // gives false results, e.g. when running Roo in test mode from Eclipse + // (PathResolver cannot be used because the config file needs to be + // available even + // if there is no project created) + final File shellDirectory = new File(StringUtils.defaultIfEmpty( + OSGiUtils.getRooWorkingDirectory(context), CURRENT_DIRECTORY)); + shellRootPath = FileUtils.getCanonicalPath(shellDirectory); + } + + private void logTailorXMLInvalid(final String msg) { + LOGGER.warning("Invalid tailor.xml - please correct and restart the shell to use this configuration (" + + msg + ")"); + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/ActionLocator.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/ActionLocator.java new file mode 100644 index 000000000..69abc258b --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/ActionLocator.java @@ -0,0 +1,22 @@ +package org.springframework.roo.addon.tailor.service; + +import java.util.Map; + +import org.springframework.roo.addon.tailor.actions.Action; + +/** + * Locates all actions. + * + * @author Vladimir Tihomirov + */ +public interface ActionLocator { + + Action getAction(String caseInsensitiveKey); + + /** + * Get all available actions + * + * @return map of actions + */ + Map getAllActions(); +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/ConfigurationLocator.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/ConfigurationLocator.java new file mode 100644 index 000000000..a8108385b --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/ConfigurationLocator.java @@ -0,0 +1,33 @@ +package org.springframework.roo.addon.tailor.service; + +import java.util.Map; + +import org.springframework.roo.addon.tailor.config.TailorConfiguration; + +/** + * Locates and binds all {@link TailorConfiguration} implementations in the + * container. Holds the information which of these configurations is currently + * activated. + * + * @author Birgitta Boeckeler + */ +public interface ConfigurationLocator { + + /** + * @return the currently active TailorConfiguration + */ + TailorConfiguration getActiveTailorConfiguration(); + + /** + * @return all available {@link TailorConfiguration} instances + */ + Map getAvailableConfigurations(); + + /** + * Activate Tailor Configuration with certain name + * + * @param name Name of configuration to be activated + */ + void setActiveTailorConfiguration(String name); + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/DefaultActionLocator.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/DefaultActionLocator.java new file mode 100644 index 000000000..05047b72c --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/DefaultActionLocator.java @@ -0,0 +1,51 @@ +package org.springframework.roo.addon.tailor.service; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.tailor.actions.Action; + +/** + * Locates all available actions => all OSGi Services of type {@link Action} + * + * @author Vladimir Tihomirov + */ +@Component +@Service +@Reference(name = "action", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = Action.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class DefaultActionLocator implements ActionLocator { + + /** + * A map of all the actions found in the OSGi container. Bound dynamically + * by Felix, keys are the simple class names in lower case, values the + * respective OSGi services. + */ + private final Map actionsMap = new LinkedHashMap(); + + public Action getAction(final String caseInsensitiveKey) { + return actionsMap.get(caseInsensitiveKey.toLowerCase()); + } + + public Map getAllActions() { + return actionsMap; + } + + protected void bindAction(final Action action) { + final String actionClassName = action.getClass().getSimpleName() + .toLowerCase(); + actionsMap.put(actionClassName, action); + } + + protected void unbindAction(final Action action) { + final String actionClassName = action.getClass().getSimpleName() + .toLowerCase(); + actionsMap.remove(actionClassName); + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/DefaultConfigurationLocator.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/DefaultConfigurationLocator.java new file mode 100644 index 000000000..d5bdf441f --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/service/DefaultConfigurationLocator.java @@ -0,0 +1,101 @@ +package org.springframework.roo.addon.tailor.service; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.tailor.config.TailorConfiguration; +import org.springframework.roo.addon.tailor.config.TailorConfigurationFactory; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Default implementation of {@link ConfigurationLocator} + * + * @author Birgitta Boeckeler + * @since 1.2.0 + */ +@Component +@Service +@Reference(name = "config", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = TailorConfigurationFactory.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class DefaultConfigurationLocator implements ConfigurationLocator { + + private static final Logger LOGGER = HandlerUtils + .getLogger(DefaultConfigurationLocator.class); + + /** + * Name of currently activated configuration + */ + private String activatedTailorConfigName = null; + + /** + * Map of all available configurations - dynamically bound by Felix on + * startup + */ + private final Map configurations = new LinkedHashMap(); + + public TailorConfiguration getActiveTailorConfiguration() { + + return configurations.get(activatedTailorConfigName); + } + + public Map getAvailableConfigurations() { + + return configurations; + } + + public void setActiveTailorConfiguration(final String name) { + if (name == null) { + activatedTailorConfigName = null; + LOGGER.info("Tailor deactivated"); + return; + } + if (configurations.get(name) != null) { + activatedTailorConfigName = name; + } + else { + LOGGER.severe("Couldn't activate tailor configuration '" + name + + "', not available."); + } + } + + protected void bindConfig(final TailorConfigurationFactory factory) { + final List configs = factory + .createTailorConfiguration(); + if (CollectionUtils.isEmpty(configs)) { + return; + } + for (final TailorConfiguration config : configs) { + if (configurations.get(config.getName()) != null) { + LOGGER.warning("TailorConfiguration duplicate '" + + config.getName() + "', not binding again: " + + config.toString()); + } + if (config.isActive()) { + activatedTailorConfigName = config.getName(); + } + configurations.put(config.getName(), config); + } + } + + protected void unbindConfig(final TailorConfigurationFactory factory) { + // TODO It's a little unelegant to call "create" method here again, but + // we need the name... + final List configs = factory + .createTailorConfiguration(); + if (CollectionUtils.isEmpty(configs)) { + return; + } + for (final TailorConfiguration config : configs) { + configurations.remove(config.getName()); + } + } + +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/util/CommentedLine.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/util/CommentedLine.java new file mode 100644 index 000000000..6bed58c7e --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/util/CommentedLine.java @@ -0,0 +1,27 @@ +package org.springframework.roo.addon.tailor.util; + +public class CommentedLine { + private String line; + private Boolean inBlockComment; + + public CommentedLine(final String line, final Boolean inBlockComment) { + this.line = line; + this.inBlockComment = inBlockComment; + } + + public Boolean getInBlockComment() { + return inBlockComment; + } + + public String getLine() { + return line; + } + + public void setInBlockComment(final Boolean inBlockComment) { + this.inBlockComment = inBlockComment; + } + + public void setLine(final String line) { + this.line = line; + } +} diff --git a/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/util/TailorHelper.java b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/util/TailorHelper.java new file mode 100644 index 000000000..d1bc2d2ce --- /dev/null +++ b/addon-tailor/src/main/java/org/springframework/roo/addon/tailor/util/TailorHelper.java @@ -0,0 +1,106 @@ +package org.springframework.roo.addon.tailor.util; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.addon.tailor.CommandTransformation; +import org.springframework.roo.addon.tailor.actions.ActionConfig; + +/** + * Helper static operations. + * + * @author Birgitta Boeckeler + * @author Vladimir Tihomirov + */ +public class TailorHelper { + + /** + * Pattern to look for ${xxx} usage in a command string + */ + private static final Pattern VAR_PATTERN = Pattern + .compile("\\$\\{([\\w\\*]*)\\}"); + + public static void removeComment(final CommentedLine commentedLine) { + String line = commentedLine.getLine(); + boolean inBlockComment = commentedLine.getInBlockComment(); + if (StringUtils.isBlank(line)) { + return; + } + if (line.contains("/*")) { + inBlockComment = true; + final String lhs = line.substring(0, line.lastIndexOf("/*")); + if (line.contains("*/")) { + line = lhs + line.substring(line.lastIndexOf("*/") + 2); + inBlockComment = false; + } + else { + line = lhs; + } + } + else if (inBlockComment && line.contains("*/")) { + line = line.substring(line.lastIndexOf("*/") + 2); + inBlockComment = false; + } + else if (inBlockComment) { + line = ""; + } + else if (line.trim().startsWith("//") || line.trim().startsWith("#")) { + line = ""; + } + commentedLine.setLine(line.replace('\t', ' ')); + commentedLine.setInBlockComment(inBlockComment); + } + + /** + * Looks for ${xxx} pattern in {@link ActionConfig#getCommand()} and + * replaces those placeholders with the respective values from the + * inputCommand's arguments. + * + * @param trafo The CommandTransformation instance with the inputCommand to + * use to extract the values + * @param text A string with potential occurrences of placeholders + * @return The new command string + */ + public static String replaceVars(final CommandTransformation trafo, + String text) { + /* + * TODO: This could also be done the other way around: iterate over all + * arguments of the input command and replace the corresponding ${} + * occurrences. >> Think about which makes more sense. + */ + final Map inputArguments = trafo.getArguments(); + if (inputArguments == null || inputArguments.isEmpty()) { + return text; + } + + final Matcher matcher = VAR_PATTERN.matcher(text); + while (matcher.find()) { + // Placeholder name between ${} + final String placeholder = matcher.group(1); + String inputValue = null; + if ("*".equals(placeholder)) { + // In this case, take the last fragment of the original command + // that is not defined with "--" > assumed this is the first + // argument, + // which can sometimes be given without a "--" name + final String[] split = inputArguments.get("").split(" "); + inputValue = split[split.length - 1]; + } + else { + inputValue = inputArguments.get(placeholder); + } + if (inputValue != null) { + // Escape the special characters to ensure correct replacement + String replace = matcher.group().replace("$", "\\$"); + replace = replace.replace("{", "\\{"); + replace = replace.replace("}", "\\}"); + replace = replace.replace("*", "\\*"); + // Do the actual replacement + text = text.replaceAll(replace, inputValue); + } + } + return text; + } +} diff --git a/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/actions/TestFocusModule.java b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/actions/TestFocusModule.java new file mode 100644 index 000000000..feaa695f8 --- /dev/null +++ b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/actions/TestFocusModule.java @@ -0,0 +1,121 @@ +package org.springframework.roo.addon.tailor.actions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.roo.addon.tailor.CommandTransformation; +import org.springframework.roo.project.MavenOperationsImpl; + +/** + * Tests for {@link Focus} + * + * @author Birgitta Boeckeler + */ +public class TestFocusModule { + + /** + * Tests a list of match strings for the module name + */ + @Test + public void testList() { + + final Focus action = createTestActionObject(); + final ActionConfig config = ActionConfigFactory + .focusModuleAction("data,it"); + final CommandTransformation trafo = new CommandTransformation( + "command not relevant for this test"); + action.execute(trafo, config); + // Test data: "data" module is first, "data-it" second. + // Expected that action will discard "data" and choose "data-it" + Assert.assertTrue(trafo.getOutputCommands().contains( + "module focus --moduleName data-it")); + } + + @Test + public void testListWithout() { + final Focus action = createTestActionObject(); + final ActionConfig config = ActionConfigFactory + .focusModuleAction("domain,/it"); + final CommandTransformation trafo = new CommandTransformation( + "command not relevant for this test"); + action.execute(trafo, config); + // Test data: "domain-it" module is first, "domain" second. + // Expected that action will discard "domain-it" because it contains + // "it", and choose "domain" + Assert.assertTrue(trafo.getOutputCommands().contains( + "module focus --moduleName domain")); + } + + @Test + public void testModulesNotLoadedYet() { + final Focus action = new Focus(); + action.projectOperations = new MockProjectOperationsEmpty(); + + final ActionConfig config = ActionConfigFactory + .focusModuleAction("domain"); + final CommandTransformation trafo = new CommandTransformation( + "command not relevant for this test"); + + try { + action.execute(trafo, config); + Assert.fail("Should throw exception (is caught by DefaultTailorImpl, but this test goes directly to the action)"); + } + catch (final IllegalStateException e) { + Assert.assertTrue(trafo.getOutputCommands().isEmpty()); + } + + } + + @Test + public void testStandard() { + final Focus action = createTestActionObject(); + final ActionConfig config = ActionConfigFactory + .focusModuleAction("domain"); + final CommandTransformation trafo = new CommandTransformation( + "command not relevant for this test"); + action.execute(trafo, config); + // Test data: "domain-it" module is first, "domain" second. + // Expected that action will choose "domain-it" as the first positive + // match + Assert.assertTrue(trafo.getOutputCommands().contains( + "module focus --moduleName domain-it")); + } + + private Focus createTestActionObject() { + final Focus action = new Focus(); + action.projectOperations = new MockProjectOperations(); + return action; + } + + /** + * Mock ProjectOperations to return a list of test module names + */ + private class MockProjectOperations extends MavenOperationsImpl { + + @Override + public Collection getModuleNames() { + final List result = new ArrayList(); + result.add(""); + result.add("domain-it"); + result.add("domain"); + result.add("data"); + result.add("data-it"); + return result; + } + + } + + private class MockProjectOperationsEmpty extends MavenOperationsImpl { + + @Override + public Collection getModuleNames() { + return Collections.emptyList(); + } + + } + +} diff --git a/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/config/xml/TestTailorParser.java b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/config/xml/TestTailorParser.java new file mode 100644 index 000000000..3cb25424e --- /dev/null +++ b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/config/xml/TestTailorParser.java @@ -0,0 +1,32 @@ +package org.springframework.roo.addon.tailor.config.xml; + +import org.junit.Test; +import org.springframework.roo.addon.tailor.actions.Focus; + +/** + * Tests for {@link Focus} + * + * @author Vladimir Tihomirov + */ +public class TestTailorParser { + + @Test + public void testStandard() { + // Document readXml = XmlUtils.readXml(this.getClass() + // .getResourceAsStream("testtailor.xml")); + // Element root = readXml.getDocumentElement(); + // List configs = TailorParser + // .mapXmlToTailorConfiguration(root); + // Assert.assertNotNull(configs); + // Assert.assertNotNull(configs.get(0)); + // Assert.assertNotNull(configs.get(1)); + // Assert.assertNotNull(configs.get(2)); + // Assert.assertEquals("afpj", configs.get(0).getName()); + // Assert.assertEquals("test", configs.get(1).getName()); + // Assert.assertEquals("helper", configs.get(2).getName()); + // Assert.assertEquals(3, configs.get(0).getCommandConfigs().size()); + // Assert.assertEquals(2, configs.get(1).getCommandConfigs().size()); + // Assert.assertEquals(1, configs.get(2).getCommandConfigs().size()); + } + +} diff --git a/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/config/xml/testtailor.xml b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/config/xml/testtailor.xml new file mode 100644 index 000000000..8d0914a77 --- /dev/null +++ b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/config/xml/testtailor.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TailorHelperTest.java b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TailorHelperTest.java new file mode 100644 index 000000000..65b201848 --- /dev/null +++ b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TailorHelperTest.java @@ -0,0 +1,79 @@ +package org.springframework.roo.addon.tailor.util; + +import junit.framework.Assert; + +import org.junit.Test; +import org.springframework.roo.addon.tailor.CommandTransformation; + +/** + * Tests for {@link TailorHelper#replaceVars(CommandTransformation, String)}. + * + * @author Birgitta Boeckeler + */ +public class TailorHelperTest { + + /** + * Tests a standard case: 2 arguments in the trigger command, both of them + * represented as placeholders in the target + */ + @Test + public void testReplaceVars() { + final CommandTransformation rooCommand = new CommandTransformation( + "project --topLevelPackage com.foo.sample --projectName test --domain otherdomainname"); + final String result = TailorHelper + .replaceVars(rooCommand, + "module create --moduleName ${domain} --topLevelPackage ${topLevelPackage}"); + final String expectedResult = "module create --moduleName otherdomainname --topLevelPackage com.foo.sample"; + Assert.assertEquals("Unexpected result: " + result, expectedResult, + result); + } + + /** + * Test replaceVars when there is more than one occurence of the same + * placeholder in the target + */ + @Test + public void testReplaceVarsDuplicatePlaceholders() { + final CommandTransformation rooCommand = new CommandTransformation( + "project --topLevelPackage com.foo.sample --projectName test --domain otherdomainname"); + final String result = TailorHelper + .replaceVars( + rooCommand, + "module create --moduleName ${domain} --topLevelPackage ${topLevelPackage}.${domain}"); + final String expectedResult = "module create --moduleName otherdomainname --topLevelPackage com.foo.sample.otherdomainname"; + Assert.assertEquals("Unexpected result: " + result, expectedResult, + result); + } + + /** + * Test for "unnamed argument": Use of ${*} as placeholder should result in + * replacing it with the last fragment of the trigger command, i.e. the + * first "unnamed" argument. + */ + @Test + public void testReplaceVarsForUnnamedArgument() { + final CommandTransformation rooCommand = new CommandTransformation( + "cd test-data"); + final String result = TailorHelper.replaceVars(rooCommand, + "module focus --moduleName ${*}"); + Assert.assertTrue("* was not replaced: " + result, + result.endsWith("test-data")); + } + + /** + * Test makes sure that the regex used by TailorHelper does not match + * placeholders that are similar, but different. (Prefix and suffix) + */ + @Test + public void testReplaceVarsSimilarPlaceholders() { + final CommandTransformation rooCommand = new CommandTransformation( + "project --topLevelPackage com.foo.sample --projectName test --domain otherdomainname"); + final String result = TailorHelper + .replaceVars(rooCommand, + "module create --moduleName ${xdomain} --topLevelPackage ${topLevelPackagex}"); + final String expectedResult = "module create --moduleName ${xdomain} --topLevelPackage ${topLevelPackagex}"; + Assert.assertEquals("Unexpected result: " + result, expectedResult, + result); + } + +} diff --git a/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TestTailorHelper.java b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TestTailorHelper.java new file mode 100644 index 000000000..c906dbc41 --- /dev/null +++ b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TestTailorHelper.java @@ -0,0 +1,78 @@ +package org.springframework.roo.addon.tailor.util; + +import junit.framework.Assert; + +import org.junit.Test; +import org.springframework.roo.addon.tailor.CommandTransformation; + +/** + * Tests for {@link TailorHelper#replaceVars(CommandTransformation, String)} + * + * @author Birgitta Boeckeler + */ +public class TestTailorHelper { + + /** + * Tests a standard case: 2 arguments in the trigger command, both of them + * represented as placeholders in the target + */ + @Test + public void testReplaceVars() { + final CommandTransformation rooCommand = new CommandTransformation( + "project --topLevelPackage com.foo.sample --projectName test --domain otherdomainname"); + final String result = TailorHelper + .replaceVars(rooCommand, + "module create --moduleName ${domain} --topLevelPackage ${topLevelPackage}"); + final String expectedResult = "module create --moduleName otherdomainname --topLevelPackage com.foo.sample"; + Assert.assertEquals("Unexpected result: " + result, expectedResult, + result); + } + + /** + * Test replaceVars when there is more than one occurence of the same + * placeholder in the target + */ + @Test + public void testReplaceVarsDuplicatePlaceholders() { + final CommandTransformation rooCommand = new CommandTransformation( + "project --topLevelPackage com.foo.sample --projectName test --domain otherdomainname"); + final String result = TailorHelper + .replaceVars( + rooCommand, + "module create --moduleName ${domain} --topLevelPackage ${topLevelPackage}.${domain}"); + final String expectedResult = "module create --moduleName otherdomainname --topLevelPackage com.foo.sample.otherdomainname"; + Assert.assertEquals("Unexpected result: " + result, expectedResult, + result); + } + + /** + * Test for "unnamed argument": Use of ${*} as placeholder should result in + * replacing it with the last fragment of the trigger command, i.e. the + * first "unnamed" argument. + */ + @Test + public void testReplaceVarsForUnnamedArgument() { + final CommandTransformation rooCommand = new CommandTransformation( + "cd test-data"); + final String result = TailorHelper.replaceVars(rooCommand, + "module focus --moduleName ${*}"); + Assert.assertTrue("* was not replaced: " + result, + result.endsWith("test-data")); + } + + /** + * Test makes sure that the regex used by TailorHelper does not match + * placeholders that are similar, but different. (Prefix and suffix) + */ + @Test + public void testReplaceVarsSimilarPlaceholders() { + final CommandTransformation rooCommand = new CommandTransformation( + "project --topLevelPackage com.foo.sample --projectName test --domain otherdomainname"); + final String result = TailorHelper + .replaceVars(rooCommand, + "module create --moduleName ${xdomain} --topLevelPackage ${topLevelPackagex}"); + final String expectedResult = "module create --moduleName ${xdomain} --topLevelPackage ${topLevelPackagex}"; + Assert.assertEquals("Unexpected result: " + result, expectedResult, + result); + } +} diff --git a/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TestTailorHelperRemoveComment.java b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TestTailorHelperRemoveComment.java new file mode 100644 index 000000000..8e0bb540a --- /dev/null +++ b/addon-tailor/src/test/java/org/springframework/roo/addon/tailor/util/TestTailorHelperRemoveComment.java @@ -0,0 +1,73 @@ +package org.springframework.roo.addon.tailor.util; + +import junit.framework.Assert; + +import org.junit.Test; + +/** + * Tests for {@link TailorHelper#removeComment(String, boolean)} + * + * @author Vladimir Tihomirov + */ +public class TestTailorHelperRemoveComment { + + /** + * Tests a block comment in the line + **/ + @Test + public void testBlockLine() { + final CommentedLine comment = new CommentedLine("test/*comment*/test", + false); + TailorHelper.removeComment(comment); + Assert.assertEquals("Unexpected result: " + comment.getLine(), + "testtest", comment.getLine()); + Assert.assertFalse(comment.getInBlockComment()); + } + + /** + * Tests a block comment in the script + **/ + @Test + public void testBlockScript() { + CommentedLine comment = new CommentedLine("start/*script comment", + false); + TailorHelper.removeComment(comment); + Assert.assertEquals("Unexpected result: " + comment.getLine(), "start", + comment.getLine()); + Assert.assertTrue(comment.getInBlockComment()); + comment = new CommentedLine("inblock comment", true); + TailorHelper.removeComment(comment); + Assert.assertEquals("Unexpected result: " + comment.getLine(), "", + comment.getLine()); + Assert.assertTrue(comment.getInBlockComment()); + comment = new CommentedLine("close comment*/stop", true); + TailorHelper.removeComment(comment); + Assert.assertEquals("Unexpected result: " + comment.getLine(), "stop", + comment.getLine()); + Assert.assertFalse(comment.getInBlockComment()); + } + + /** + * Tests a inline comment + **/ + @Test + public void testInLineHash() { + final CommentedLine comment = new CommentedLine("#comment", false); + TailorHelper.removeComment(comment); + Assert.assertEquals("Unexpected result: " + comment.getLine(), "", + comment.getLine()); + Assert.assertFalse(comment.getInBlockComment()); + } + + /** + * Tests a inline comment + **/ + @Test + public void testInLineSlash() { + final CommentedLine comment = new CommentedLine("//comment", false); + TailorHelper.removeComment(comment); + Assert.assertEquals("Unexpected result: " + comment.getLine(), "", + comment.getLine()); + Assert.assertFalse(comment.getInBlockComment()); + } +} diff --git a/addon-tailor/src/test/resources/org/springframework/roo/addon/tailor/config/xml/testtailor.xml b/addon-tailor/src/test/resources/org/springframework/roo/addon/tailor/config/xml/testtailor.xml new file mode 100644 index 000000000..8d0914a77 --- /dev/null +++ b/addon-tailor/src/test/resources/org/springframework/roo/addon/tailor/config/xml/testtailor.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-tailor/src/test/resources/pizzashop.roo b/addon-tailor/src/test/resources/pizzashop.roo new file mode 100644 index 000000000..43c989f53 --- /dev/null +++ b/addon-tailor/src/test/resources/pizzashop.roo @@ -0,0 +1,61 @@ +tailor activate --name web-simple +// Create a new project +project --topLevelPackage com.springsource.pizzashop --projectName pizzashop + +// Setup JPA persistence using EclipseLink and H2 +jpa setup --provider ECLIPSELINK --database H2_IN_MEMORY + +// Create domain entities +entity jpa --class ~.domain.Base --activeRecord false --testAutomatically +field string --fieldName name --sizeMin 2 --notNull + +entity jpa --class ~.domain.Topping --activeRecord false --testAutomatically +field string --fieldName name --sizeMin 2 --notNull + +entity jpa --class ~.domain.Pizza --activeRecord false --testAutomatically +field string --fieldName name --notNull --sizeMin 2 +field number --fieldName price --type java.math.BigDecimal +field set --fieldName toppings --type ~.domain.Topping +field reference --fieldName base --type ~.domain.Base + +entity jpa --class ~.domain.PizzaOrder --testAutomatically --activeRecord false --identifierType ~.domain.PizzaOrderPk +field string --fieldName name --notNull --sizeMin 2 +field string --fieldName address --sizeMax 30 +field number --fieldName total --type java.math.BigDecimal +field date --fieldName deliveryDate --type java.util.Date +field set --fieldName pizzas --type ~.domain.Pizza + +field string --fieldName shopCountry --class ~.domain.PizzaOrderPk +field string --fieldName shopCity +field string --fieldName shopName + +// Define a repository layer for persistence +repository jpa --interface ~.repository.ToppingRepository --entity ~.domain.Topping +repository jpa --interface ~.repository.BaseRepository --entity ~.domain.Base +repository jpa --interface ~.repository.PizzaRepository --entity ~.domain.Pizza +repository jpa --interface ~.repository.PizzaOrderRepository --entity ~.domain.PizzaOrder + +// Define a service/facade layer +service type --interface ~.service.ToppingService --entity ~.domain.Topping +service type --interface ~.service.BaseService --entity ~.domain.Base +service type --interface ~.service.PizzaService --entity ~.domain.Pizza +service type --interface ~.service.PizzaOrderService --entity ~.domain.PizzaOrder + +// Offer JSON remoting for all domain types through Spring MVC +json all --deepSerialize +web mvc json setup +web mvc json all --package ~.web + +web mvc setup +web mvc all --package ~.web + +// Example scripts for JSON remoting: +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{name: "Thin Crust"}' http://localhost:8080/pizzashop/bases +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '[{name: "Cheesy Crust"},{name: "Thick Crust"}]' http://localhost:8080/pizzashop/bases/jsonArray +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '[{name: "Fresh Tomato"},{name: "Prawns"},{name: "Mozarella"},{name: "Bogus"}]' http://localhost:8080/pizzashop/toppings/jsonArray +// curl -i -X DELETE -H "Accept: application/json" http://localhost:8080/pizzashop/toppings/7 +// curl -i -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -d '{id:6,name:"Mozzarella",version:1}' http://localhost:8080/pizzashop/toppings +// curl -i -H "Accept: application/json" http://localhost:8080/pizzashop/toppings +// curl -i -H "Accept: application/json" http://localhost:8080/pizzashop/toppings/6 +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{name:"Napolitana",price:7.5,base:{id:1},toppings:[{name: "Anchovy fillets"},{name: "Mozzarella"}]}' http://localhost:8080/pizzashop/pizzas +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{name:"Stefan",total:7.5,address:"Sydney, AU",deliveryDate:1314595427866,id:{shopCountry:"AU",shopCity:"Sydney",shopName:"Pizza Pan 1"},pizzas:[{id:8,version:1}]}' http://localhost:8080/pizzashop/pizzaorders \ No newline at end of file diff --git a/addon-tailor/src/test/resources/tailor.xml b/addon-tailor/src/test/resources/tailor.xml new file mode 100644 index 000000000..63ef2bf5b --- /dev/null +++ b/addon-tailor/src/test/resources/tailor.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-test/pom.xml b/addon-test/pom.xml new file mode 100644 index 000000000..bf4a63879 --- /dev/null +++ b/addon-test/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.test + bundle + Spring Roo - Addon - Automated Integration Testing + Integration of JUnit integration tests in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.dod + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestAnnotationValues.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestAnnotationValues.java new file mode 100644 index 000000000..a23507669 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestAnnotationValues.java @@ -0,0 +1,79 @@ +package org.springframework.roo.addon.test; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooIntegrationTest} annotation. + * + * @author Ben Alex + * @since 1.0 + */ +public class IntegrationTestAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private boolean count = true; + @AutoPopulate private JavaType entity; + @AutoPopulate private boolean find = true; + @AutoPopulate private boolean findAll = true; + @AutoPopulate private int findAllMaximum = 250; + @AutoPopulate private boolean findEntries = true; + @AutoPopulate private boolean flush = true; + @AutoPopulate private boolean merge = true; + @AutoPopulate private boolean persist = true; + @AutoPopulate private boolean remove = true; + @AutoPopulate private boolean transactional = true; + + public IntegrationTestAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_INTEGRATION_TEST); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public JavaType getEntity() { + return entity; + } + + public int getFindAllMaximum() { + return findAllMaximum; + } + + public boolean isCount() { + return count; + } + + public boolean isFind() { + return find; + } + + public boolean isFindAll() { + return findAll; + } + + public boolean isFindEntries() { + return findEntries; + } + + public boolean isFlush() { + return flush; + } + + public boolean isMerge() { + return merge; + } + + public boolean isPersist() { + return persist; + } + + public boolean isRemove() { + return remove; + } + + public boolean isTransactional() { + return transactional; + } +} diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestCommands.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestCommands.java new file mode 100644 index 000000000..58be44e07 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestCommands.java @@ -0,0 +1,75 @@ +package org.springframework.roo.addon.test; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.ReservedWords; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Shell commands for {@link IntegrationTestOperationsImpl}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class IntegrationTestCommands implements CommandMarker { + + @Reference private IntegrationTestOperations integrationTestOperations; + + @CliAvailabilityIndicator({ "test integration", "test mock", "test stub" }) + public boolean isPersistentClassAvailable() { + return integrationTestOperations + .isIntegrationTestInstallationPossible(); + } + + @CliCommand(value = "test integration", help = "Creates a new integration test for the specified entity") + public void newIntegrationTest( + @CliOption(key = "entity", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the entity to create an integration test for") final JavaType entity, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords, + @CliOption(key = "transactional", mandatory = false, unspecifiedDefaultValue = "true", specifiedDefaultValue = "true", help = "Indicates whether the created test cases should be run withing a Spring transaction") final boolean transactional) { + + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(entity); + } + + Validate.isTrue( + BeanInfoUtils.isEntityReasonablyNamed(entity), + "Cannot create an integration test for an entity named 'Test' or 'TestCase' under any circumstances"); + + integrationTestOperations.newIntegrationTest(entity, transactional); + } + + @CliCommand(value = "test mock", help = "Creates a mock test for the specified entity") + public void newMockTest( + @CliOption(key = "entity", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the entity this mock test is targeting") final JavaType entity, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(entity); + } + + integrationTestOperations.newMockTest(entity); + } + + @CliCommand(value = "test stub", help = "Creates a test stub for the specified class") + public void newTestStub( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class this mock test is targeting") final JavaType javaType, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(javaType); + } + + integrationTestOperations.newTestStub(javaType); + } +} diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadata.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadata.java new file mode 100644 index 000000000..3c3d16c88 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadata.java @@ -0,0 +1,970 @@ +package org.springframework.roo.addon.test; + +import static org.springframework.roo.model.GoogleJavaType.GAE_LOCAL_SERVICE_TEST_HELPER; +import static org.springframework.roo.model.JdkJavaType.ITERATOR; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.Jsr303JavaType.CONSTRAINT_VIOLATION; +import static org.springframework.roo.model.Jsr303JavaType.CONSTRAINT_VIOLATION_EXCEPTION; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; +import static org.springframework.roo.model.SpringJavaType.CONTEXT_CONFIGURATION; +import static org.springframework.roo.model.SpringJavaType.PROPAGATION; +import static org.springframework.roo.model.SpringJavaType.TRANSACTIONAL; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.dod.DataOnDemandMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooIntegrationTest}. + * + * @author Ben Alex + * @since 1.0 + */ +public class IntegrationTestMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final JavaType AFTER_CLASS = new JavaType( + "org.junit.AfterClass"); + private static final JavaType ASSERT = new JavaType("org.junit.Assert"); + + private static final JavaType BEFORE_CLASS = new JavaType( + "org.junit.BeforeClass"); + private static final String PROVIDES_TYPE_STRING = IntegrationTestMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final JavaType RUN_WITH = new JavaType( + "org.junit.runner.RunWith"); + private static final JavaType[] SETUP_PARAMETERS = {}; + private static final JavaType[] TEARDOWN_PARAMETERS = {}; + private static final JavaType TEST = new JavaType("org.junit.Test"); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private IntegrationTestAnnotationValues annotationValues; + private DataOnDemandMetadata dataOnDemandMetadata; + private boolean entityHasSuperclass; + private boolean hasEmbeddedIdentifier; + private boolean isGaeSupported = false; + private String transactionManager; + + public IntegrationTestMetadata(final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final IntegrationTestAnnotationValues annotationValues, + final DataOnDemandMetadata dataOnDemandMetadata, + final MethodMetadata identifierAccessorMethod, + final MethodMetadata versionAccessorMethod, + final MemberTypeAdditions countMethod, + final MemberTypeAdditions findMethod, + final MemberTypeAdditions findAllMethod, + final MemberTypeAdditions findEntriesMethod, + final MemberTypeAdditions flushMethod, + final MemberTypeAdditions mergeMethod, + final MemberTypeAdditions persistMethod, + final MemberTypeAdditions removeMethod, + final String transactionManager, + final boolean hasEmbeddedIdentifier, + final boolean entityHasSuperclass, final boolean isGaeEnabled) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(dataOnDemandMetadata, + "Data on demand metadata required"); + + if (!isValid()) { + return; + } + + this.annotationValues = annotationValues; + this.dataOnDemandMetadata = dataOnDemandMetadata; + this.transactionManager = transactionManager; + this.hasEmbeddedIdentifier = hasEmbeddedIdentifier; + this.entityHasSuperclass = entityHasSuperclass; + + addRequiredIntegrationTestClassIntroductions(DataOnDemandMetadata + .getJavaType(dataOnDemandMetadata.getId())); + + // Add GAE LocalServiceTestHelper instance and @BeforeClass/@AfterClass + // methods if GAE is enabled + if (isGaeEnabled) { + isGaeSupported = true; + addOptionalIntegrationTestClassIntroductions(); + } + + builder.addMethod(getCountMethodTest(countMethod)); + builder.addMethod(getFindMethodTest(findMethod, + identifierAccessorMethod)); + builder.addMethod(getFindAllMethodTest(findAllMethod, countMethod)); + builder.addMethod(getFindEntriesMethodTest(countMethod, + findEntriesMethod)); + if (flushMethod != null) { + builder.addMethod(getFlushMethodTest(versionAccessorMethod, + identifierAccessorMethod, flushMethod, findMethod)); + } + builder.addMethod(getMergeMethodTest(mergeMethod, findMethod, + flushMethod, versionAccessorMethod, identifierAccessorMethod)); + builder.addMethod(getPersistMethodTest(persistMethod, flushMethod, + identifierAccessorMethod)); + builder.addMethod(getRemoveMethodTest(removeMethod, findMethod, + flushMethod, identifierAccessorMethod)); + + itdTypeDetails = builder.build(); + } + + private void addOptionalIntegrationTestClassIntroductions() { + // Add the GAE test helper field if the user did not define it on the + // governor directly + final JavaType helperType = GAE_LOCAL_SERVICE_TEST_HELPER; + final FieldMetadata helperField = governorTypeDetails + .getField(new JavaSymbolName("helper")); + if (helperField != null) { + Validate.isTrue( + helperField.getFieldType().getFullyQualifiedTypeName() + .equals(helperType.getFullyQualifiedTypeName()), + "Field 'helper' on '%s' must be of type '%s'", + destination.getFullyQualifiedTypeName(), + helperType.getFullyQualifiedTypeName()); + } + else { + // Add the field via the ITD + final String initializer = "new LocalServiceTestHelper(new com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig())"; + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId(), Modifier.PRIVATE | Modifier.STATIC + | Modifier.FINAL, new JavaSymbolName("helper"), + helperType, initializer); + builder.addField(fieldBuilder); + } + + // Prepare setUp method signature + final JavaSymbolName setUpMethodName = new JavaSymbolName("setUp"); + final MethodMetadata setUpMethod = getGovernorMethod(setUpMethodName, + SETUP_PARAMETERS); + if (setUpMethod != null) { + Validate.notNull( + MemberFindingUtils.getAnnotationOfType( + setUpMethod.getAnnotations(), BEFORE_CLASS), + "Method 'setUp' on '%s' must be annotated with @BeforeClass", + destination.getFullyQualifiedTypeName()); + } + else { + // Add the method via the ITD + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(BEFORE_CLASS)); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("helper.setUp();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC | Modifier.STATIC, + setUpMethodName, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(SETUP_PARAMETERS), + new ArrayList(), bodyBuilder); + methodBuilder.setAnnotations(annotations); + builder.addMethod(methodBuilder); + } + + // Prepare tearDown method signature + final JavaSymbolName tearDownMethodName = new JavaSymbolName("tearDown"); + final MethodMetadata tearDownMethod = getGovernorMethod( + tearDownMethodName, TEARDOWN_PARAMETERS); + if (tearDownMethod != null) { + Validate.notNull( + MemberFindingUtils.getAnnotationOfType( + tearDownMethod.getAnnotations(), AFTER_CLASS), + "Method 'tearDown' on '%s' must be annotated with @AfterClass", + destination.getFullyQualifiedTypeName()); + } + else { + // Add the method via the ITD + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(AFTER_CLASS)); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("helper.tearDown();"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), + Modifier.PUBLIC | Modifier.STATIC, + tearDownMethodName, + JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(TEARDOWN_PARAMETERS), + new ArrayList(), bodyBuilder); + methodBuilder.setAnnotations(annotations); + builder.addMethod(methodBuilder); + } + } + + /** + * Adds the JUnit and Spring type level annotations if needed + */ + private void addRequiredIntegrationTestClassIntroductions( + final JavaType dodGovernor) { + // Add an @RunWith(SpringJunit4ClassRunner) annotation to the type, if + // the user did not define it on the governor directly + if (MemberFindingUtils.getAnnotationOfType( + governorTypeDetails.getAnnotations(), RUN_WITH) == null) { + final AnnotationMetadataBuilder runWithBuilder = new AnnotationMetadataBuilder( + RUN_WITH); + runWithBuilder + .addClassAttribute("value", + "org.springframework.test.context.junit4.SpringJUnit4ClassRunner"); + builder.addAnnotation(runWithBuilder); + } + + // Add an @ContextConfiguration("classpath:/applicationContext.xml") + // annotation to the type, if the user did not define it on the governor + // directly + if (MemberFindingUtils.getAnnotationOfType( + governorTypeDetails.getAnnotations(), CONTEXT_CONFIGURATION) == null) { + final AnnotationMetadataBuilder contextConfigurationBuilder = new AnnotationMetadataBuilder( + CONTEXT_CONFIGURATION); + contextConfigurationBuilder.addStringAttribute("locations", + "classpath*:/META-INF/spring/applicationContext*.xml"); + builder.addAnnotation(contextConfigurationBuilder); + } + + // Add an @Transactional, if the user did not define it on the governor + // directly + if (annotationValues.isTransactional() + && MemberFindingUtils.getAnnotationOfType( + governorTypeDetails.getAnnotations(), TRANSACTIONAL) == null) { + final AnnotationMetadataBuilder transactionalBuilder = new AnnotationMetadataBuilder( + TRANSACTIONAL); + if (StringUtils.isNotBlank(transactionManager) + && !"transactionManager".equals(transactionManager)) { + transactionalBuilder.addStringAttribute("value", + transactionManager); + } + builder.addAnnotation(transactionalBuilder); + } + + // Add the data on demand field if the user did not define it on the + // governor directly + final FieldMetadata field = governorTypeDetails + .getField(new JavaSymbolName("dod")); + if (field != null) { + Validate.isTrue(field.getFieldType().equals(dodGovernor), + "Field 'dod' on '%s' must be of type '%s'", + destination.getFullyQualifiedTypeName(), + dodGovernor.getFullyQualifiedTypeName()); + Validate.notNull( + MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), AUTOWIRED), + "Field 'dod' on '%s' must be annotated with @Autowired", + destination.getFullyQualifiedTypeName()); + } + else { + // Add the field via the ITD + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(AUTOWIRED)); + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + getId(), 0, annotations, new JavaSymbolName("dod"), + dodGovernor); + builder.addField(fieldBuilder); + } + + builder.getImportRegistrationResolver().addImport(ASSERT); + } + + /** + * @return a test for the count method, if available and requested (may + * return null) + */ + private MethodMetadataBuilder getCountMethodTest( + final MemberTypeAdditions countMethod) { + if (!annotationValues.isCount() || countMethod == null) { + // User does not want this method + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(countMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", dod." + + dataOnDemandMetadata + .getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "());"); + bodyBuilder.appendFormalLine("long count = " + + countMethod.getMethodCall() + ";"); + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Counter for '" + + entityName + + "' incorrectly reported there were no entries\", count > 0);"); + + countMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the find all method, if available and requested (may + * return null) + */ + private MethodMetadataBuilder getFindAllMethodTest( + final MemberTypeAdditions findAllMethod, + final MemberTypeAdditions countMethod) { + if (!annotationValues.isFindAll() || findAllMethod == null + || countMethod == null) { + // User does not want this method, or core dependencies are missing + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(findAllMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImport(LIST); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", dod." + + dataOnDemandMetadata + .getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "());"); + bodyBuilder.appendFormalLine("long count = " + + countMethod.getMethodCall() + ";"); + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Too expensive to perform a find all test for '" + + entityName + + "', as there are \" + count + \" entries; set the findAllMaximum to exceed this value or set findAll=false on the integration test annotation to disable the test\", count < " + + annotationValues.getFindAllMaximum() + ");"); + bodyBuilder.appendFormalLine("List<" + entityName + "> result = " + + findAllMethod.getMethodCall() + ";"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Find all method for '" + + entityName + "' illegally returned null\", result);"); + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Find all method for '" + + entityName + + "' failed to return any data\", result.size() > 0);"); + + findAllMethod.copyAdditionsTo(builder, governorTypeDetails); + countMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the find entries method, if available and requested + * (may return null) + */ + private MethodMetadataBuilder getFindEntriesMethodTest( + final MemberTypeAdditions countMethod, + final MemberTypeAdditions findEntriesMethod) { + if (!annotationValues.isFindEntries() || countMethod == null + || findEntriesMethod == null) { + // User does not want this method, or core dependencies are missing + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(findEntriesMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImport(LIST); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", dod." + + dataOnDemandMetadata + .getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "());"); + bodyBuilder.appendFormalLine("long count = " + + countMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("if (count > 20) count = 20;"); + bodyBuilder.appendFormalLine("int firstResult = 0;"); + bodyBuilder.appendFormalLine("int maxResults = (int) count;"); + bodyBuilder.appendFormalLine("List<" + entityName + "> result = " + + findEntriesMethod.getMethodCall() + ";"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Find entries method for '" + + entityName + "' illegally returned null\", result);"); + bodyBuilder + .appendFormalLine("Assert.assertEquals(\"Find entries method for '" + + entityName + + "' returned an incorrect number of entries\", count, result.size());"); + + findEntriesMethod.copyAdditionsTo(builder, governorTypeDetails); + countMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the find (by ID) method, if available and requested + * (may return null) + */ + private MethodMetadataBuilder getFindMethodTest( + final MemberTypeAdditions findMethod, + final MethodMetadata identifierAccessorMethod) { + if (!annotationValues.isFind() || findMethod == null + || identifierAccessorMethod == null) { + // User does not want this method + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(findMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImport( + identifierAccessorMethod.getReturnType()); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName + + " obj = dod." + + dataOnDemandMetadata.getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", obj);"); + bodyBuilder.appendFormalLine(identifierAccessorMethod.getReturnType() + .getSimpleTypeName() + + " id = obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to provide an identifier\", id);"); + bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + + ";"); + bodyBuilder.appendFormalLine("Assert.assertNotNull(\"Find method for '" + + entityName + + "' illegally returned null for id '\" + id + \"'\", obj);"); + bodyBuilder.appendFormalLine("Assert.assertEquals(\"Find method for '" + + entityName + + "' returned the incorrect identifier\", id, obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "());"); + + findMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the flush method, if available and requested (may + * return null) + */ + private MethodMetadataBuilder getFlushMethodTest( + final MethodMetadata versionAccessorMethod, + final MethodMetadata identifierAccessorMethod, + final MemberTypeAdditions flushMethod, + final MemberTypeAdditions findMethod) { + if (!annotationValues.isFlush() || versionAccessorMethod == null + || identifierAccessorMethod == null || flushMethod == null + || findMethod == null) { + // User does not want this method, or core dependencies are missing + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(flushMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType versionType = versionAccessorMethod.getReturnType(); + builder.getImportRegistrationResolver().addImports( + identifierAccessorMethod.getReturnType(), versionType); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName + + " obj = dod." + + dataOnDemandMetadata.getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", obj);"); + bodyBuilder.appendFormalLine(identifierAccessorMethod.getReturnType() + .getSimpleTypeName() + + " id = obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to provide an identifier\", id);"); + bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + + ";"); + bodyBuilder.appendFormalLine("Assert.assertNotNull(\"Find method for '" + + entityName + + "' illegally returned null for id '\" + id + \"'\", obj);"); + bodyBuilder.appendFormalLine("boolean modified = dod." + + dataOnDemandMetadata.getModifyMethod().getMethodName() + .getSymbolName() + "(obj);"); + + bodyBuilder + .appendFormalLine(versionAccessorMethod.getReturnType() + .getSimpleTypeName() + + " currentVersion = obj." + + versionAccessorMethod.getMethodName().getSymbolName() + + "();"); + bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";"); + if (JdkJavaType.isDateField(versionType)) { + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Version for '" + + entityName + + "' failed to increment on flush directive\", (currentVersion != null && obj." + + versionAccessorMethod.getMethodName() + .getSymbolName() + + "().after(currentVersion)) || !modified);"); + } + else if (JavaType.STRING.equals(versionType)) { + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Version for '" + + entityName + + "' failed to increment on flush directive\", (currentVersion != null && !currentVersion.equals(obj." + + versionAccessorMethod.getMethodName() + .getSymbolName() + "())) || !modified);"); + } + else { + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Version for '" + + entityName + + "' failed to increment on flush directive\", (currentVersion != null && obj." + + versionAccessorMethod.getMethodName() + .getSymbolName() + + "() > currentVersion) || !modified);"); + } + flushMethod.copyAdditionsTo(builder, governorTypeDetails); + findMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the merge method, if available and requested (may + * return null) + */ + private MethodMetadataBuilder getMergeMethodTest( + final MemberTypeAdditions mergeMethod, + final MemberTypeAdditions findMethod, + final MemberTypeAdditions flushMethod, + final MethodMetadata versionAccessorMethod, + final MethodMetadata identifierAccessorMethod) { + if (!annotationValues.isMerge() || mergeMethod == null + || versionAccessorMethod == null || findMethod == null + || identifierAccessorMethod == null) { + // User does not want this method, or core dependencies are missing + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(mergeMethod.getMethodName()) + + "Update"); + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType versionType = versionAccessorMethod.getReturnType(); + builder.getImportRegistrationResolver().addImports( + identifierAccessorMethod.getReturnType(), versionType); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName + + " obj = dod." + + dataOnDemandMetadata.getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", obj);"); + bodyBuilder.appendFormalLine(identifierAccessorMethod.getReturnType() + .getSimpleTypeName() + + " id = obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to provide an identifier\", id);"); + bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + + ";"); + bodyBuilder.appendFormalLine("boolean modified = dod." + + dataOnDemandMetadata.getModifyMethod().getMethodName() + .getSymbolName() + "(obj);"); + + bodyBuilder + .appendFormalLine(versionAccessorMethod.getReturnType() + .getSimpleTypeName() + + " currentVersion = obj." + + versionAccessorMethod.getMethodName().getSymbolName() + + "();"); + + final String castStr = entityHasSuperclass ? "(" + entityName + ")" + : ""; + bodyBuilder.appendFormalLine(entityName + " merged = " + castStr + + mergeMethod.getMethodCall() + ";"); + + if (flushMethod != null) { + bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";"); + flushMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + bodyBuilder + .appendFormalLine("Assert.assertEquals(\"Identifier of merged object not the same as identifier of original object\", merged." + + identifierAccessorMethod.getMethodName() + .getSymbolName() + "(), id);"); + if (JdkJavaType.isDateField(versionType)) { + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Version for '" + + entityName + + "' failed to increment on merge and flush directive\", (currentVersion != null && obj." + + versionAccessorMethod.getMethodName() + .getSymbolName() + + "().after(currentVersion)) || !modified);"); + } + else if (JavaType.STRING.equals(versionType)) { + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Version for '" + + entityName + + "' failed to increment on flush directive\", (currentVersion != null && !currentVersion.equals(obj." + + versionAccessorMethod.getMethodName() + .getSymbolName() + "())) || !modified);"); + } + else { + bodyBuilder + .appendFormalLine("Assert.assertTrue(\"Version for '" + + entityName + + "' failed to increment on merge and flush directive\", (currentVersion != null && obj." + + versionAccessorMethod.getMethodName() + .getSymbolName() + + "() > currentVersion) || !modified);"); + } + mergeMethod.copyAdditionsTo(builder, governorTypeDetails); + findMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the persist method, if available and requested (may + * return null) + */ + private MethodMetadataBuilder getPersistMethodTest( + final MemberTypeAdditions persistMethod, + final MemberTypeAdditions flushMethod, + final MethodMetadata identifierAccessorMethod) { + if (!annotationValues.isPersist() || persistMethod == null + || identifierAccessorMethod == null) { + // User does not want this method + return null; + } + + builder.getImportRegistrationResolver().addImports(ITERATOR, + CONSTRAINT_VIOLATION_EXCEPTION, CONSTRAINT_VIOLATION); + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(persistMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", dod." + + dataOnDemandMetadata + .getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "());"); + bodyBuilder.appendFormalLine(entityName + + " obj = dod." + + dataOnDemandMetadata.getNewTransientEntityMethod() + .getMethodName().getSymbolName() + + "(Integer.MAX_VALUE);"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to provide a new transient entity\", obj);"); + + if (!hasEmbeddedIdentifier) { + bodyBuilder.appendFormalLine("Assert.assertNull(\"Expected '" + + entityName + "' identifier to be null\", obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "());"); + } + + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";"); + bodyBuilder.indentRemove(); + bodyBuilder + .appendFormalLine("} catch (final ConstraintViolationException e) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("final StringBuilder msg = new StringBuilder();"); + bodyBuilder + .appendFormalLine("for (Iterator> iter = e.getConstraintViolations().iterator(); iter.hasNext();) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("final ConstraintViolation cv = iter.next();"); + bodyBuilder + .appendFormalLine("msg.append(\"[\").append(cv.getRootBean().getClass().getName()).append(\".\").append(cv.getPropertyPath()).append(\": \").append(cv.getMessage()).append(\" (invalid value = \").append(cv.getInvalidValue()).append(\")\").append(\"]\");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("throw new IllegalStateException(msg.toString(), e);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + if (flushMethod != null) { + bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";"); + flushMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + bodyBuilder.appendFormalLine("Assert.assertNotNull(\"Expected '" + + entityName + "' identifier to no longer be null\", obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "());"); + + persistMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * @return a test for the persist method, if available and requested (may + * return null) + */ + private MethodMetadataBuilder getRemoveMethodTest( + final MemberTypeAdditions removeMethod, + final MemberTypeAdditions findMethod, + final MemberTypeAdditions flushMethod, + final MethodMetadata identifierAccessorMethod) { + if (!annotationValues.isRemove() || removeMethod == null + || findMethod == null || identifierAccessorMethod == null) { + // User does not want this method or one of its core dependencies + return null; + } + + // Prepare method signature + final JavaSymbolName methodName = new JavaSymbolName("test" + + StringUtils.capitalize(removeMethod.getMethodName())); + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImport( + identifierAccessorMethod.getReturnType()); + + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(TEST)); + if (isGaeSupported) { + final AnnotationMetadataBuilder transactionalBuilder = new AnnotationMetadataBuilder( + TRANSACTIONAL); + if (StringUtils.isNotBlank(transactionManager) + && !"transactionManager".equals(transactionManager)) { + transactionalBuilder.addStringAttribute("value", + transactionManager); + } + transactionalBuilder + .addEnumAttribute("propagation", new EnumDetails( + PROPAGATION, new JavaSymbolName("SUPPORTS"))); + annotations.add(transactionalBuilder); + } + + final String entityName = annotationValues.getEntity() + .getSimpleTypeName(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(entityName + + " obj = dod." + + dataOnDemandMetadata.getRandomPersistentEntityMethod() + .getMethodName().getSymbolName() + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to initialize correctly\", obj);"); + bodyBuilder.appendFormalLine(identifierAccessorMethod.getReturnType() + .getSimpleTypeName() + + " id = obj." + + identifierAccessorMethod.getMethodName().getSymbolName() + + "();"); + bodyBuilder + .appendFormalLine("Assert.assertNotNull(\"Data on demand for '" + + entityName + + "' failed to provide an identifier\", id);"); + bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + + ";"); + bodyBuilder.appendFormalLine(removeMethod.getMethodCall() + ";"); + + if (flushMethod != null) { + bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";"); + flushMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + bodyBuilder.appendFormalLine("Assert.assertNull(\"Failed to remove '" + + entityName + "' with identifier '\" + id + \"'\", " + + findMethod.getMethodCall() + ");"); + + removeMethod.copyAdditionsTo(builder, governorTypeDetails); + findMethod.copyAdditionsTo(builder, governorTypeDetails); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} \ No newline at end of file diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadataProvider.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadataProvider.java new file mode 100644 index 000000000..3d844f833 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.test; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link IntegrationTestMetadata}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface IntegrationTestMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadataProviderImpl.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadataProviderImpl.java new file mode 100644 index 000000000..5eb1c4ab6 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestMetadataProviderImpl.java @@ -0,0 +1,466 @@ +package org.springframework.roo.addon.test; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FLUSH_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSISTENT_TYPE; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.RooJavaType.ROO_DATA_ON_DEMAND; +import static org.springframework.roo.model.RooJavaType.ROO_INTEGRATION_TEST; +import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.configurable.ConfigurableMetadataProvider; +import org.springframework.roo.addon.dod.DataOnDemandMetadata; +import org.springframework.roo.classpath.PhysicalTypeDetails; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link IntegrationTestMetadataProvider}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class IntegrationTestMetadataProviderImpl extends + AbstractItdMetadataProvider implements IntegrationTestMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(IntegrationTestMetadataProviderImpl.class); + + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + private static final JavaSymbolName TRANSACTION_MANAGER_ATTRIBUTE = new JavaSymbolName( + "transactionManager"); + + private ConfigurableMetadataProvider configurableMetadataProvider; + private LayerService layerService; + private ProjectOperations projectOperations; + + private final Map managedEntityTypes = new HashMap(); + private final Set producedMids = new LinkedHashSet(); + private Boolean wasGaeEnabled; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + // Integration test classes are @Configurable because they may need DI + // of other DOD classes that provide M:1 relationships + getConfigurableMetadataProvider().addMetadataTrigger(ROO_INTEGRATION_TEST); + addMetadataTrigger(ROO_INTEGRATION_TEST); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return IntegrationTestMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getConfigurableMetadataProvider() + .removeMetadataTrigger(ROO_INTEGRATION_TEST); + removeMetadataTrigger(ROO_INTEGRATION_TEST); + } + + /** + * Returns the {@link JavaType} for the given entity's "data on demand" + * class. + * + * @param entity the entity for which to get the DoD type + * @return a non-null type (which may or may not exist yet) + */ + private JavaType getDataOnDemandType(final JavaType entity) { + // First check for an existing type with the standard DoD naming + // convention + final JavaType defaultDodType = new JavaType( + entity.getFullyQualifiedTypeName() + "DataOnDemand"); + if (getTypeLocationService().getTypeDetails(defaultDodType) != null) { + return defaultDodType; + } + + // Otherwise we look through all DoD-annotated classes for this entity's + // one + for (final ClassOrInterfaceTypeDetails dodType : getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(ROO_DATA_ON_DEMAND)) { + final AnnotationMetadata dodAnnotation = MemberFindingUtils + .getFirstAnnotation(dodType, ROO_DATA_ON_DEMAND); + if (dodAnnotation != null + && dodAnnotation.getAttribute("entity").getValue() + .equals(entity)) { + return dodType.getName(); + } + } + + // No existing DoD class was found for this entity, so use the default + // name + return defaultDodType; + } + + private ClassOrInterfaceTypeDetails getEntitySuperclass( + final JavaType entity) { + final String physicalTypeIdentifier = PhysicalTypeIdentifier + .createIdentifier(entity, + getTypeLocationService().getTypePath(entity)); + final PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) getMetadataService() + .get(physicalTypeIdentifier); + Validate.notNull(ptm, "Java source code unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + final PhysicalTypeDetails ptd = ptm.getMemberHoldingTypeDetails(); + Validate.notNull(ptd, + "Java source code details unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + Validate.isInstanceOf(ClassOrInterfaceTypeDetails.class, ptd, + "Java source code is immutable for type %s", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + final ClassOrInterfaceTypeDetails cid = (ClassOrInterfaceTypeDetails) ptd; + return cid.getSuperclass(); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = IntegrationTestMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = IntegrationTestMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "IntegrationTest"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + if(layerService == null){ + layerService = getLayerService(); + } + Validate.notNull(layerService, "LayerService is required"); + + // We need to parse the annotation, which we expect to be present + final IntegrationTestAnnotationValues annotationValues = new IntegrationTestAnnotationValues( + governorPhysicalTypeMetadata); + final JavaType entity = annotationValues.getEntity(); + if (!annotationValues.isAnnotationFound() || entity == null) { + return null; + } + + final JavaType dataOnDemandType = getDataOnDemandType(entity); + final String dataOnDemandMetadataKey = DataOnDemandMetadata + .createIdentifier(dataOnDemandType, + getTypeLocationService().getTypePath(dataOnDemandType)); + final DataOnDemandMetadata dataOnDemandMetadata = (DataOnDemandMetadata) getMetadataService() + .get(dataOnDemandMetadataKey); + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency(dataOnDemandMetadataKey, + metadataIdentificationString); + + if (dataOnDemandMetadata == null || !dataOnDemandMetadata.isValid()) { + return null; + } + + final JavaType identifierType = getPersistenceMemberLocator() + .getIdentifierType(entity); + if (identifierType == null) { + return null; + } + + final MemberDetails memberDetails = getMemberDetails(entity); + if (memberDetails == null) { + return null; + } + + final MemberHoldingTypeDetails persistenceMemberHoldingTypeDetails = MemberFindingUtils + .getMostConcreteMemberHoldingTypeDetailsWithTag(memberDetails, + PERSISTENT_TYPE); + if (persistenceMemberHoldingTypeDetails == null) { + return null; + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + persistenceMemberHoldingTypeDetails.getDeclaredByMetadataId(), + metadataIdentificationString); + + final MethodParameter firstResultParameter = new MethodParameter( + INT_PRIMITIVE, "firstResult"); + final MethodParameter maxResultsParameter = new MethodParameter( + INT_PRIMITIVE, "maxResults"); + + final MethodMetadata identifierAccessorMethod = memberDetails + .getMostConcreteMethodWithTag(IDENTIFIER_ACCESSOR_METHOD); + final MethodMetadata versionAccessorMethod = getPersistenceMemberLocator() + .getVersionAccessor(entity); + final MemberTypeAdditions countMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + COUNT_ALL_METHOD.name(), entity, identifierType, + LAYER_POSITION); + final MemberTypeAdditions findMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_METHOD.name(), entity, identifierType, + LAYER_POSITION, new MethodParameter(identifierType, + "id")); + final MemberTypeAdditions findAllMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_ALL_METHOD.name(), entity, identifierType, + LAYER_POSITION); + final MemberTypeAdditions findEntriesMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_ENTRIES_METHOD.name(), entity, identifierType, + LAYER_POSITION, firstResultParameter, + maxResultsParameter); + final MethodParameter entityParameter = new MethodParameter(entity, + "obj"); + final MemberTypeAdditions flushMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FLUSH_METHOD.name(), entity, identifierType, + LAYER_POSITION, entityParameter); + final MemberTypeAdditions mergeMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + MERGE_METHOD.name(), entity, identifierType, + LAYER_POSITION, entityParameter); + final MemberTypeAdditions persistMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + PERSIST_METHOD.name(), entity, identifierType, + LAYER_POSITION, entityParameter); + final MemberTypeAdditions removeMethodAdditions = layerService + .getMemberTypeAdditions(metadataIdentificationString, + REMOVE_METHOD.name(), entity, identifierType, + LAYER_POSITION, entityParameter); + if (persistMethodAdditions == null || findMethodAdditions == null + || identifierAccessorMethod == null) { + return null; + } + + String transactionManager = null; + final AnnotationMetadata jpaActiveRecordAnnotation = memberDetails + .getAnnotation(ROO_JPA_ACTIVE_RECORD); + if (jpaActiveRecordAnnotation != null) { + final StringAttributeValue transactionManagerAttr = (StringAttributeValue) jpaActiveRecordAnnotation + .getAttribute(TRANSACTION_MANAGER_ATTRIBUTE); + if (transactionManagerAttr != null) { + transactionManager = transactionManagerAttr.getValue(); + } + } + + final boolean hasEmbeddedIdentifier = dataOnDemandMetadata + .hasEmbeddedIdentifier(); + final boolean entityHasSuperclass = getEntitySuperclass(entity) != null; + + // In order to handle switching between GAE and JPA produced MIDs need + // to be remembered so they can be regenerated on JPA <-> GAE switch + producedMids.add(metadataIdentificationString); + + // Maintain a list of entities that are being tested + managedEntityTypes.put(entity, metadataIdentificationString); + + final String moduleName = PhysicalTypeIdentifierNamingUtils.getPath( + metadataIdentificationString).getModule(); + final boolean isGaeEnabled = projectOperations + .isProjectAvailable(moduleName) + && projectOperations.isFeatureInstalledInModule( + FeatureNames.GAE, moduleName); + + return new IntegrationTestMetadata(metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, annotationValues, + dataOnDemandMetadata, identifierAccessorMethod, + versionAccessorMethod, countMethodAdditions, + findMethodAdditions, findAllMethodAdditions, findEntriesMethod, + flushMethodAdditions, mergeMethodAdditions, + persistMethodAdditions, removeMethodAdditions, + transactionManager, hasEmbeddedIdentifier, entityHasSuperclass, + isGaeEnabled); + } + + public String getProvidesType() { + return IntegrationTestMetadata.getMetadataIdentiferType(); + } + + private void handleChangesToLayeringForTestedEntities( + final JavaType physicalType) { + final MemberHoldingTypeDetails memberHoldingTypeDetails = getTypeLocationService() + .getTypeDetails(physicalType); + if (memberHoldingTypeDetails != null) { + for (final JavaType type : memberHoldingTypeDetails + .getLayerEntities()) { + handleChangesToTestedEntities(type); + } + } + } + + private void handleChangesToTestedEntities(final JavaType physicalType) { + final String localMid = managedEntityTypes.get(physicalType); + if (localMid != null) { + // One of the entities for which we produce metadata has changed; + // refresh that metadata + getMetadataService().get(localMid); + } + } + + /** + * Handles a generic change (i.e. with no explicit downstream dependency) to + * the given physical type + * + * @param physicalType the type that changed (required) + */ + private void handleGenericChangeToPhysicalType(final JavaType physicalType) { + handleChangesToTestedEntities(physicalType); + handleChangesToLayeringForTestedEntities(physicalType); + } + + /** + * Handles a generic change (i.e. with no explicit downstream dependency) to + * the project metadata + */ + private void handleGenericChangeToProject(final String moduleName) { + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + Validate.notNull(projectOperations, "ProjectOperations is required"); + + final ProjectMetadata projectMetadata = projectOperations + .getProjectMetadata(moduleName); + if (projectMetadata != null && projectMetadata.isValid()) { + final boolean isGaeEnabled = projectOperations + .isFeatureInstalledInModule(FeatureNames.GAE, moduleName); + // We need to determine if the persistence state has changed, we do + // this by comparing the last known state to the current state + final boolean hasGaeStateChanged = wasGaeEnabled == null + || isGaeEnabled != wasGaeEnabled; + if (hasGaeStateChanged) { + wasGaeEnabled = isGaeEnabled; + for (final String producedMid : producedMids) { + getMetadataService().evictAndGet(producedMid); + } + } + } + } + + @Override + protected void notifyForGenericListener(final String upstreamDependency) { + if (PhysicalTypeIdentifier.isValid(upstreamDependency)) { + handleGenericChangeToPhysicalType(PhysicalTypeIdentifier + .getJavaType(upstreamDependency)); + } + if (ProjectMetadata.isValid(upstreamDependency)) { + handleGenericChangeToProject(ProjectMetadata + .getModuleName(upstreamDependency)); + } + } + + public ConfigurableMetadataProvider getConfigurableMetadataProvider(){ + if(configurableMetadataProvider == null){ + // Get all Services implement ConfigurableMetadataProvider interface + try { + ServiceReference[] references = context.getAllServiceReferences(ConfigurableMetadataProvider.class.getName(), null); + + for(ServiceReference ref : references){ + return (ConfigurableMetadataProvider) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ConfigurableMetadataProvider on IntegrationTestMetadataProviderImpl"); + return null; + } + }else{ + return configurableMetadataProvider; + } + + } + + public LayerService getLayerService(){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on IntegrationTestMetadataProviderImpl."); + return null; + } + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on IntegrationTestMetadataProviderImpl."); + return null; + } + } +} diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestOperations.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestOperations.java new file mode 100644 index 000000000..c80c5d381 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestOperations.java @@ -0,0 +1,54 @@ +package org.springframework.roo.addon.test; + +import org.springframework.roo.model.JavaType; + +/** + * Interface of {@link IntegrationTestOperationsImpl}. + * + * @author Ben Alex + */ +public interface IntegrationTestOperations { + + /** + * Checks for the existence the META-INF/persistence.xml + * + * @return true if the META-INF/persistence.xml exists, otherwise false + */ + boolean isIntegrationTestInstallationPossible(); + + /** + * Creates an integration test for the entity. Automatically produces a + * data-on-demand (DoD) class if one does not exist. Silently returns if the + * integration test file already exists. + * + * @param entity the entity to produce an integration test for (required) + */ + void newIntegrationTest(JavaType entity); + + /** + * Creates an integration test for the entity. Automatically produces a + * data-on-demand (DoD) class if one does not exist. Silently returns if the + * integration test file already exists. + * + * @param entity the entity to produce an integration test for (required) + * @param transactional indicates if the test case should be wrapped in a + * Spring transaction + */ + void newIntegrationTest(JavaType entity, boolean transactional); + + /** + * Creates a mock test for the entity. Silently returns if the mock test + * file already exists. + * + * @param entity to produce a mock test for (required) + */ + void newMockTest(JavaType entity); + + /** + * Creates a test stub for the class. Silently returns if the test file + * already exists. + * + * @param javaType to produce a test stub for (required) + */ + void newTestStub(JavaType javaType); +} \ No newline at end of file diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestOperationsImpl.java b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestOperationsImpl.java new file mode 100644 index 000000000..8ff6030e4 --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/IntegrationTestOperationsImpl.java @@ -0,0 +1,303 @@ +package org.springframework.roo.addon.test; + +import static org.springframework.roo.model.RooJavaType.ROO_INTEGRATION_TEST; +import static org.springframework.roo.model.SpringJavaType.MOCK_STATIC_ENTITY_METHODS; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.dod.DataOnDemandOperations; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * Provides convenience methods that can be used to create mock tests. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class IntegrationTestOperationsImpl implements IntegrationTestOperations { + + private static final JavaType JUNIT_4 = new JavaType( + "org.junit.runners.JUnit4"); + private static final JavaType RUN_WITH = new JavaType( + "org.junit.runner.RunWith"); + private static final JavaType TEST = new JavaType("org.junit.Test"); + + @Reference private DataOnDemandOperations dataOnDemandOperations; + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + /** + * @param entity the entity to lookup required + * @return the type details (never null; throws an exception if it cannot be + * obtained or parsed) + */ + private ClassOrInterfaceTypeDetails getEntity(final JavaType entity) { + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(entity); + Validate.notNull(cid, + "Java source code details unavailable for type %s", cid); + return cid; + } + + public boolean isIntegrationTestInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && projectOperations.isFeatureInstalledInFocusedModule( + FeatureNames.JPA, FeatureNames.MONGO); + } + + public void newIntegrationTest(final JavaType entity) { + newIntegrationTest(entity, true); + } + + public void newIntegrationTest(final JavaType entity, + final boolean transactional) { + Validate.notNull(entity, + "Entity to produce an integration test for is required"); + + // Verify the requested entity actually exists as a class and is not + // abstract + final ClassOrInterfaceTypeDetails cid = getEntity(entity); + Validate.isTrue(!Modifier.isAbstract(cid.getModifier()), + "Type %s is abstract", entity.getFullyQualifiedTypeName()); + + final LogicalPath path = PhysicalTypeIdentifier.getPath(cid + .getDeclaredByMetadataId()); + dataOnDemandOperations.newDod(entity, + new JavaType(entity.getFullyQualifiedTypeName() + + "DataOnDemand")); + + final JavaType name = new JavaType(entity + "IntegrationTest"); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, + Path.SRC_TEST_JAVA.getModulePathId(path.getModule())); + + if (metadataService.get(declaredByMetadataId) != null) { + // The file already exists + return; + } + + final List annotations = new ArrayList(); + final List> config = new ArrayList>(); + config.add(new ClassAttributeValue(new JavaSymbolName("entity"), entity)); + if (!transactional) { + config.add(new BooleanAttributeValue(new JavaSymbolName( + "transactional"), false)); + } + annotations.add(new AnnotationMetadataBuilder(ROO_INTEGRATION_TEST, + config)); + + final List methods = new ArrayList(); + final List methodAnnotations = new ArrayList(); + methodAnnotations.add(new AnnotationMetadataBuilder(TEST)); + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + declaredByMetadataId, Modifier.PUBLIC, new JavaSymbolName( + "testMarkerMethod"), JavaType.VOID_PRIMITIVE, + new InvocableMemberBodyBuilder()); + methodBuilder.setAnnotations(methodAnnotations); + methods.add(methodBuilder); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, name, + PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(annotations); + cidBuilder.setDeclaredMethods(methods); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + /** + * Creates a mock test for the entity. Silently returns if the mock test + * file already exists. + * + * @param entity to produce a mock test for (required) + */ + public void newMockTest(final JavaType entity) { + Validate.notNull(entity, + "Entity to produce a mock test for is required"); + + final JavaType name = new JavaType(entity + "Test"); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, Path.SRC_TEST_JAVA + .getModulePathId(projectOperations + .getFocusedModuleName())); + + if (metadataService.get(declaredByMetadataId) != null) { + // The file already exists + return; + } + + // Determine if the mocking infrastructure needs installing + final List annotations = new ArrayList(); + final List> config = new ArrayList>(); + config.add(new ClassAttributeValue(new JavaSymbolName("value"), JUNIT_4)); + annotations.add(new AnnotationMetadataBuilder(RUN_WITH, config)); + annotations.add(new AnnotationMetadataBuilder( + MOCK_STATIC_ENTITY_METHODS)); + + final List methods = new ArrayList(); + final List methodAnnotations = new ArrayList(); + methodAnnotations.add(new AnnotationMetadataBuilder(TEST)); + + // Get the entity so we can hopefully make a demo method that will be + // usable + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(entity); + if (cid != null) { + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails( + IntegrationTestOperationsImpl.class.getName(), cid); + final List countMethods = memberDetails + .getMethodsWithTag(CustomDataKeys.COUNT_ALL_METHOD); + if (countMethods.size() == 1) { + final String countMethod = entity.getSimpleTypeName() + "." + + countMethods.get(0).getMethodName().getSymbolName() + + "()"; + bodyBuilder.appendFormalLine("int expectedCount = 13;"); + bodyBuilder.appendFormalLine(countMethod + ";"); + bodyBuilder + .appendFormalLine("org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.expectReturn(expectedCount);"); + bodyBuilder + .appendFormalLine("org.springframework.mock.staticmock.AnnotationDrivenStaticEntityMockingControl.playback();"); + bodyBuilder + .appendFormalLine("org.junit.Assert.assertEquals(expectedCount, " + + countMethod + ");"); + } + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + declaredByMetadataId, Modifier.PUBLIC, new JavaSymbolName( + "testMethod"), JavaType.VOID_PRIMITIVE, bodyBuilder); + methodBuilder.setAnnotations(methodAnnotations); + methods.add(methodBuilder); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, name, + PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(annotations); + cidBuilder.setDeclaredMethods(methods); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void newTestStub(final JavaType javaType) { + Validate.notNull(javaType, + "Class to produce a test stub for is required"); + + final JavaType name = new JavaType(javaType + "Test"); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, Path.SRC_TEST_JAVA + .getModulePathId(projectOperations + .getFocusedModuleName())); + + if (metadataService.get(declaredByMetadataId) != null) { + // The file already exists + return; + } + + // Determine if the test infrastructure needs installing + final List annotations = new ArrayList(); + final List> config = new ArrayList>(); + config.add(new ClassAttributeValue(new JavaSymbolName("value"), JUNIT_4)); + annotations.add(new AnnotationMetadataBuilder(RUN_WITH, config)); + + final List methods = new ArrayList(); + final List methodAnnotations = new ArrayList(); + methodAnnotations.add(new AnnotationMetadataBuilder(TEST)); + + // Get the class so we can hopefully make a demo method that will be + // usable + final ClassOrInterfaceTypeDetails governorTypeDetails = typeLocationService + .getTypeDetails(javaType); + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(this.getClass().getName(), + governorTypeDetails); + for (final MemberHoldingTypeDetails typeDetails : memberDetails + .getDetails()) { + if (!(typeDetails.getCustomData().keySet() + .contains(CustomDataKeys.PERSISTENT_TYPE) || typeDetails + .getDeclaredByMetadataId() + .startsWith( + "MID:org.springframework.roo.addon.tostring.ToStringMetadata"))) { + for (final MethodMetadata method : typeDetails + .getDeclaredMethods()) { + // Check if public, non-abstract method + if (Modifier.isPublic(method.getModifier()) + && !Modifier.isAbstract(method.getModifier())) { + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("org.junit.Assert.assertTrue(true);"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + declaredByMetadataId, Modifier.PUBLIC, + method.getMethodName(), + JavaType.VOID_PRIMITIVE, bodyBuilder); + methodBuilder.setAnnotations(methodAnnotations); + methods.add(methodBuilder); + } + } + } + } + + // Only create test class if there are test methods present + if (!methods.isEmpty()) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, name, + PhysicalTypeCategory.CLASS); + + // Create instance of entity to test + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + declaredByMetadataId); + fieldBuilder.setModifier(Modifier.PRIVATE); + fieldBuilder.setFieldName(new JavaSymbolName(StringUtils + .uncapitalize(javaType.getSimpleTypeName()))); + fieldBuilder.setFieldType(javaType); + fieldBuilder.setFieldInitializer("new " + + javaType.getFullyQualifiedTypeName() + "()"); + final List fields = new ArrayList(); + fields.add(fieldBuilder); + cidBuilder.setDeclaredFields(fields); + + cidBuilder.setDeclaredMethods(methods); + + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + } +} \ No newline at end of file diff --git a/addon-test/src/main/java/org/springframework/roo/addon/test/RooIntegrationTest.java b/addon-test/src/main/java/org/springframework/roo/addon/test/RooIntegrationTest.java new file mode 100644 index 000000000..811d7b6ea --- /dev/null +++ b/addon-test/src/main/java/org/springframework/roo/addon/test/RooIntegrationTest.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates to produce an integration test class. + * + * @author Ben Alex + * @since 1.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooIntegrationTest { + + boolean count() default true; + + /** + * @return the type of class that will have an entity test created + * (required; must offer entity services) + */ + Class entity(); + + boolean find() default true; + + boolean findAll() default true; + + int findAllMaximum() default 250; + + boolean findEntries() default true; + + boolean flush() default true; + + boolean merge() default true; + + boolean persist() default true; + + boolean remove() default true; + + boolean transactional() default true; +} diff --git a/addon-test/src/test/java/org/springframework/roo/addon/test/IntegrationTestAnnotationValuesTest.java b/addon-test/src/test/java/org/springframework/roo/addon/test/IntegrationTestAnnotationValuesTest.java new file mode 100644 index 000000000..9de98d6ab --- /dev/null +++ b/addon-test/src/test/java/org/springframework/roo/addon/test/IntegrationTestAnnotationValuesTest.java @@ -0,0 +1,24 @@ +package org.springframework.roo.addon.test; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link IntegrationTestAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class IntegrationTestAnnotationValuesTest + extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooIntegrationTest.class; + } + + @Override + protected Class getValuesClass() { + return IntegrationTestAnnotationValues.class; + } +} \ No newline at end of file diff --git a/addon-tostring/pom.xml b/addon-tostring/pom.xml new file mode 100644 index 000000000..9b5ca909f --- /dev/null +++ b/addon-tostring/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.tostring + bundle + Spring Roo - Addon - toString + Support for the automatic management of toString() method through an AspectJ ITD. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/RooToString.java b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/RooToString.java new file mode 100644 index 000000000..3619887fc --- /dev/null +++ b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/RooToString.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.tostring; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; + +/** + * Provides a {@link Object#toString()} method if requested. + *

    + * Whilst it is possible to apply this annotation to any class that you'd like a + * {@link Object#toString()} method produced for, it is generally triggered + * automatically via the use of most other annotations in the system. The + * created method is conservative and only includes public accessor methods + * within the produced string. Further, any accessor which returns a common JDK + * {@link Collection} type is restricted to displaying its size only. + * + * @author Ben Alex + * @since 1.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RooToString { + + /** + * @return an array of fields to exclude in the toString method + */ + String[] excludeFields() default ""; + + /** + * @return the name of the {@link Object#toString()} method to generate + * (defaults to "toString"; if empty, does not create) + */ + String toStringMethod() default "toString"; +} diff --git a/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringAnnotationValues.java b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringAnnotationValues.java new file mode 100644 index 000000000..472f1f566 --- /dev/null +++ b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringAnnotationValues.java @@ -0,0 +1,38 @@ +package org.springframework.roo.addon.tostring; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooToString} annotation. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class ToStringAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate private String[] excludeFields; + @AutoPopulate private String toStringMethod = "toString"; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public ToStringAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_TO_STRING); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String[] getExcludeFields() { + return excludeFields; + } + + public String getToStringMethod() { + return toStringMethod; + } +} diff --git a/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringMetadata.java b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringMetadata.java new file mode 100644 index 000000000..486fcbe0b --- /dev/null +++ b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringMetadata.java @@ -0,0 +1,155 @@ +package org.springframework.roo.addon.tostring; + +import static org.springframework.roo.model.JavaType.STRING; + +import java.lang.reflect.Modifier; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooToString}. + * + * @author Ben Alex + * @since 1.0 + */ +public class ToStringMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = ToStringMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final String STYLE = "SHORT_PREFIX_STYLE"; + private static final JavaType TO_STRING_BUILDER = new JavaType( + "org.apache.commons.lang3.builder.ReflectionToStringBuilder"); + private static final JavaType TO_STRING_STYLE = new JavaType( + "org.apache.commons.lang3.builder.ToStringStyle"); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final ToStringAnnotationValues annotationValues; + + /** + * Constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalTypeMetadata + * @param annotationValues + */ + public ToStringMetadata(final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final ToStringAnnotationValues annotationValues) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + + this.annotationValues = annotationValues; + + // Generate the toString() method + builder.addMethod(getToStringMethod()); + + // Create a representation of the desired output ITD + itdTypeDetails = builder.build(); + } + + /** + * Obtains the "toString" method for this type, if available. + *

    + * If the user provided a non-default name for "toString", that method will + * be returned. + * + * @return the "toString" method declared on this type or that will be + * introduced (or null if undeclared and not introduced) + */ + private MethodMetadataBuilder getToStringMethod() { + final String toStringMethod = annotationValues.getToStringMethod(); + if (StringUtils.isBlank(toStringMethod)) { + return null; + } + + // Compute the relevant toString method name + final JavaSymbolName methodName = new JavaSymbolName(toStringMethod); + + // See if the type itself declared the method + if (governorHasMethod(methodName)) { + return null; + } + + builder.getImportRegistrationResolver().addImports(TO_STRING_BUILDER, + TO_STRING_STYLE); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String[] excludeFields = annotationValues.getExcludeFields(); + String str; + if (excludeFields != null && excludeFields.length > 0) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < excludeFields.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append("\"").append(excludeFields[i]).append("\""); + } + str = "new ReflectionToStringBuilder(this, ToStringStyle." + STYLE + + ").setExcludeFieldNames(" + builder.toString() + + ").toString();"; + } + else { + str = "ReflectionToStringBuilder.toString(this, ToStringStyle." + + STYLE + ");"; + } + bodyBuilder.appendFormalLine("return " + str); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + STRING, bodyBuilder); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringMetadataProvider.java b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringMetadataProvider.java new file mode 100644 index 000000000..a4538f832 --- /dev/null +++ b/addon-tostring/src/main/java/org/springframework/roo/addon/tostring/ToStringMetadataProvider.java @@ -0,0 +1,94 @@ +package org.springframework.roo.addon.tostring; + +import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Provides {@link ToStringMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class ToStringMetadataProvider extends + AbstractMemberDiscoveringItdMetadataProvider { + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_TO_STRING); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return ToStringMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_TO_STRING); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = ToStringMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = ToStringMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "ToString"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + return getLocalMid(itdTypeDetails); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + final ToStringAnnotationValues annotationValues = new ToStringAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound()) { + return null; + } + + final MemberDetails memberDetails = getMemberDetails(governorPhysicalTypeMetadata); + if (memberDetails == null || memberDetails.getFields().isEmpty()) { + return null; + } + + return new ToStringMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues); + } + + public String getProvidesType() { + return ToStringMetadata.getMetadataIdentiferType(); + } +} diff --git a/addon-web-flow/pom.xml b/addon-web-flow/pom.xml new file mode 100644 index 000000000..795b5c8e7 --- /dev/null +++ b/addon-web-flow/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.web.flow + bundle + Spring Roo - Addon - Web - Flow + Configuration and integration of Spring Web Flow features in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.jpa + + + org.springframework.roo + org.springframework.roo.addon.finder + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowCommands.java b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowCommands.java new file mode 100644 index 000000000..a8965255e --- /dev/null +++ b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowCommands.java @@ -0,0 +1,34 @@ +package org.springframework.roo.addon.web.flow; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'web flow' add-on to be used by the Roo shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class WebFlowCommands implements CommandMarker { + + @Reference private WebFlowOperations webFlowOperations; + + @CliCommand(value = "web flow", help = "Install Spring Web Flow configuration artifacts into your project") + public void installWebFlow( + @CliOption(key = { "flowName" }, mandatory = false, help = "The name for your web flow") final String flowName) { + + webFlowOperations.installWebFlow(flowName); + } + + @CliAvailabilityIndicator("web flow") + public boolean isInstallWebFlowAvailable() { + return webFlowOperations.isWebFlowInstallationPossible(); + } +} \ No newline at end of file diff --git a/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowOperations.java b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowOperations.java new file mode 100644 index 000000000..f9281f476 --- /dev/null +++ b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowOperations.java @@ -0,0 +1,26 @@ +package org.springframework.roo.addon.web.flow; + +/** + * Interface for {@link WebFlowOperationsImpl}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface WebFlowOperations { + + /** + * Installs a new flow in its own directory under /WEB-INF/views. For + * example if the flow name is "main" then all flow artifacts will be in + * /WEB-INF/views/main. The first time a flow is installed, Web Flow related + * configuration will also be added. The flow directory is expected to be + * used exclusively for flow-related artifacts. A new flow will not be + * created if the flow directory already exists. + * + * @param flowName the name of the flow to install + * @throws IllegalStateException if the directory for the flow already + * exists. + */ + void installWebFlow(String flowName); + + boolean isWebFlowInstallationPossible(); +} \ No newline at end of file diff --git a/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowOperationsImpl.java b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowOperationsImpl.java new file mode 100644 index 000000000..0180830a6 --- /dev/null +++ b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/WebFlowOperationsImpl.java @@ -0,0 +1,208 @@ +package org.springframework.roo.addon.web.flow; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.flow.XmlTemplate.DomElementCallback; +import org.springframework.roo.addon.web.mvc.controller.WebMvcOperations; +import org.springframework.roo.addon.web.mvc.jsp.JspOperations; +import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations; +import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperations; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.ProjectType; +import org.springframework.roo.project.Repository; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Provides Web Flow configuration operations. + * + * @author Stefan Schmidt + * @author Rossen Stoyanchev + * @since 1.0 + */ +@Component +@Service +public class WebFlowOperationsImpl implements WebFlowOperations { + + @Reference private FileManager fileManager; + @Reference private JspOperations jspOperations; + @Reference private MenuOperations menuOperations; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private TilesOperations tilesOperations; + @Reference private WebMvcOperations webMvcOperations; + + private void copyTemplate(final String templateFileName, + final String resolvedTargetDirectoryPath) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils + .getInputStream(getClass(), templateFileName); + outputStream = fileManager.createFile( + resolvedTargetDirectoryPath + "/" + templateFileName) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException( + "Encountered an error during copying of resources for Web Flow addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + private String getFlowId(String flowName) { + flowName = StringUtils.defaultIfEmpty(flowName, "sample-flow"); + if (flowName.startsWith("/")) { + flowName = flowName.substring(1); + } + return flowName.replaceAll("[^a-zA-Z/_]", ""); + } + + /** + * See {@link WebFlowOperations#installWebFlow(String)}. + */ + public void installWebFlow(final String flowName) { + installWebFlowConfiguration(); + + final String flowId = getFlowId(flowName); + final String webRelativeFlowPath = "/WEB-INF/views/" + flowId; + final String resolvedFlowPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, webRelativeFlowPath); + final String resolvedFlowDefinitionPath = resolvedFlowPath + + "/flow.xml"; + + if (fileManager.exists(resolvedFlowPath)) { + throw new IllegalStateException("Flow directory already exists: " + + resolvedFlowPath); + } + fileManager.createDirectory(resolvedFlowPath); + + copyTemplate("flow.xml", resolvedFlowPath); + copyTemplate("view-state-1.jspx", resolvedFlowPath); + copyTemplate("view-state-2.jspx", resolvedFlowPath); + copyTemplate("end-state.jspx", resolvedFlowPath); + + new XmlTemplate(fileManager).update(resolvedFlowDefinitionPath, + new DomElementCallback() { + public boolean doWithElement(final Document document, + final Element root) { + final List states = XmlUtils.findElements( + "/flow/view-state|end-state", root); + for (final Element state : states) { + state.setAttribute("view", + flowId + "/" + state.getAttribute("id")); + } + return true; + } + }); + + final JavaSymbolName flowMenuCategory = new JavaSymbolName("Flows"); + final JavaSymbolName flowMenuName = new JavaSymbolName(flowId.replace( + "/", "_")); + menuOperations.addMenuItem(flowMenuCategory, flowMenuName, + flowMenuName.getReadableSymbolName(), "webflow_menu_enter", "/" + + flowId, null, + pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP)); + + tilesOperations.addViewDefinition(flowId, + pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP), flowId + + "/*", TilesOperations.DEFAULT_TEMPLATE, + webRelativeFlowPath + "/{1}.jspx"); + + updateConfiguration(); + webMvcOperations.registerWebFlowConversionServiceExposingInterceptor(); + } + + private void installWebFlowConfiguration() { + final String resolvedSpringConfigPath = pathResolver + .getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, "WEB-INF/spring"); + if (fileManager + .exists(resolvedSpringConfigPath + "/webflow-config.xml")) { + return; + } + + copyTemplate("webflow-config.xml", resolvedSpringConfigPath); + + final String webMvcConfigPath = resolvedSpringConfigPath + + "/webmvc-config.xml"; + if (!fileManager.exists(webMvcConfigPath)) { + webMvcOperations.installAllWebMvcArtifacts(); + } + + jspOperations.installCommonViewArtefacts(); + + new XmlTemplate(fileManager).update(webMvcConfigPath, + new DomElementCallback() { + public boolean doWithElement(final Document document, + final Element root) { + if (null == XmlUtils + .findFirstElement( + "/beans/import[@resource='webflow-config.xml']", + root)) { + final Element importSWF = document + .createElement("import"); + importSWF.setAttribute("resource", + "webflow-config.xml"); + root.appendChild(importSWF); + return true; + } + return false; + } + }); + } + + public boolean isWebFlowInstallationPossible() { + return projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.MVC) + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + private void updateConfiguration() { + final Element configuration = XmlUtils.getConfiguration(getClass()); + final String focusedModuleName = projectOperations + .getFocusedModuleName(); + + final List dependencyElements = new ArrayList(); + for (final Element webFlowDependencyElement : XmlUtils.findElements( + "/configuration/springWebFlow/dependencies/dependency", + configuration)) { + dependencyElements.add(new Dependency(webFlowDependencyElement)); + } + projectOperations + .addDependencies(focusedModuleName, dependencyElements); + + final List repositoryElements = new ArrayList(); + for (final Element repositoryElement : XmlUtils.findElements( + "/configuration/springWebFlow/repositories/repository", + configuration)) { + repositoryElements.add(new Repository(repositoryElement)); + } + projectOperations + .addRepositories(focusedModuleName, repositoryElements); + + projectOperations.updateProjectType(focusedModuleName, ProjectType.WAR); + } +} \ No newline at end of file diff --git a/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/XmlTemplate.java b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/XmlTemplate.java new file mode 100644 index 000000000..91c196cd6 --- /dev/null +++ b/addon-web-flow/src/main/java/org/springframework/roo/addon/web/flow/XmlTemplate.java @@ -0,0 +1,57 @@ +package org.springframework.roo.addon.web.flow; + +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Provides a method for making updates to an XML file. It relies on + * {@link FileManager} and {@link XmlUtils} to do update the XML and provides a + * convenient callback mechanism that provides access to the {@link Document} + * and the root {@link Element}. + * + * @author Rossen Stoyanchev + */ +public class XmlTemplate { + + public interface DomElementCallback { + + /** + * Use this method to provide logic for updating a {@link Document}. + * + * @param document the document + * @param rootElement the root element of the document. + * @return true if any changes were made that require saving, false + * otherwise + */ + boolean doWithElement(Document document, Element rootElement); + } + + private final FileManager fileManager; + + public XmlTemplate(final FileManager fileManager) { + this.fileManager = fileManager; + } + + /** + * Wraps common code for updating an XML file. Callers provide a callback + * and focus on the actual changes without having to know the rest of the + * details. + * + * @param resolvedPathIdentifier the path to the XML file to update. + * @param rootElementCallback A callback with the logic that needs to be + * applied. + */ + public void update(final String resolvedPathIdentifier, + final DomElementCallback rootElementCallback) { + final Document document = XmlUtils.readXml(fileManager + .getInputStream(resolvedPathIdentifier)); + final Element root = document.getDocumentElement(); + if (rootElementCallback.doWithElement(document, root)) { + fileManager.createOrUpdateTextFileIfRequired( + resolvedPathIdentifier, XmlUtils.nodeToString(document), + false); + } + } +} diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/configuration.xml b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/configuration.xml new file mode 100644 index 000000000..a035b7afa --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/configuration.xml @@ -0,0 +1,12 @@ + + + + + + org.springframework.webflow + spring-webflow + 2.3.1.RELEASE + + + + \ No newline at end of file diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/end-state.jspx b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/end-state.jspx new file mode 100644 index 000000000..949a587c6 --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/end-state.jspx @@ -0,0 +1,12 @@ +

    + + + + + +

    ${fn:escapeXml(title)}

    +

    + +

    +
    +
    \ No newline at end of file diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/flow-template.xml b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/flow-template.xml new file mode 100644 index 000000000..cd7068791 --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/flow-template.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/flow.xml b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/flow.xml new file mode 100644 index 000000000..6587f7a52 --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/flow.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/view-state-1.jspx b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/view-state-1.jspx new file mode 100644 index 000000000..912ecc99a --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/view-state-1.jspx @@ -0,0 +1,19 @@ +
    + + + + +

    ${fn:escapeXml(title)}

    +

    + +

    +
    +
    + + + + +
    +
    +
    +
    diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/view-state-2.jspx b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/view-state-2.jspx new file mode 100644 index 000000000..a3c251560 --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/view-state-2.jspx @@ -0,0 +1,17 @@ +
    + + + + +

    ${fn:escapeXml(title)}

    +

    + +

    +
    +
    + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/webflow-config.xml b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/webflow-config.xml new file mode 100644 index 000000000..44b494b14 --- /dev/null +++ b/addon-web-flow/src/main/resources/org/springframework/roo/addon/web/flow/webflow-config.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-controller/pom.xml b/addon-web-mvc-controller/pom.xml new file mode 100644 index 000000000..ba1d757c6 --- /dev/null +++ b/addon-web-mvc-controller/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.web.mvc.controller + bundle + Spring Roo - Addon - Web MVC Controller + Configuration and integration of Spring MVC controllers in the target project. + + + + org.apache.maven.plugins + maven-surefire-plugin + + junit:junit + + **/*Tests.java + + + + + + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.finder + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.addon.json + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerCommands.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerCommands.java new file mode 100644 index 000000000..585dc4aa0 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerCommands.java @@ -0,0 +1,153 @@ +package org.springframework.roo.addon.web.mvc.controller; + +import static org.springframework.roo.shell.OptionContexts.PROJECT; +import static org.springframework.roo.shell.OptionContexts.UPDATE; + +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.text.StrTokenizer; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Commands for the 'mvc controller' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class ControllerCommands implements CommandMarker { + + private static Logger LOGGER = HandlerUtils + .getLogger(ControllerCommands.class); + + @Reference private ControllerOperations controllerOperations; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + + @Deprecated + @CliCommand(value = "controller all", help = "Scaffold controllers for all project entities without an existing controller - deprecated, use 'web mvc setup' + 'web mvc all' instead") + public void generateAll( + @CliOption(key = "package", mandatory = true, optionContext = UPDATE, help = "The package in which new controllers will be placed") final JavaPackage javaPackage) { + + LOGGER.warning("This command has been deprecated and will be disabled soon! Please use 'web mvc setup' followed by 'web mvc all --package ' instead."); + controllerOperations.setup(); + webMvcAll(javaPackage); + } + + @Deprecated + @CliAvailabilityIndicator({ "controller scaffold", "controller all" }) + public boolean isNewControllerAvailable() { + return controllerOperations.isNewControllerAvailable(); + } + + @CliAvailabilityIndicator({ "web mvc all", "web mvc scaffold" }) + public boolean isScaffoldAvailable() { + return controllerOperations.isControllerInstallationPossible(); + } + + @Deprecated + @CliCommand(value = "controller scaffold", help = "Create a new scaffold Controller (ie where we maintain CRUD automatically) - deprecated, use 'web mvc scaffold' instead") + public void newController( + @CliOption(key = { "class", "" }, mandatory = true, help = "The path and name of the controller object to be created") final JavaType controller, + @CliOption(key = "entity", mandatory = false, optionContext = PROJECT, unspecifiedDefaultValue = "*", help = "The name of the entity object which the controller exposes to the web tier") final JavaType entity, + @CliOption(key = "path", mandatory = false, help = "The base path under which the controller listens for RESTful requests (defaults to the simple name of the form backing object)") final String path, + @CliOption(key = "disallowedOperations", mandatory = false, help = "A comma separated list of operations (only create, update, delete allowed) that should not be generated in the controller") final String disallowedOperations) { + + LOGGER.warning("This command has been deprecated and will be disabled soon! Please use 'web mvc setup' followed by 'web mvc scaffold' instead."); + controllerOperations.setup(); + webMvcScaffold(controller, entity, path, disallowedOperations); + } + + @CliCommand(value = "web mvc all", help = "Scaffold Spring MVC controllers for all project entities without an existing controller") + public void webMvcAll( + @CliOption(key = "package", mandatory = true, optionContext = UPDATE, help = "The package in which new controllers will be placed") final JavaPackage javaPackage) { + + if (!javaPackage.getFullyQualifiedPackageName().startsWith( + projectOperations.getTopLevelPackage( + projectOperations.getFocusedModuleName()) + .getFullyQualifiedPackageName())) { + LOGGER.warning("Your controller was created outside of the project's top level package and is therefore not included in the preconfigured component scanning. Please adjust your component scanning manually in webmvc-config.xml"); + } + controllerOperations.generateAll(javaPackage); + } + + @CliCommand(value = "web mvc scaffold", help = "Create a new scaffold Controller (ie where Roo maintains CRUD functionality automatically)") + public void webMvcScaffold( + @CliOption(key = { "class", "" }, mandatory = true, help = "The path and name of the controller object to be created") final JavaType controller, + @CliOption(key = "backingType", mandatory = false, optionContext = PROJECT, unspecifiedDefaultValue = "*", help = "The name of the form backing type which the controller exposes to the web tier") final JavaType backingType, + @CliOption(key = "path", mandatory = false, help = "The base path under which the controller listens for RESTful requests (defaults to the simple name of the form backing object)") String path, + @CliOption(key = "disallowedOperations", mandatory = false, help = "A comma separated list of operations (only create, update, delete allowed) that should not be generated in the controller") final String disallowedOperations) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(backingType); + if (cid == null) { + LOGGER.warning("The specified entity can not be resolved to a type in your project"); + return; + } + + if (controller.getSimpleTypeName().equalsIgnoreCase( + backingType.getSimpleTypeName())) { + LOGGER.warning("Controller class name needs to be different from the class name of the form backing object (suggestion: '" + + backingType.getSimpleTypeName() + "Controller')"); + return; + } + + final Set disallowedOperationSet = new HashSet(); + if (!"".equals(disallowedOperations)) { + final String[] disallowedOperationsTokens = new StrTokenizer( + disallowedOperations, ",").getTokenArray(); + for (final String operation : disallowedOperationsTokens) { + if (!("create".equals(operation) || "update".equals(operation) || "delete" + .equals(operation))) { + LOGGER.warning("-disallowedOperations options can only contain 'create', 'update', 'delete': -disallowedOperations update,delete"); + return; + } + disallowedOperationSet.add(operation.toLowerCase()); + } + } + + if (StringUtils.isBlank(path)) { + final LogicalPath targetPath = PhysicalTypeIdentifier.getPath(cid + .getDeclaredByMetadataId()); + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(PluralMetadata.createIdentifier(backingType, + targetPath)); + Validate.notNull(pluralMetadata, + "Could not determine plural for '%s'", + backingType.getSimpleTypeName()); + path = pluralMetadata.getPlural().toLowerCase(); + } + else if (path.equals("/") || path.equals("/*")) { + LOGGER.warning("Your application already contains a mapping to '/' or '/*' by default. Please provide a different path."); + return; + } + else if (path.startsWith("/")) { + path = path.substring(1); + } + + controllerOperations.createAutomaticController(controller, backingType, + disallowedOperationSet, path); + } +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerOperations.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerOperations.java new file mode 100644 index 000000000..358316caa --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerOperations.java @@ -0,0 +1,47 @@ +package org.springframework.roo.addon.web.mvc.controller; + +import java.util.Set; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Provides Controller configuration operations. + * + * @author Ben Alex + * @author Stefan Schmidt + */ +public interface ControllerOperations { + + /** + * Creates a new Spring MVC controller which will be automatically + * scaffolded. + *

    + * Request mappings assigned by this method will always commence with "/" + * and end with "/**". You may present this prefix and/or this suffix if you + * wish, although it will automatically be added should it not be provided. + * + * @param controller the controller class to create (required) + * @param entity the entity this controller should edit (required) + * @param disallowedOperations specify a set of disallowed operation names + * (required, but can be empty) + * @param path the path which the controller should be accessible via REST + * requests + */ + void createAutomaticController(JavaType controller, JavaType entity, + Set disallowedOperations, String path); + + /** + * Creates Spring MVC controllers for all JPA entities in the project. + * + * @param javaPackage The package where the new controllers are scaffolded. + */ + void generateAll(JavaPackage javaPackage); + + boolean isControllerInstallationPossible(); + + @Deprecated + boolean isNewControllerAvailable(); + + void setup(); +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerOperationsImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerOperationsImpl.java new file mode 100644 index 000000000..92cfd9cb8 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/ControllerOperationsImpl.java @@ -0,0 +1,242 @@ +package org.springframework.roo.addon.web.mvc.controller; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSISTENT_TYPE; +import static org.springframework.roo.model.RooJavaType.ROO_WEB_SCAFFOLD; +import static org.springframework.roo.model.SpringJavaType.CONTROLLER; +import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link ControllerOperations}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class ControllerOperationsImpl implements ControllerOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(ControllerOperationsImpl.class); + private static final JavaSymbolName PATH = new JavaSymbolName("path"); + private static final JavaSymbolName VALUE = new JavaSymbolName("value"); + + @Reference private MetadataDependencyRegistry dependencyRegistry; + @Reference private MetadataService metadataService; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + @Reference private WebMvcOperations webMvcOperations; + + public void createAutomaticController(final JavaType controller, + final JavaType entity, final Set disallowedOperations, + final String path) { + Validate.notNull(controller, "Controller Java Type required"); + Validate.notNull(entity, "Entity Java Type required"); + Validate.notNull(disallowedOperations, + "Set of disallowed operations required"); + Validate.notBlank(path, "Controller base path required"); + + // Look for an existing controller mapped to this path + final ClassOrInterfaceTypeDetails existingController = getExistingController(path); + + webMvcOperations.installConversionService(controller.getPackage()); + + List annotations = null; + + ClassOrInterfaceTypeDetailsBuilder cidBuilder = null; + if (existingController == null) { + final LogicalPath controllerPath = pathResolver + .getFocusedPath(Path.SRC_MAIN_JAVA); + final String resourceIdentifier = typeLocationService + .getPhysicalTypeCanonicalPath(controller, controllerPath); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(controller, + pathResolver.getPath(resourceIdentifier)); + + // Create annotation @RequestMapping("/myobject/**") + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue(VALUE, "/" + + path)); + annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(REQUEST_MAPPING, + requestMappingAttributes)); + + // Create annotation @Controller + final List> controllerAttributes = new ArrayList>(); + annotations.add(new AnnotationMetadataBuilder(CONTROLLER, + controllerAttributes)); + + // Create annotation @RooWebScaffold(path = "/test", + // formBackingObject = MyObject.class) + annotations.add(getRooWebScaffoldAnnotation(entity, + disallowedOperations, path, PATH)); + cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, controller, + PhysicalTypeCategory.CLASS); + } + else { + cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + existingController); + annotations = cidBuilder.getAnnotations(); + if (MemberFindingUtils.getAnnotationOfType( + existingController.getAnnotations(), ROO_WEB_SCAFFOLD) == null) { + annotations.add(getRooWebScaffoldAnnotation(entity, + disallowedOperations, path, PATH)); + } + } + cidBuilder.setAnnotations(annotations); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void generateAll(final JavaPackage javaPackage) { + for (final ClassOrInterfaceTypeDetails entityDetails : typeLocationService + .findClassesOrInterfaceDetailsWithTag(PERSISTENT_TYPE)) { + if (Modifier.isAbstract(entityDetails.getModifier())) { + continue; + } + + final JavaType entityType = entityDetails.getType(); + final LogicalPath entityPath = PhysicalTypeIdentifier + .getPath(entityDetails.getDeclaredByMetadataId()); + + // Check to see if this persistent type has a web scaffold metadata + // listening to it + final String downstreamWebScaffoldMetadataId = WebScaffoldMetadata + .createIdentifier(entityType, entityPath); + if (dependencyRegistry.getDownstream( + entityDetails.getDeclaredByMetadataId()).contains( + downstreamWebScaffoldMetadataId)) { + // There is already a controller for this entity + continue; + } + + // To get here, there is no listening controller, so add one + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(PluralMetadata + .createIdentifier(entityType, entityPath)); + if (pluralMetadata != null) { + final JavaType controller = new JavaType( + javaPackage.getFullyQualifiedPackageName() + "." + + entityType.getSimpleTypeName() + "Controller"); + createAutomaticController(controller, entityType, + new HashSet(), pluralMetadata.getPlural() + .toLowerCase()); + } + } + } + + public boolean isControllerInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.MVC) + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + public boolean isNewControllerAvailable() { + return projectOperations.isFocusedProjectAvailable(); + } + + public void setup() { + webMvcOperations.installAllWebMvcArtifacts(); + } + + /** + * Looks for an existing controller mapped to the given path + * + * @param path (required) + * @return null if there is no such controller + */ + private ClassOrInterfaceTypeDetails getExistingController(final String path) { + for (final ClassOrInterfaceTypeDetails cid : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(REQUEST_MAPPING)) { + final AnnotationAttributeValue attribute = MemberFindingUtils + .getAnnotationOfType(cid.getAnnotations(), REQUEST_MAPPING) + .getAttribute(VALUE); + if (attribute instanceof ArrayAttributeValue) { + final ArrayAttributeValue mappingAttribute = (ArrayAttributeValue) attribute; + if (mappingAttribute.getValue().size() > 1) { + LOGGER.warning("Skipping controller '" + + cid.getName().getFullyQualifiedTypeName() + + "' as it contains more than one path"); + continue; + } + else if (mappingAttribute.getValue().size() == 1) { + final StringAttributeValue attr = (StringAttributeValue) mappingAttribute + .getValue().get(0); + final String mapping = attr.getValue(); + if (StringUtils.isNotBlank(mapping) + && mapping.equalsIgnoreCase("/" + path)) { + return cid; + } + } + } + else if (attribute instanceof StringAttributeValue) { + final StringAttributeValue mappingAttribute = (StringAttributeValue) attribute; + if (mappingAttribute != null) { + final String mapping = mappingAttribute.getValue(); + if (StringUtils.isNotBlank(mapping) + && mapping.equalsIgnoreCase("/" + path)) { + return cid; + } + } + } + } + return null; + } + + private AnnotationMetadataBuilder getRooWebScaffoldAnnotation( + final JavaType entity, final Set disallowedOperations, + final String path, final JavaSymbolName pathName) { + final List> rooWebScaffoldAttributes = new ArrayList>(); + rooWebScaffoldAttributes.add(new StringAttributeValue(pathName, path)); + rooWebScaffoldAttributes.add(new ClassAttributeValue( + new JavaSymbolName("formBackingObject"), entity)); + for (final String operation : disallowedOperations) { + rooWebScaffoldAttributes.add(new BooleanAttributeValue( + new JavaSymbolName(operation), false)); + } + return new AnnotationMetadataBuilder(ROO_WEB_SCAFFOLD, + rooWebScaffoldAttributes); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/WebMvcOperations.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/WebMvcOperations.java new file mode 100644 index 000000000..e0e885246 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/WebMvcOperations.java @@ -0,0 +1,35 @@ +package org.springframework.roo.addon.web.mvc.controller; + +import org.springframework.roo.model.JavaPackage; + +/** + * Provides operations to create various view layer resources. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.1 + */ +public interface WebMvcOperations { + + String CHARACTER_ENCODING_FILTER_NAME = "CharacterEncodingFilter"; + + String HTTP_METHOD_FILTER_NAME = "HttpMethodFilter"; + + String OPEN_ENTITYMANAGER_IN_VIEW_FILTER_NAME = "Spring OpenEntityManagerInViewFilter"; + + void installAllWebMvcArtifacts(); + + /** + * Installs and configures an application-wide + * FormattingConversionServiceFactoryBean that can be used to register + * application-specific Converters and Formatters. + * + * @param destinationPackage the package to install the conversion service + * class + */ + void installConversionService(JavaPackage destinationPackage); + + void installMinimalWebArtifacts(); + + void registerWebFlowConversionServiceExposingInterceptor(); +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/WebMvcOperationsImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/WebMvcOperationsImpl.java new file mode 100644 index 000000000..33943eac4 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/WebMvcOperationsImpl.java @@ -0,0 +1,429 @@ +package org.springframework.roo.addon.web.mvc.controller; + +import static org.springframework.roo.model.JdkJavaType.EXCEPTION; +import static org.springframework.roo.model.SpringJavaType.CHARACTER_ENCODING_FILTER; +import static org.springframework.roo.model.SpringJavaType.CONTEXT_LOADER_LISTENER; +import static org.springframework.roo.model.SpringJavaType.CONVERSION_SERVICE_EXPOSING_INTERCEPTOR; +import static org.springframework.roo.model.SpringJavaType.DISPATCHER_SERVLET; +import static org.springframework.roo.model.SpringJavaType.FLOW_HANDLER_MAPPING; +import static org.springframework.roo.model.SpringJavaType.HIDDEN_HTTP_METHOD_FILTER; +import static org.springframework.roo.model.SpringJavaType.OPEN_ENTITY_MANAGER_IN_VIEW_FILTER; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.DependencyScope; +import org.springframework.roo.project.DependencyType; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.ProjectType; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.WebXmlUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * Implementation of {@link WebMvcOperations}. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class WebMvcOperationsImpl implements WebMvcOperations { + + private static final String CONVERSION_SERVICE_BEAN_NAME = "applicationConversionService"; + private static final String CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME = "conversionServiceExposingInterceptor"; + private static final String CONVERSION_SERVICE_SIMPLE_TYPE = "ApplicationConversionServiceFactoryBean"; + private static final String WEB_XML = "WEB-INF/web.xml"; + private static final String WEBMVC_CONFIG_XML = "WEB-INF/spring/webmvc-config.xml"; + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + + private void copyWebXml() { + Validate.isTrue(projectOperations.isFocusedProjectAvailable(), + "Project metadata required"); + + // Verify the servlet application context already exists + final String servletCtxFilename = WEBMVC_CONFIG_XML; + Validate.isTrue(fileManager.exists(pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, servletCtxFilename)), + "'%s' does not exist", servletCtxFilename); + + final String webXmlPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, WEB_XML); + if (fileManager.exists(webXmlPath)) { + // File exists, so nothing to do + return; + } + + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), "web-template.xml"); + Validate.notNull(templateInputStream, + "Could not acquire web.xml template"); + final Document document = XmlUtils.readXml(templateInputStream); + + final String projectName = projectOperations + .getProjectName(projectOperations.getFocusedModuleName()); + WebXmlUtils.setDisplayName(projectName, document, null); + WebXmlUtils.setDescription("Roo generated " + projectName + + " application", document, null); + + fileManager.createOrUpdateTextFileIfRequired(webXmlPath, + XmlUtils.nodeToString(document), true); + } + + private void createWebApplicationContext() { + Validate.isTrue(projectOperations.isFocusedProjectAvailable(), + "Project metadata required"); + final String webConfigFile = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML); + final Document document = readOrCreateSpringWebConfigFile(webConfigFile); + setBasePackageForComponentScan(document); + fileManager.createOrUpdateTextFileIfRequired(webConfigFile, + XmlUtils.nodeToString(document), true); + } + + public void installAllWebMvcArtifacts() { + installMinimalWebArtifacts(); + manageWebXml(); + updateConfiguration(); + } + + public void installConversionService(final JavaPackage destinationPackage) { + final String webMvcConfigPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML); + Validate.isTrue(fileManager.exists(webMvcConfigPath), + "'%s' does not exist", webMvcConfigPath); + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(webMvcConfigPath)); + final Element root = document.getDocumentElement(); + + final Element annotationDriven = DomUtils.findFirstElementByName( + "mvc:annotation-driven", root); + if (isConversionServiceConfigured(root, annotationDriven)) { + // Conversion service already defined, moving on. + return; + } + annotationDriven.setAttribute("conversion-service", + CONVERSION_SERVICE_BEAN_NAME); + + final Element conversionServiceBean = new XmlElementBuilder("bean", + document) + .addAttribute("id", CONVERSION_SERVICE_BEAN_NAME) + .addAttribute( + "class", + destinationPackage.getFullyQualifiedPackageName() + "." + + CONVERSION_SERVICE_SIMPLE_TYPE).build(); + root.appendChild(conversionServiceBean); + + fileManager.createOrUpdateTextFileIfRequired(webMvcConfigPath, + XmlUtils.nodeToString(document), false); + + installConversionServiceJavaClass(destinationPackage); + + registerWebFlowConversionServiceExposingInterceptor(); + } + + private void installConversionServiceJavaClass(final JavaPackage thePackage) { + final JavaType javaType = new JavaType( + thePackage.getFullyQualifiedPackageName() + + ".ApplicationConversionServiceFactoryBean"); + final String physicalPath = pathResolver.getFocusedCanonicalPath( + Path.SRC_MAIN_JAVA, javaType); + if (fileManager.exists(physicalPath)) { + return; + } + InputStream inputStream = null; + try { + inputStream = FileUtils + .getInputStream(getClass(), + "converter/ApplicationConversionServiceFactoryBean-template._java"); + String input = IOUtils.toString(inputStream); + input = input.replace("__PACKAGE__", + thePackage.getFullyQualifiedPackageName()); + fileManager.createOrUpdateTextFileIfRequired(physicalPath, input, + false); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to create '" + physicalPath + + "'", e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + public void installMinimalWebArtifacts() { + // Note that the sequence matters here as some of these artifacts are + // loaded further down the line + createWebApplicationContext(); + copyWebXml(); + } + + private boolean isConversionServiceConfigured() { + final String webMvcConfigPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML); + Validate.isTrue(fileManager.exists(webMvcConfigPath), + "'%s' doesn't exist", webMvcConfigPath); + + final MutableFile mutableFile = fileManager + .updateFile(webMvcConfigPath); + final Document document = XmlUtils + .readXml(mutableFile.getInputStream()); + final Element root = document.getDocumentElement(); + + final Element annotationDrivenElement = DomUtils + .findFirstElementByName("mvc:annotation-driven", root); + return isConversionServiceConfigured(root, annotationDrivenElement); + } + + private boolean isConversionServiceConfigured(final Element root, + final Element annotationDrivenElement) { + final String beanName = annotationDrivenElement + .getAttribute("conversion-service"); + if (StringUtils.isBlank(beanName)) { + return false; + } + + final Element bean = XmlUtils.findFirstElement("/beans/bean[@id=\"" + + beanName + "\"]", root); + final String classAttribute = bean.getAttribute("class"); + final StringBuilder sb = new StringBuilder( + "Found custom ConversionService installed in webmvc-config.xml. "); + sb.append("Remove the conversion-service attribute, let Spring ROO 1.1.1 (or higher), install the new application-wide "); + sb.append("ApplicationConversionServiceFactoryBean and then use that to register your custom converters and formatters."); + Validate.isTrue( + classAttribute.endsWith(CONVERSION_SERVICE_SIMPLE_TYPE), + sb.toString()); + return true; + } + + private void manageWebXml() { + Validate.isTrue(projectOperations.isFocusedProjectAvailable(), + "Project metadata required"); + + // Verify that the web.xml already exists + final String webXmlPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, WEB_XML); + Validate.isTrue(fileManager.exists(webXmlPath), "'%s' does not exist", + webXmlPath); + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(webXmlPath)); + + WebXmlUtils.addContextParam(new WebXmlUtils.WebXmlParam( + "defaultHtmlEscape", "true"), document, + "Enable escaping of form submission contents"); + WebXmlUtils.addContextParam(new WebXmlUtils.WebXmlParam( + "contextConfigLocation", + "classpath*:META-INF/spring/applicationContext*.xml"), + document, null); + WebXmlUtils.addFilter(CHARACTER_ENCODING_FILTER_NAME, + CHARACTER_ENCODING_FILTER.getFullyQualifiedTypeName(), "/*", + document, null, + new WebXmlUtils.WebXmlParam("encoding", "UTF-8"), + new WebXmlUtils.WebXmlParam("forceEncoding", "true")); + WebXmlUtils.addFilter(HTTP_METHOD_FILTER_NAME, + HIDDEN_HTTP_METHOD_FILTER.getFullyQualifiedTypeName(), "/*", + document, null); + if (projectOperations.isFeatureInstalled(FeatureNames.JPA)) { + WebXmlUtils.addFilter(OPEN_ENTITYMANAGER_IN_VIEW_FILTER_NAME, + OPEN_ENTITY_MANAGER_IN_VIEW_FILTER + .getFullyQualifiedTypeName(), "/*", document, null); + } + WebXmlUtils + .addListener( + CONTEXT_LOADER_LISTENER.getFullyQualifiedTypeName(), + document, + "Creates the Spring Container shared by all Servlets and Filters"); + WebXmlUtils.addServlet(projectOperations.getFocusedProjectName(), + DISPATCHER_SERVLET.getFullyQualifiedTypeName(), "/", 1, + document, "Handles Spring requests", + new WebXmlUtils.WebXmlParam("contextConfigLocation", + WEBMVC_CONFIG_XML)); + WebXmlUtils.setSessionTimeout(10, document, null); + WebXmlUtils.addExceptionType(EXCEPTION.getFullyQualifiedTypeName(), + "/uncaughtException", document, null); + WebXmlUtils.addErrorCode(new Integer(404), "/resourceNotFound", + document, null); + + fileManager.createOrUpdateTextFileIfRequired(webXmlPath, + XmlUtils.nodeToString(document), false); + } + + private Document readOrCreateSpringWebConfigFile(final String webConfigFile) { + final InputStream inputStream; + if (fileManager.exists(webConfigFile)) { + inputStream = fileManager.getInputStream(webConfigFile); + } + else { + inputStream = FileUtils.getInputStream(getClass(), + "webmvc-config.xml"); + Validate.notNull(inputStream, "Could not acquire web.xml template"); + } + return XmlUtils.readXml(inputStream); + } + + public void registerWebFlowConversionServiceExposingInterceptor() { + final String webFlowConfigPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/spring/webflow-config.xml"); + if (!fileManager.exists(webFlowConfigPath)) { + // No web flow configured, moving on. + return; + } + + if (!isConversionServiceConfigured()) { + // We only need to install the ConversionServiceExposingInterceptor + // for Web Flow if a custom conversion service is present. + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(webFlowConfigPath)); + final Element root = document.getDocumentElement(); + + if (XmlUtils.findFirstElement("/beans/bean[@id='" + + CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME + "']", root) == null) { + final Element conversionServiceExposingInterceptor = new XmlElementBuilder( + "bean", document) + .addAttribute( + "class", + CONVERSION_SERVICE_EXPOSING_INTERCEPTOR + .getFullyQualifiedTypeName()) + .addAttribute("id", + CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME) + .addChild( + new XmlElementBuilder("constructor-arg", document) + .addAttribute("ref", + CONVERSION_SERVICE_BEAN_NAME) + .build()).build(); + root.appendChild(conversionServiceExposingInterceptor); + } + final Element flowHandlerMapping = XmlUtils.findFirstElement( + "/beans/bean[@class='" + + FLOW_HANDLER_MAPPING.getFullyQualifiedTypeName() + + "']", root); + if (flowHandlerMapping != null) { + if (XmlUtils.findFirstElement( + "property[@name='interceptors']/array/ref[@bean='" + + CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME + + "']", flowHandlerMapping) == null) { + final Element interceptors = new XmlElementBuilder("property", + document) + .addAttribute("name", "interceptors") + .addChild( + new XmlElementBuilder("array", document) + .addChild( + new XmlElementBuilder("ref", + document) + .addAttribute("bean", + CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME) + .build()).build()) + .build(); + flowHandlerMapping.appendChild(interceptors); + } + } + + fileManager.createOrUpdateTextFileIfRequired(webFlowConfigPath, + XmlUtils.nodeToString(document), false); + } + + private void setBasePackageForComponentScan(final Document document) { + final Element componentScanElement = DomUtils.findFirstElementByName( + "context:component-scan", (Element) document.getFirstChild()); + final JavaPackage topLevelPackage = projectOperations + .getTopLevelPackage(projectOperations.getFocusedModuleName()); + componentScanElement.setAttribute("base-package", + topLevelPackage.getFullyQualifiedPackageName()); + } + + private void updateConfiguration() { + // Update webmvc-config.xml if needed. + final String webConfigFile = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML); + Validate.isTrue(fileManager.exists(webConfigFile), + "Aborting: Unable to find %s", webConfigFile); + InputStream webMvcConfigInputStream = null; + try { + webMvcConfigInputStream = fileManager.getInputStream(webConfigFile); + Validate.notNull(webMvcConfigInputStream, + "Aborting: Unable to acquire webmvc-config.xml file"); + final Document webMvcConfig = XmlUtils + .readXml(webMvcConfigInputStream); + final Element root = webMvcConfig.getDocumentElement(); + if (XmlUtils.findFirstElement("/beans/interceptors", root) == null) { + final InputStream templateInputStream = FileUtils + .getInputStream(getClass(), + "webmvc-config-additions.xml"); + Validate.notNull(templateInputStream, + "Could not acquire webmvc-config-additions.xml template"); + final Document webMvcConfigAdditions = XmlUtils + .readXml(templateInputStream); + final NodeList nodes = webMvcConfigAdditions + .getDocumentElement().getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + root.appendChild(webMvcConfig.importNode(nodes.item(i), + true)); + } + fileManager.createOrUpdateTextFileIfRequired(webConfigFile, + XmlUtils.nodeToString(webMvcConfig), true); + } + } + finally { + IOUtils.closeQuietly(webMvcConfigInputStream); + } + + // Add MVC dependencies. + final boolean isGaeEnabled = projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.GAE); + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List springDependencies = XmlUtils.findElements( + "/configuration/springWebMvc/dependencies/dependency", + configuration); + for (final Element dependencyElement : springDependencies) { + final Dependency dependency = new Dependency(dependencyElement); + if (isGaeEnabled + && dependency.getGroupId().equals("org.glassfish.web") + && dependency.getArtifactId().equals("jstl-impl")) { + dependencies.add(new Dependency(dependency.getGroupId(), + dependency.getArtifactId(), dependency.getVersion(), + DependencyType.JAR, DependencyScope.PROVIDED)); + } + else { + dependencies.add(dependency); + } + } + + projectOperations.addDependencies( + projectOperations.getFocusedModuleName(), dependencies); + + projectOperations.updateProjectType( + projectOperations.getFocusedModuleName(), ProjectType.WAR); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadata.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadata.java new file mode 100644 index 000000000..a643fef10 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadata.java @@ -0,0 +1,544 @@ +package org.springframework.roo.addon.web.mvc.controller.converter; + +import static org.springframework.roo.model.SpringJavaType.CONFIGURABLE; +import static org.springframework.roo.model.SpringJavaType.FORMATTER_REGISTRY; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.json.CustomDataJsonTags; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.SpringJavaType; + +/** + * Represents metadata for the application-wide conversion service. Generates + * the following ITD methods: + *

      + *
    • afterPropertiesSet() - overrides InitializingBean lifecycle parent method + *
    • + *
    • installLabelConverters(FormatterRegistry registry) - registers all + * converter methods
    • + *
    • a converter method for all scaffolded domain types as well their + * associations
    • + *
    + * + * @author Rossen Stoyanchev + * @author Stefan Schmidt + * @since 1.1.1 + */ +public class ConversionServiceMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final JavaType BASE_64 = new JavaType( + "org.apache.commons.codec.binary.Base64"); + private static final String CONVERTER = "Converter"; + private static final JavaSymbolName INSTALL_LABEL_CONVERTERS = new JavaSymbolName( + "installLabelConverters"); + private static final JavaSymbolName INSTALL_EMBEDDABLE_CONVERTERS = new JavaSymbolName( + "installEmbeddableConverters"); + + private TypeLocationService typeLocationService; + + private Map> compositePrimaryKeyTypes; + private Map findMethods; + private Map idTypes; + private Set relevantDomainTypes; + private Map> toStringMethods; + private List embeddableTypes; + + /** + * Production constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalTypeMetadata + * @param findMethods + * @param idTypes + * the ID types of the domain types for which to generate + * converters (required); must be one for each domain type + * @param relevantDomainTypes + * the types for which to generate converters (required) + * @param compositePrimaryKeyTypes + * (required) + */ + public ConversionServiceMetadata( + final TypeLocationService typeLocationService, + final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final Map findMethods, + final Map idTypes, + final Set relevantDomainTypes, + final Map> compositePrimaryKeyTypes, + final Map> toStringMethods, + final List embeddableTypes) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.notNull(findMethods, "Find methods required"); + Validate.notNull(compositePrimaryKeyTypes, "List of PK types required"); + Validate.notNull(idTypes, "List of ID types required"); + Validate.notNull(relevantDomainTypes, + "List of relevant domain types required"); + Validate.isTrue(relevantDomainTypes.size() == idTypes.size(), + "Expected %d ID types, but was %d", relevantDomainTypes.size(), + idTypes.size()); + Validate.notNull(toStringMethods, "ToString methods required"); + + if (!isValid() || relevantDomainTypes.isEmpty() + && compositePrimaryKeyTypes.isEmpty()) { + valid = false; + return; + } + + this.typeLocationService = typeLocationService; + + this.findMethods = findMethods; + this.compositePrimaryKeyTypes = compositePrimaryKeyTypes; + this.idTypes = idTypes; + this.relevantDomainTypes = relevantDomainTypes; + this.toStringMethods = toStringMethods; + this.embeddableTypes = embeddableTypes; + + builder.addAnnotation(getTypeAnnotation(CONFIGURABLE)); + builder.addMethod(getInstallLabelConvertersMethod()); + builder.addMethod(getInstallEmbeddableConvertersMethod()); + builder.addMethod(getAfterPropertiesSetMethod()); + + itdTypeDetails = builder.build(); + } + + private MethodMetadataBuilder getInstallEmbeddableConvertersMethod() { + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + if (this.embeddableTypes.isEmpty()) { + return null; + } + + // Generating all embeddableToString methods + List embeddableMethods = generateEmbeddableTypeToStringMethods(); + + for (MethodMetadataBuilder method : embeddableMethods) { + builder.addMethod(method); + bodyBuilder + .appendFormalLine(String.format( + "registry.addConverter(%s());", + method.getMethodName())); + } + + final JavaType parameterType = FORMATTER_REGISTRY; + + final List parameterNames = Arrays + .asList(new JavaSymbolName("registry")); + builder.getImportRegistrationResolver().addImport(parameterType); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + INSTALL_EMBEDDABLE_CONVERTERS, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + + } + + private MethodMetadataBuilder getAfterPropertiesSetMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + "afterPropertiesSet"); + if (governorHasMethod(methodName)) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("super.afterPropertiesSet();"); + bodyBuilder.appendFormalLine(INSTALL_LABEL_CONVERTERS.getSymbolName() + + "(getObject());"); + if (!this.embeddableTypes.isEmpty()) { + bodyBuilder.appendFormalLine(INSTALL_EMBEDDABLE_CONVERTERS + .getSymbolName() + "(getObject());"); + } + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + JavaType.VOID_PRIMITIVE, bodyBuilder); + } + + private MethodMetadataBuilder getInstallLabelConvertersMethod() { + final List sortedRelevantDomainTypes = new ArrayList( + relevantDomainTypes); + Collections.sort(sortedRelevantDomainTypes); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + final Set methodNames = new HashSet(); + for (final JavaType formBackingObject : sortedRelevantDomainTypes) { + String simpleName = formBackingObject.getSimpleTypeName(); + while (methodNames.contains(simpleName)) { + simpleName += "_"; + } + methodNames.add(simpleName); + + final JavaSymbolName toIdMethodName = new JavaSymbolName("get" + + simpleName + "ToStringConverter"); + builder.addMethod(getToStringConverterMethod(formBackingObject, + toIdMethodName, toStringMethods.get(formBackingObject))); + bodyBuilder.appendFormalLine("registry.addConverter(" + + toIdMethodName.getSymbolName() + "());"); + + final JavaSymbolName toTypeMethodName = new JavaSymbolName( + "getIdTo" + simpleName + CONVERTER); + final MethodMetadataBuilder toTypeConverterMethod = getToTypeConverterMethod( + formBackingObject, toTypeMethodName, + findMethods.get(formBackingObject), + idTypes.get(formBackingObject)); + if (toTypeConverterMethod != null) { + builder.addMethod(toTypeConverterMethod); + bodyBuilder.appendFormalLine("registry.addConverter(" + + toTypeMethodName.getSymbolName() + "());"); + } + + // Only allow conversion if ID type is not String already. + if (!idTypes.get(formBackingObject).equals(JavaType.STRING)) { + final JavaSymbolName stringToTypeMethodName = new JavaSymbolName( + "getStringTo" + simpleName + CONVERTER); + builder.addMethod(getStringToTypeConverterMethod( + formBackingObject, stringToTypeMethodName, + idTypes.get(formBackingObject))); + bodyBuilder.appendFormalLine("registry.addConverter(" + + stringToTypeMethodName.getSymbolName() + "());"); + } + } + + for (final Entry> entry : compositePrimaryKeyTypes + .entrySet()) { + final JavaType targetType = entry.getKey(); + final Map jsonMethodNames = entry + .getValue(); + + final MethodMetadataBuilder jsonToConverterMethod = getJsonToConverterMethod( + targetType, + jsonMethodNames.get(CustomDataJsonTags.FROM_JSON_METHOD)); + if (jsonToConverterMethod != null) { + builder.addMethod(jsonToConverterMethod); + bodyBuilder.appendFormalLine("registry.addConverter(" + + jsonToConverterMethod.getMethodName().getSymbolName() + + "());"); + } + + final MethodMetadataBuilder toJsonConverterMethod = getToJsonConverterMethod( + targetType, + jsonMethodNames.get(CustomDataJsonTags.TO_JSON_METHOD)); + if (toJsonConverterMethod != null) { + builder.addMethod(toJsonConverterMethod); + bodyBuilder.appendFormalLine("registry.addConverter(" + + toJsonConverterMethod.getMethodName().getSymbolName() + + "());"); + } + } + + final JavaType parameterType = FORMATTER_REGISTRY; + if (governorHasMethod(INSTALL_LABEL_CONVERTERS, parameterType)) { + return null; + } + + final List parameterNames = Arrays + .asList(new JavaSymbolName("registry")); + builder.getImportRegistrationResolver().addImport(parameterType); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + INSTALL_LABEL_CONVERTERS, JavaType.VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterType), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getJsonToConverterMethod( + final JavaType targetType, final JavaSymbolName jsonMethodName) { + final JavaSymbolName methodName = new JavaSymbolName("getJsonTo" + + targetType.getSimpleTypeName() + CONVERTER); + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType converterJavaType = SpringJavaType.getConverterType( + JavaType.STRING, targetType); + + final String base64Name = BASE_64.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final String typeName = targetType.getNameIncludingTypeParameters( + false, builder.getImportRegistrationResolver()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return new " + + converterJavaType.getNameIncludingTypeParameters() + "() {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("public " + targetType.getSimpleTypeName() + + " convert(String encodedJson) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return " + typeName + "." + + jsonMethodName.getSymbolName() + "(new String(" + base64Name + + ".decodeBase64(encodedJson)));"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("};"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + converterJavaType, bodyBuilder); + } + + /** + * Returns the "string to type" converter method to be generated, if any + * + * @param targetType + * the type being converted into (required) + * @param methodName + * the name of the method to generate if necessary (required) + * @param idType + * the ID type of the given target type (required) + * @return null if none is to be generated + */ + private MethodMetadataBuilder getStringToTypeConverterMethod( + final JavaType targetType, final JavaSymbolName methodName, + final JavaType idType) { + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType converterJavaType = SpringJavaType.getConverterType( + JavaType.STRING, targetType); + final String idTypeName = idType.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return new " + + converterJavaType.getNameIncludingTypeParameters() + "() {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("public " + + targetType.getFullyQualifiedTypeName() + + " convert(String id) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("return getObject().convert(getObject().convert(id, " + + idTypeName + + ".class), " + + targetType.getSimpleTypeName() + ".class);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("};"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + converterJavaType, bodyBuilder); + } + + private MethodMetadataBuilder getToJsonConverterMethod( + final JavaType targetType, final JavaSymbolName jsonMethodName) { + final JavaSymbolName methodName = new JavaSymbolName("get" + + targetType.getSimpleTypeName() + "ToJsonConverter"); + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType converterJavaType = SpringJavaType.getConverterType( + targetType, JavaType.STRING); + + final String base64Name = BASE_64.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + final String targetTypeName = StringUtils.uncapitalize(targetType + .getSimpleTypeName()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return new " + + converterJavaType.getNameIncludingTypeParameters() + "() {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("public String convert(" + + targetType.getSimpleTypeName() + " " + targetTypeName + + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return " + base64Name + + ".encodeBase64URLSafeString(" + targetTypeName + "." + + jsonMethodName.getSymbolName() + "().getBytes());"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("};"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + converterJavaType, bodyBuilder); + } + + private MethodMetadataBuilder getToStringConverterMethod( + final JavaType targetType, final JavaSymbolName methodName, + final List toStringMethods) { + if (governorHasMethod(methodName)) { + return null; + } + + final JavaType converterJavaType = SpringJavaType.getConverterType( + targetType, JavaType.STRING); + final String targetTypeName = StringUtils.uncapitalize(targetType + .getSimpleTypeName()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return new " + + converterJavaType.getNameIncludingTypeParameters() + "() {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("public String convert(" + + targetType.getSimpleTypeName() + " " + targetTypeName + + ") {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(getTypeToStringLine(targetType, + targetTypeName, toStringMethods)); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("};"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + converterJavaType, bodyBuilder); + } + + private String getTypeToStringLine(final JavaType targetType, + final String targetTypeName, + final List toStringMethods) { + if (toStringMethods.isEmpty()) { + return "return \"(no displayable fields)\";"; + } + + final StringBuilder sb = new StringBuilder("return new StringBuilder()"); + for (int i = 0; i < toStringMethods.size(); i++) { + if (i > 0) { + sb.append(".append(' ')"); + } + sb.append(".append("); + sb.append(targetTypeName); + sb.append("."); + sb.append(toStringMethods.get(i).getMethodName().getSymbolName()); + sb.append("())"); + } + sb.append(".toString();"); + return sb.toString(); + } + + private MethodMetadataBuilder getToTypeConverterMethod( + final JavaType targetType, final JavaSymbolName methodName, + final MemberTypeAdditions findMethod, final JavaType idType) { + final MethodMetadata toTypeConverterMethod = getGovernorMethod(methodName); + if (findMethod == null) { + return null; + } + if (toTypeConverterMethod != null) { + return new MethodMetadataBuilder(toTypeConverterMethod); + } + + findMethod.copyAdditionsTo(builder, governorTypeDetails); + final JavaType converterJavaType = SpringJavaType.getConverterType( + idType, targetType); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return new " + + converterJavaType.getNameIncludingTypeParameters() + "() {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("public " + + targetType.getFullyQualifiedTypeName() + " convert(" + idType + + " id) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return " + findMethod.getMethodCall() + + ";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("};"); + + return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, + converterJavaType, bodyBuilder); + } + + /** + * Method to generate embeddable to string method + * + * @param embeddableTypeDetails + * @return + */ + private List generateEmbeddableTypeToStringMethods() { + final List embeddableToStringMethods = new ArrayList(); + + for (JavaType embeddableType : this.embeddableTypes) { + + ClassOrInterfaceTypeDetails embeddableTypeDetails = typeLocationService + .getTypeDetails(embeddableType); + + // Generating method + final JavaSymbolName methodName = new JavaSymbolName(String.format( + "get%sToStringConverter", + embeddableType.getSimpleTypeName())); + + if (governorHasMethod(methodName)) { + continue; + } + + final JavaType converterJavaType = SpringJavaType.getConverterType( + embeddableType, JavaType.STRING); + final String targetTypeName = StringUtils + .uncapitalize(embeddableType.getSimpleTypeName()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return new " + + converterJavaType.getNameIncludingTypeParameters() + + "() {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("public String convert(" + + embeddableType.getSimpleTypeName() + " " + targetTypeName + + ") {"); + bodyBuilder.indent(); + + // Getting embedded class fields + List embeddableFields = embeddableTypeDetails + .getDeclaredFields(); + + if (embeddableFields.isEmpty()) { + bodyBuilder.appendFormalLine("return \"\";"); + } else { + StringBuilder sb = new StringBuilder(); + for (FieldMetadata field : embeddableFields) { + sb.append(String.format( + ".append(%s.get%s()).append(\" \")", + targetTypeName, field.getFieldName() + .getSymbolNameCapitalisedFirstLetter())); + } + + bodyBuilder.appendFormalLine(String.format( + "return new StringBuilder()%s.toString();", + sb.toString())); + } + + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("};"); + + embeddableToStringMethods + .add(new MethodMetadataBuilder(getId(), Modifier.PUBLIC, + methodName, converterJavaType, bodyBuilder)); + + } + + return embeddableToStringMethods; + } +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadataProvider.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadataProvider.java new file mode 100644 index 000000000..f65e5ba72 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadataProvider.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.web.mvc.controller.converter; + +import org.springframework.roo.addon.web.mvc.controller.scaffold.RooWebScaffold; +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Metadata provider for {@link ConversionServiceMetadata}. Monitors + * notifications for {@link RooConversionService} and {@link RooWebScaffold} + * annotated types. Also listens for changes to the scaffolded domain types and + * their associated domain types. + * + * @author Rossen Stoyanchev + * @author Stefan Schmidt + * @since 1.1.1 + */ +public interface ConversionServiceMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadataProviderImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadataProviderImpl.java new file mode 100644 index 000000000..93fa1ed58 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/ConversionServiceMetadataProviderImpl.java @@ -0,0 +1,333 @@ +package org.springframework.roo.addon.web.mvc.controller.converter; + +import static org.springframework.roo.model.RooJavaType.ROO_CONVERSION_SERVICE; +import static org.springframework.roo.model.RooJavaType.ROO_WEB_SCAFFOLD; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.json.CustomDataJsonTags; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link ConversionServiceMetadataProvider}. + * + * @author Rossen Stoyanchev + * @author Stefan Schmidt + * @since 1.1.1 + */ +@Component +@Service +public class ConversionServiceMetadataProviderImpl extends + AbstractItdMetadataProvider implements + ConversionServiceMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ConversionServiceMetadataProviderImpl.class); + + private final static JavaType EMBEDDABLE_ANNOTATION = new JavaType("javax.persistence.Embeddable"); + + // Stores the MID (as accepted by this + // ConversionServiceMetadataProviderImpl) for the one (and only one) + // application-wide conversion service + private String applicationConversionServiceFactoryBeanMid; + + private LayerService layerService; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getMetadataDependencyRegistry().registerDependency( + WebScaffoldMetadata.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_CONVERSION_SERVICE); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + ConversionServiceMetadata.class.getName(), javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + getMetadataDependencyRegistry().deregisterDependency( + WebScaffoldMetadata.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_CONVERSION_SERVICE); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier(final String metadataId) { + final JavaType javaType = PhysicalTypeIdentifierNamingUtils + .getJavaType(ConversionServiceMetadata.class.getName(), + metadataId); + final LogicalPath path = PhysicalTypeIdentifierNamingUtils.getPath( + ConversionServiceMetadata.class.getName(), metadataId); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "ConversionService"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(layerService == null){ + layerService = getLayerService(); + } + Validate.notNull(layerService, "LayerService is required"); + + applicationConversionServiceFactoryBeanMid = metadataIdentificationString; + + // To get here we know the governor is the + // ApplicationConversionServiceFactoryBean so let's go ahead and create + // its ITD + final Map> compositePrimaryKeyTypes = new HashMap>(); + final Set relevantDomainTypes = new LinkedHashSet(); + final Map findMethods = new HashMap(); + final Map idTypes = new HashMap(); + final Map> toStringMethods = new HashMap>(); + final List embeddableToStringMethods = new ArrayList(); + + for (final ClassOrInterfaceTypeDetails controllerTypeDetails : getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(ROO_WEB_SCAFFOLD)) { + getMetadataDependencyRegistry().registerDependency( + controllerTypeDetails.getDeclaredByMetadataId(), + metadataIdentificationString); + + final WebScaffoldAnnotationValues webScaffoldAnnotationValues = new WebScaffoldAnnotationValues( + controllerTypeDetails); + final JavaType formBackingObject = webScaffoldAnnotationValues + .getFormBackingObject(); + if (formBackingObject == null) { + continue; + } + final MemberDetails memberDetails = getMemberDetails(formBackingObject); + + // Find composite primary key types requiring a converter + final List embeddedIdFields = MemberFindingUtils + .getFieldsWithTag(memberDetails, + CustomDataKeys.EMBEDDED_ID_FIELD); + if (embeddedIdFields.size() > 1) { + throw new IllegalStateException( + "Found multiple embedded ID fields in " + + formBackingObject.getFullyQualifiedTypeName() + + " type. Only one is allowed."); + } + else if (embeddedIdFields.size() == 1) { + final Map jsonMethodNames = new LinkedHashMap(); + final MemberDetails fieldMemberDetails = getMemberDetails(embeddedIdFields + .get(0).getFieldType()); + final MethodMetadata fromJsonMethod = MemberFindingUtils + .getMostConcreteMethodWithTag(fieldMemberDetails, + CustomDataJsonTags.FROM_JSON_METHOD); + if (fromJsonMethod != null) { + jsonMethodNames.put(CustomDataJsonTags.FROM_JSON_METHOD, + fromJsonMethod.getMethodName()); + final MethodMetadata toJsonMethod = MemberFindingUtils + .getMostConcreteMethodWithTag(fieldMemberDetails, + CustomDataJsonTags.TO_JSON_METHOD); + if (toJsonMethod != null) { + jsonMethodNames.put(CustomDataJsonTags.TO_JSON_METHOD, + toJsonMethod.getMethodName()); + compositePrimaryKeyTypes.put(embeddedIdFields.get(0) + .getFieldType(), jsonMethodNames); + } + } + } + + final JavaType identifierType = getPersistenceMemberLocator() + .getIdentifierType(formBackingObject); + if (identifierType == null) { + // This type either has no ID field (e.g. an embedded type) or + // it's ID type is unknown right now; + // don't generate a converter for it; this will happen later if + // and when the ID field becomes known. + continue; + } + + relevantDomainTypes.add(formBackingObject); + idTypes.put(formBackingObject, identifierType); + final MemberTypeAdditions findMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + CustomDataKeys.FIND_METHOD.name(), + formBackingObject, identifierType, + LayerType.HIGHEST.getPosition(), + new MethodParameter(identifierType, "id")); + findMethods.put(formBackingObject, findMethod); + toStringMethods.put( + formBackingObject, + getToStringMethods(memberDetails, + metadataIdentificationString)); + } + + // Getting embeddable classes and adding toString methods + for (final ClassOrInterfaceTypeDetails embeddableTypeDetails : getTypeLocationService() + .findClassesOrInterfaceDetailsWithAnnotation(EMBEDDABLE_ANNOTATION)) { + JavaType embeddableType = embeddableTypeDetails.getType(); + // Adding embeddable type to generate conversor method + embeddableToStringMethods.add(embeddableType); + } + + return new ConversionServiceMetadata(getTypeLocationService(), metadataIdentificationString, + aspectName, governorPhysicalTypeMetadata, findMethods, idTypes, + relevantDomainTypes, compositePrimaryKeyTypes, toStringMethods, embeddableToStringMethods); + } + + public String getProvidesType() { + return MetadataIdentificationUtils + .create(ConversionServiceMetadata.class.getName()); + } + + private List getToStringMethods( + final MemberDetails memberDetails, + final String metadataIdentificationString) { + final List toStringMethods = new ArrayList(); + + int counter = 0; + for (final MethodMetadata method : memberDetails.getMethods()) { + // Track any changes to that method (eg it goes away) + getMetadataDependencyRegistry().registerDependency( + method.getDeclaredByMetadataId(), + metadataIdentificationString); + + if (counter < 4 && isMethodOfInterest(method, memberDetails)) { + counter++; + toStringMethods.add(method); + } + } + + return toStringMethods; + } + + private boolean isMethodOfInterest(final MethodMetadata method, + final MemberDetails memberDetails) { + if (!BeanInfoUtils.isAccessorMethod(method)) { + return false; // Only interested in accessors + } + if (method.getCustomData().keySet() + .contains(CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD) + || method.getCustomData().keySet() + .contains(CustomDataKeys.VERSION_ACCESSOR_METHOD)) { + return false; // Only interested in methods which are not accessors + // for persistence id or version fields + } + + final FieldMetadata field = BeanInfoUtils.getFieldForJavaBeanMethod( + memberDetails, method); + if (field == null) { + return false; + } + final JavaType fieldType = field.getFieldType(); + if (fieldType.isCommonCollectionType() + || fieldType.isArray() // Exclude collections and arrays + || getTypeLocationService().isInProject(fieldType) // Exclude + // references to + // other domain + // objects as they + // are too verbose + || fieldType.equals(JavaType.BOOLEAN_PRIMITIVE) + || fieldType.equals(JavaType.BOOLEAN_OBJECT) // Exclude boolean + // values as they + // would not be + // meaningful in + // this + // presentation + || field.getCustomData().keySet() + .contains(CustomDataKeys.EMBEDDED_FIELD) /* + * Not + * interested + * in embedded + * types + */) { + return false; + } + return true; + } + + @Override + protected String resolveDownstreamDependencyIdentifier( + final String upstreamDependency) { + if (MetadataIdentificationUtils.getMetadataClass(upstreamDependency) + .equals(MetadataIdentificationUtils + .getMetadataClass(WebScaffoldMetadata + .getMetadataIdentiferType()))) { + // A WebScaffoldMetadata upstream MID has changed or become + // available for the first time + // It's OK to return null if we don't yet know the MID because its + // JavaType has never been found + return applicationConversionServiceFactoryBeanMid; + } + + // It wasn't a WebScaffoldMetadata, so we can let the superclass handle + // it + // (it's expected it would be a PhysicalTypeIdentifier notification, as + // that's the only other thing we registered to receive) + return super.resolveDownstreamDependencyIdentifier(upstreamDependency); + } + + public LayerService getLayerService(){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on ConversionServiceMetadataProviderImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/RooConversionService.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/RooConversionService.java new file mode 100644 index 000000000..97831da81 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/converter/RooConversionService.java @@ -0,0 +1,30 @@ +package org.springframework.roo.addon.web.mvc.controller.converter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

    + * Designates a class as the application-wide conversion service for registering + * application Converters and Formatters. This conversion service is typically + * automatically installed by Spring ROO at the same time and in the same + * package as the first controller created through the "controller" command. + *

    + *

    + * The installed conversion service is a sub-type of + * FormattingConversionServiceFactoryBean. The installFormatters method can be + * used to manually install application converters and formatters. In additional + * ROO will generate methods to register converters for all application domain + * types that may need to be displayed as Strings in drop-downs as well as in + * various places in the UI. + *

    + * + * @author Rossen Stoyanchev + * @since 1.1.1 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooConversionService { +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/DateTimeFormatDetails.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/DateTimeFormatDetails.java new file mode 100644 index 000000000..5512df22b --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/DateTimeFormatDetails.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +/** + * Simple detail holder for date formats. + * + * @author Rossen Stoyanchev + * @since 1.1.2 + */ +public class DateTimeFormatDetails { + + /** + * Factory method for a {@link DateTimeFormatDetails} with the given pattern + * + * @param the pattern to set (can be null) + * @return a non-null instance + */ + public static DateTimeFormatDetails withPattern(final String pattern) { + final DateTimeFormatDetails instance = new DateTimeFormatDetails(); + instance.pattern = pattern; + return instance; + } + + /** + * Factory method for a {@link DateTimeFormatDetails} with the given style + * + * @param style the style to set (can be null) + * @return a non-null instance + */ + public static DateTimeFormatDetails withStyle(final String style) { + final DateTimeFormatDetails instance = new DateTimeFormatDetails(); + instance.style = style; + return instance; + } + + public String pattern; + public String style; + + @Override + public String toString() { + // For debugging + return style + ":" + pattern; + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/FinderMetadataDetails.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/FinderMetadataDetails.java new file mode 100644 index 000000000..aaa978c02 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/FinderMetadataDetails.java @@ -0,0 +1,72 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; + +/** + * Aggregates metadata for a given Roo finder which is scaffolded by Web + * add-ons. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +public class FinderMetadataDetails implements Comparable { + + private final MethodMetadata finderMethodMetadata; + private final List finderMethodParamFields; + private final String finderName; + + public FinderMetadataDetails(final String finderName, + final MethodMetadata finderMethodMetadata, + final List finderMethodParamFields) { + Validate.notBlank(finderName, "Finder name required"); + Validate.notNull(finderMethodMetadata, + "Finder method metadata required"); + this.finderName = finderName; + this.finderMethodMetadata = finderMethodMetadata; + this.finderMethodParamFields = finderMethodParamFields; + } + + public final int compareTo(final FinderMetadataDetails o) { + // NB: If adding more fields to this class ensure the equals(Object) + // method is updated accordingly + if (o == null) { + return -1; + } + final int cmp = finderName.compareTo(o.finderName); + return cmp; + } + + @Override + public final boolean equals(final Object obj) { + // NB: Not using the normal convention of delegating to compareTo (for + // efficiency reasons) + return obj instanceof FinderMetadataDetails + && finderName.equals(((FinderMetadataDetails) obj).finderName); + } + + public MethodMetadata getFinderMethodMetadata() { + return finderMethodMetadata; + } + + public List getFinderMethodParamFields() { + return finderMethodParamFields; + } + + public String getFinderName() { + return finderName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (finderName == null ? 0 : finderName.hashCode()); + return result; + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypeMetadataDetails.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypeMetadataDetails.java new file mode 100644 index 000000000..1898daae6 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypeMetadataDetails.java @@ -0,0 +1,107 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaType; + +/** + * Aggregates metadata for a given {@link JavaType} which is needed by Web + * scaffolding add-ons. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +public class JavaTypeMetadataDetails { + + private final String controllerPath; + private final boolean isApplicationType; + private final boolean isEnumType; + private final JavaType javaType; + private final JavaTypePersistenceMetadataDetails persistenceDetails; + private final String plural; + + /** + * Constructor for JavaTypeMetadataDetails. + * + * @param javaType (must not be null) + * @param plural (must contain text) + * @param isEnumType + * @param isApplicationType + * @param persistenceDetails (may be null if no persistence metadata is + * present for the javaType) + * @param controllerPath (must contain text) + */ + public JavaTypeMetadataDetails(final JavaType javaType, + final String plural, final boolean isEnumType, + final boolean isApplicationType, + final JavaTypePersistenceMetadataDetails persistenceDetails, + final String controllerPath) { + Validate.notNull(javaType, "Java type required"); + Validate.notBlank(plural, "Plural required"); + Validate.notBlank(controllerPath, "Controller path required"); + this.javaType = javaType; + this.plural = plural; + this.isEnumType = isEnumType; + this.isApplicationType = isApplicationType; + this.persistenceDetails = persistenceDetails; + this.controllerPath = controllerPath; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof JavaTypeMetadataDetails)) { + return false; + } + return javaType.equals(((JavaTypeMetadataDetails) obj).getJavaType()); + } + + public String getControllerPath() { + return controllerPath; + } + + public JavaType getJavaType() { + return javaType; + } + + public JavaTypePersistenceMetadataDetails getPersistenceDetails() { + return persistenceDetails; + } + + public String getPlural() { + return plural; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (javaType == null ? 0 : javaType.hashCode()); + return result; + } + + public boolean isApplicationType() { + return isApplicationType; + } + + public boolean isEnumType() { + return isEnumType; + } + + /** + * Indicates whether this {@link JavaType} is persisted by the application. + * + * @return false if for example it's an enum type + * @since 1.2.1 + */ + public boolean isPersistent() { + return isApplicationType && persistenceDetails != null; + } + + @Override + public String toString() { + // For debugging + return javaType.getFullyQualifiedTypeName(); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypePersistenceMetadataDetails.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypePersistenceMetadataDetails.java new file mode 100644 index 000000000..093e8e199 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypePersistenceMetadataDetails.java @@ -0,0 +1,257 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.model.JavaType; + +/** + * Aggregates persistence metadata for a given {@link JavaType} which is needed + * by Web scaffolding add-ons. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +public class JavaTypePersistenceMetadataDetails { + + private final MemberTypeAdditions countMethod; + private final MemberTypeAdditions findAllMethod; + private final MemberTypeAdditions findAllSortedMethod; + private final MemberTypeAdditions findEntriesMethod; + private final MemberTypeAdditions findEntriesSortedMethod; + private final List finderNames; + private final MemberTypeAdditions findMethod; + private final MethodMetadata identifierAccessorMethod; + private final FieldMetadata identifierField; + private final JavaType identifierType; + private final boolean isRooIdentifier; + private final MemberTypeAdditions mergeMethod; + private final MemberTypeAdditions persistMethod; + + private final MemberTypeAdditions removeMethod; + + private final List rooIdentifierFields; + private final MethodMetadata versionAccessorMethod; + + /** + * Constructor for JavaTypePersistenceMetadataDetails + * + * @param identifierType (must not be null) + * @param identifierField (must not be null) + * @param identifierAccessorMethod (must not be null) + * @param versionAccessorMethod (may be null if no version accessor is + * present) + * @param persistMethod (may be null if no persist method is present) + * @param mergeMethod (may be null if no merge method is present) + * @param removeMethod (may be null if no remove method is present) + * @param findAllSortedMethod (may be null if no findAll method is present) + * @param findMethod (may be null if no find method is present) + * @param countMethod (may be null if no count method is present) + * @param finderNames (must not be null, but may be empty of no finders are + * defined) + * @param isRooIdentifier + * @param rooIdentifierFields (must not be null, but may be empty of no + * finders are defined) + */ + public JavaTypePersistenceMetadataDetails(final JavaType identifierType, + final FieldMetadata identifierField, + final MethodMetadata identifierAccessorMethod, + final MethodMetadata versionAccessorMethod, + final MemberTypeAdditions persistMethod, + final MemberTypeAdditions mergeMethod, + final MemberTypeAdditions removeMethod, + final MemberTypeAdditions findAllMethod, + final MemberTypeAdditions findAllSortedMethod, + final MemberTypeAdditions findMethod, + final MemberTypeAdditions countMethod, + final MemberTypeAdditions findEntriesMethod, + final MemberTypeAdditions findEntriesSortedMethod, + final List finderNames, final boolean isRooIdentifier, + final List rooIdentifierFields) { + Validate.notNull(identifierType, "Indentifier type required"); + Validate.notNull(identifierField, "Indentifier field required"); + Validate.notNull(identifierAccessorMethod, + "Indentifier accessor method required"); + Validate.notNull(finderNames, "List of finder Names required"); + Validate.notNull(rooIdentifierFields, + "List of fields for Roo identifier required (may be empty)"); + + this.identifierType = identifierType; + this.countMethod = countMethod; + this.findAllMethod = findAllMethod; + this.findAllSortedMethod = findAllSortedMethod; + this.finderNames = finderNames; + this.findMethod = findMethod; + this.identifierAccessorMethod = identifierAccessorMethod; + this.identifierField = identifierField; + this.isRooIdentifier = isRooIdentifier; + this.mergeMethod = mergeMethod; + this.persistMethod = persistMethod; + this.removeMethod = removeMethod; + this.rooIdentifierFields = rooIdentifierFields; + this.versionAccessorMethod = versionAccessorMethod; + this.findEntriesMethod = findEntriesMethod; + this.findEntriesSortedMethod = findEntriesSortedMethod; + } + + /** + * Accessor for persistence count method + * + * @return the {@link MemberTypeAdditions} for the count method presented by + * the persistence MD (null if not defined) + */ + public MemberTypeAdditions getCountMethod() { + return countMethod; + } + + /** + * Accessor for persistence findAll method + * + * @return the {@link MemberTypeAdditions} for the findAll method presented + * by the persistence MD (null if not defined) + */ + public MemberTypeAdditions getFindAllMethod() { + return findAllMethod; + } + + /** + * Accessor for persistence findEntries method + * + * @return the {@link MemberTypeAdditions} for the findEntries method + * presented by the persistence MD (null if not defined) + */ + public MemberTypeAdditions getFindEntriesMethod() { + return findEntriesMethod; + } + + /** + * Accessor for persistence findAll method + * + * @return the {@link MemberTypeAdditions} for the findAll method presented + * by the persistence MD (null if not defined) + */ + public MemberTypeAdditions getFindAllSortedMethod() { + return findAllSortedMethod; + } + + /** + * Accessor for persistence findEntries method + * + * @return the {@link MemberTypeAdditions} for the findEntries method + * presented by the persistence MD (null if not defined) + */ + public MemberTypeAdditions getFindEntriesSortedMethod() { + return findEntriesSortedMethod; + } + + /** + * Accessor for finder names + * + * @return list of finder names (may be empty) + */ + public List getFinderNames() { + return finderNames; + } + + /** + * Accessor for persistence find method + * + * @return the {@link MemberTypeAdditions} for the find method presented by + * the persistence MD (null if not defined) + */ + public MemberTypeAdditions getFindMethod() { + return findMethod; + } + + /** + * Accessor for persistence identifier + * + * @return the {@link MethodMetadata} for the identifier accessor method + * presented by the persistence MD (never null) + */ + public MethodMetadata getIdentifierAccessorMethod() { + return identifierAccessorMethod; + } + + /** + * Field metadata for identifier + * + * @return the {@link FieldMetadata} for the identifier field presented by + * the persistence MD (never null) + */ + public FieldMetadata getIdentifierField() { + return identifierField; + } + + /** + * Identifier Type + * + * @return {@link JavaType} for id field + */ + public JavaType getIdentifierType() { + return identifierType; + } + + /** + * Accessor for persistence merge method + * + * @return the {@link MemberTypeAdditions} for the merge method presented by + * the persistence MD (null if not defined) + */ + public MemberTypeAdditions getMergeMethod() { + return mergeMethod; + } + + /** + * Accessor for persistence persist method + * + * @return the {@link MemberTypeAdditions} for the persist method presented + * by the persistence MD (null if not defined) + */ + public MemberTypeAdditions getPersistMethod() { + return persistMethod; + } + + /** + * Accessor for persistence remove method + * + * @return the {@link MemberTypeAdditions} for the remove method presented + * by the persistence MD (null if not defined) + */ + public MemberTypeAdditions getRemoveMethod() { + return removeMethod; + } + + /** + * Accessor for identifier field collection in cases where the identifier + * type is annotated with @RooIdentifier. + * + * @return list of Field metadata for fields defined in the type annotated + * with @RooIdentifier + */ + public List getRooIdentifierFields() { + return rooIdentifierFields; + } + + /** + * Accessor for persistence version + * + * @return the {@link MethodMetadata} for the version accessor method + * presented by the persistence MD (null if not defined) + */ + public MethodMetadata getVersionAccessorMethod() { + return versionAccessorMethod; + } + + /** + * Indicate if this type is annotated with @RooIdentifier + * + * @return true if annotation is present + */ + public boolean isRooIdentifier() { + return isRooIdentifier; + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/WebMetadataService.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/WebMetadataService.java new file mode 100644 index 000000000..e795020da --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/WebMetadataService.java @@ -0,0 +1,85 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Service to retrieve various metadata information for use by Web scaffolding + * add-ons. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +public interface WebMetadataService { + + Map getCrudAdditions( + JavaType domainType, String metadataIdentificationString); + + Map getDatePatterns( + JavaType javaType, MemberDetails memberDetails, + String metadataIdentificationString); + + List getDependentApplicationTypeMetadata( + JavaType javaType, MemberDetails memberDetails, + String metadataIdentificationString); + + /** + * Returns details of the dynamic finders for the given form backing type + * + * @param formBackingType + * @param formBackingTypeDetails + * @param consumingMetadataId the ID of the + * {@link org.springframework.roo.metadata.MetadataItem} that's + * using these details + * @return null if this information is not currently available + */ + Set getDynamicFinderMethodsAndFields( + JavaType formBackingType, MemberDetails formBackingTypeDetails, + String consumingMetadataId); + + FieldMetadata getIdentifierField(JavaType javaType); + + JavaTypeMetadataDetails getJavaTypeMetadataDetails(JavaType javaType, + MemberDetails memberDetails, String metadataIdentificationString); + + JavaTypePersistenceMetadataDetails getJavaTypePersistenceMetadataDetails( + JavaType javaType, MemberDetails memberDetails, + String metadataIdentificationString); + + MemberDetails getMemberDetails(JavaType javaType); + + /** + * Returns details of the Java types that are related to the given type + * + * @param baseType the type for which to obtain related types + * @param baseTypeDetails the details of the given type + * @param metadataId the ID of the + * {@link org.springframework.roo.metadata.MetadataItem} + * consuming the returned details; required for registering the + * necessary metadata dependencies + * @return a non-null map that includes the given type + */ + SortedMap getRelatedApplicationTypeMetadata( + JavaType baseType, MemberDetails baseTypeDetails, String metadataId); + + List getScaffoldEligibleFieldMetadata(JavaType javaType, + MemberDetails memberDetails, String metadataIdentificationString); + + /** + * @deprecated use {@link TypeLocationService#isInProject(JavaType)} instead + */ + @Deprecated + boolean isApplicationType(JavaType javaType); + + boolean isRooIdentifier(JavaType javaType, MemberDetails memberDetails); +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/WebMetadataServiceImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/WebMetadataServiceImpl.java new file mode 100644 index 000000000..e62ccb214 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/details/WebMetadataServiceImpl.java @@ -0,0 +1,841 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_TYPE; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.VERSION_ACCESSOR_METHOD; +import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; +import static org.springframework.roo.model.RooJavaType.ROO_WEB_SCAFFOLD; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; + +import java.beans.Introspector; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.finder.FinderMetadata; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.layers.LayerService; +import org.springframework.roo.classpath.layers.LayerType; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.layers.MethodParameter; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.CollectionUtils; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link WebMetadataService} to retrieve various metadata + * information for use by Web scaffolding add-ons. + * + * @author Stefan Schmidt + * @since 1.1.2 + */ +@Component +@Service +public class WebMetadataServiceImpl implements WebMetadataService { + + protected final static Logger LOGGER = HandlerUtils.getLogger(WebMetadataServiceImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private static final MethodParameter FIRST_RESULT_PARAMETER = new MethodParameter( + JavaType.INT_PRIMITIVE, "firstResult"); + private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition(); + private static final MethodParameter MAX_RESULTS_PARAMETER = new MethodParameter( + JavaType.INT_PRIMITIVE, "sizeNo"); + private static final MethodParameter SORT_FIELDNAME_PARAMETER = new MethodParameter( + JavaType.STRING, "sortFieldName"); + private static final MethodParameter SORT_ORDER_PARAMETER = new MethodParameter( + JavaType.STRING, "sortOrder"); + + private LayerService layerService; + private MemberDetailsScanner memberDetailsScanner; + private MetadataDependencyRegistry metadataDependencyRegistry; + private MetadataService metadataService; + private PersistenceMemberLocator persistenceMemberLocator; + private TypeLocationService typeLocationService; + + private final Map pathMap = new HashMap(); + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + private String getControllerPathForType(final JavaType type, + final String metadataIdentificationString) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + Validate.notNull(metadataService, "MetadataService is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + if (pathMap.containsKey(type.getFullyQualifiedTypeName()) + && !typeLocationService.hasTypeChanged(getClass().getName(), + type)) { + return pathMap.get(type.getFullyQualifiedTypeName()); + } + String webScaffoldMetadataKey = null; + WebScaffoldMetadata webScaffoldMetadata = null; + for (final ClassOrInterfaceTypeDetails cid : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(ROO_WEB_SCAFFOLD)) { + for (final AnnotationMetadata annotation : cid.getAnnotations()) { + if (annotation.getAnnotationType().equals(ROO_WEB_SCAFFOLD)) { + final AnnotationAttributeValue formBackingObject = annotation + .getAttribute(new JavaSymbolName( + "formBackingObject")); + if (formBackingObject instanceof ClassAttributeValue) { + final ClassAttributeValue formBackingObjectValue = (ClassAttributeValue) formBackingObject; + if (formBackingObjectValue.getValue().equals(type)) { + final AnnotationAttributeValue path = annotation + .getAttribute("path"); + if (path != null) { + final String pathString = path.getValue(); + pathMap.put(type.getFullyQualifiedTypeName(), + pathString); + return pathString; + } + final LogicalPath cidPath = PhysicalTypeIdentifier + .getPath(cid.getDeclaredByMetadataId()); + webScaffoldMetadataKey = WebScaffoldMetadata + .createIdentifier(cid.getName(), cidPath); + webScaffoldMetadata = (WebScaffoldMetadata) metadataService + .get(webScaffoldMetadataKey); + break; + } + } + } + } + } + if (webScaffoldMetadata != null) { + registerDependency(webScaffoldMetadataKey, + metadataIdentificationString); + final String path = webScaffoldMetadata.getAnnotationValues() + .getPath(); + pathMap.put(type.getFullyQualifiedTypeName(), path); + return path; + } + return getPlural(type, metadataIdentificationString).toLowerCase(); + } + + public Map getCrudAdditions( + final JavaType domainType, final String metadataId) { + + if(metadataDependencyRegistry == null){ + metadataDependencyRegistry = getMetadataDependencyRegistry(); + } + Validate.notNull(metadataDependencyRegistry, "MetadataDependencyRegistry is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + final String domainTypeMid = typeLocationService + .getPhysicalTypeIdentifier(domainType); + if (domainTypeMid != null) { + metadataDependencyRegistry.registerDependency(domainTypeMid, + metadataId); + } + + final JavaTypePersistenceMetadataDetails persistenceDetails = getJavaTypePersistenceMetadataDetails( + domainType, getMemberDetails(domainType), metadataId); + if (persistenceDetails == null) { + return Collections.emptyMap(); + } + final Map additions = new HashMap(); + additions.put(COUNT_ALL_METHOD, persistenceDetails.getCountMethod()); + additions.put(REMOVE_METHOD, persistenceDetails.getRemoveMethod()); + additions.put(FIND_METHOD, persistenceDetails.getFindMethod()); + additions.put(FIND_ALL_METHOD, persistenceDetails.getFindAllMethod()); + additions.put(FIND_ENTRIES_METHOD, + persistenceDetails.getFindEntriesMethod()); + additions.put(FIND_ALL_SORTED_METHOD, persistenceDetails.getFindAllSortedMethod()); + additions.put(FIND_ENTRIES_SORTED_METHOD, + persistenceDetails.getFindEntriesSortedMethod()); + additions.put(MERGE_METHOD, persistenceDetails.getMergeMethod()); + additions.put(PERSIST_METHOD, persistenceDetails.getPersistMethod()); + return additions; + } + + public Map getDatePatterns( + final JavaType javaType, final MemberDetails memberDetails, + final String metadataIdentificationString) { + + Validate.notNull(javaType, "Java type required"); + Validate.notNull(memberDetails, "Member details required"); + + final MethodMetadata identifierAccessor = getPersistenceMemberLocator() + .getIdentifierAccessor(javaType); + final MethodMetadata versionAccessor = getPersistenceMemberLocator() + .getVersionAccessor(javaType); + + final Map dates = new LinkedHashMap(); + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataDetails = getJavaTypePersistenceMetadataDetails( + javaType, memberDetails, metadataIdentificationString); + + for (final MethodMetadata method : memberDetails.getMethods()) { + // Only interested in accessors + if (!BeanInfoUtils.isAccessorMethod(method)) { + continue; + } + // Not interested in fields that are not exposed via a mutator and + // accessor and in identifiers and version fields + if (method.hasSameName(identifierAccessor, versionAccessor)) { + continue; + } + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field == null + || !BeanInfoUtils.hasAccessorAndMutator(field, + memberDetails)) { + continue; + } + final JavaType returnType = method.getReturnType(); + if (!JdkJavaType.isDateField(returnType)) { + continue; + } + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), + DATE_TIME_FORMAT); + final JavaSymbolName patternSymbol = new JavaSymbolName("pattern"); + final JavaSymbolName styleSymbol = new JavaSymbolName("style"); + DateTimeFormatDetails dateTimeFormat = null; + if (annotation != null) { + if (annotation.getAttributeNames().contains(styleSymbol)) { + dateTimeFormat = DateTimeFormatDetails.withStyle(annotation + .getAttribute(styleSymbol).getValue().toString()); + } + else if (annotation.getAttributeNames().contains(patternSymbol)) { + dateTimeFormat = DateTimeFormatDetails + .withPattern(annotation.getAttribute(patternSymbol) + .getValue().toString()); + } + } + if (dateTimeFormat != null) { + registerDependency(field.getDeclaredByMetadataId(), + metadataIdentificationString); + dates.put(field.getFieldName(), dateTimeFormat); + if (javaTypePersistenceMetadataDetails != null) { + for (final String finder : javaTypePersistenceMetadataDetails + .getFinderNames()) { + if (finder.contains(StringUtils.capitalize(field + .getFieldName().getSymbolName()) + "Between")) { + dates.put( + new JavaSymbolName("min" + + StringUtils.capitalize(field + .getFieldName() + .getSymbolName())), + dateTimeFormat); + dates.put( + new JavaSymbolName("max" + + StringUtils.capitalize(field + .getFieldName() + .getSymbolName())), + dateTimeFormat); + } + } + } + } + else { + LOGGER.warning("It is recommended to use @DateTimeFormat(style=\"M-\") on " + + field.getFieldType().getFullyQualifiedTypeName() + + "." + + field.getFieldName() + + " to use automatic date conversion in Spring MVC"); + } + } + return Collections.unmodifiableMap(dates); + } + + public List getDependentApplicationTypeMetadata( + final JavaType javaType, final MemberDetails memberDetails, + final String metadataIdentificationString) { + Validate.notNull(javaType, "Java type required"); + Validate.notNull(memberDetails, "Member details required"); + + final List dependentTypes = new ArrayList(); + for (final MethodMetadata method : memberDetails.getMethods()) { + final JavaType type = method.getReturnType(); + if (BeanInfoUtils.isAccessorMethod(method) + && isApplicationType(type)) { + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field != null + && MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), NOT_NULL) != null) { + final MemberDetails typeMemberDetails = getMemberDetails(type); + if (getJavaTypePersistenceMetadataDetails(type, + typeMemberDetails, metadataIdentificationString) != null) { + dependentTypes + .add(getJavaTypeMetadataDetails(type, + typeMemberDetails, + metadataIdentificationString)); + } + } + } + } + return Collections.unmodifiableList(dependentTypes); + } + + public Set getDynamicFinderMethodsAndFields( + final JavaType formBackingType, + final MemberDetails formBackingTypeDetails, + final String metadataIdentificationString) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + Validate.notNull(metadataService, "MetadataService is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + Validate.notNull(formBackingType, "Java type required"); + Validate.notNull(formBackingTypeDetails, "Member details required"); + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(formBackingType); + Validate.notNull( + formBackingType, + "Class or interface type details isn't available for type '%s'", + formBackingType); + final LogicalPath logicalPath = PhysicalTypeIdentifier + .getPath(javaTypeDetails.getDeclaredByMetadataId()); + final String finderMetadataKey = FinderMetadata.createIdentifier( + formBackingType, logicalPath); + registerDependency(finderMetadataKey, metadataIdentificationString); + final FinderMetadata finderMetadata = (FinderMetadata) metadataService + .get(finderMetadataKey); + if (finderMetadata == null) { + return null; + } + final SortedSet finderMetadataDetails = new TreeSet(); + for (final MethodMetadata method : finderMetadata + .getAllDynamicFinders()) { + final List parameterNames = method + .getParameterNames(); + final List parameterTypes = AnnotatedJavaType + .convertFromAnnotatedJavaTypes(method.getParameterTypes()); + final List fields = new ArrayList(); + for (int i = 0; i < parameterTypes.size(); i++) { + JavaSymbolName fieldName = null; + if (parameterNames.get(i).getSymbolName().startsWith("max") + || parameterNames.get(i).getSymbolName() + .startsWith("min")) { + fieldName = new JavaSymbolName( + Introspector.decapitalize(StringUtils + .capitalize(parameterNames.get(i) + .getSymbolName().substring(3)))); + } + else { + fieldName = parameterNames.get(i); + } + final FieldMetadata field = BeanInfoUtils + .getFieldForPropertyName(formBackingTypeDetails, + fieldName); + if (field != null) { + final FieldMetadataBuilder fieldMd = new FieldMetadataBuilder( + field); + fieldMd.setFieldName(parameterNames.get(i)); + fields.add(fieldMd.build()); + } + } + final FinderMetadataDetails details = new FinderMetadataDetails( + method.getMethodName().getSymbolName(), method, fields); + finderMetadataDetails.add(details); + } + + SortedSet finderMetadataDetailsWoCountMethods = new TreeSet(); + for(FinderMetadataDetails dynamicFinderMethod : finderMetadataDetails) { + if(!dynamicFinderMethod.getFinderName().startsWith("count")) { + finderMetadataDetailsWoCountMethods.add(dynamicFinderMethod); + } + } + + return Collections.unmodifiableSortedSet(finderMetadataDetailsWoCountMethods); + } + + public FieldMetadata getIdentifierField(final JavaType javaType) { + return CollectionUtils.firstElementOf(getPersistenceMemberLocator() + .getIdentifierFields(javaType)); + } + + public JavaTypeMetadataDetails getJavaTypeMetadataDetails( + final JavaType javaType, final MemberDetails memberDetails, + final String metadataIdentificationString) { + Validate.notNull(javaType, "Java type required"); + registerDependency( + memberDetails.getDetails() + .get(memberDetails.getDetails().size() - 1) + .getDeclaredByMetadataId(), + metadataIdentificationString); + return new JavaTypeMetadataDetails( + javaType, + getPlural(javaType, metadataIdentificationString), + isEnumType(javaType), + isApplicationType(javaType), + getJavaTypePersistenceMetadataDetails(javaType, memberDetails, + metadataIdentificationString), + getControllerPathForType(javaType, metadataIdentificationString)); + } + + public JavaTypePersistenceMetadataDetails getJavaTypePersistenceMetadataDetails( + final JavaType javaType, final MemberDetails memberDetails, + final String metadataIdentificationString) { + + + if(layerService == null){ + layerService = getLayerService(); + } + Validate.notNull(layerService, "LayerService is required"); + + Validate.notNull(javaType, "Java type required"); + Validate.notNull(memberDetails, "Member details required"); + Validate.notBlank(metadataIdentificationString, "Metadata id required"); + + final MethodMetadata idAccessor = memberDetails + .getMostConcreteMethodWithTag(IDENTIFIER_ACCESSOR_METHOD); + if (idAccessor == null) { + return null; + } + + final FieldMetadata idField = CollectionUtils + .firstElementOf(getPersistenceMemberLocator() + .getIdentifierFields(javaType)); + if (idField == null) { + return null; + } + + final JavaType idType = getPersistenceMemberLocator() + .getIdentifierType(javaType); + if (idType == null) { + return null; + } + + registerDependency(idAccessor.getDeclaredByMetadataId(), + metadataIdentificationString); + registerDependency(idField.getDeclaredByMetadataId(), + metadataIdentificationString); + + final MethodParameter entityParameter = new MethodParameter(javaType, + JavaSymbolName.getReservedWordSafeName(javaType)); + final MethodParameter idParameter = new MethodParameter(idType, idField + .getFieldName().getSymbolName()); + final MethodMetadata versionAccessor = memberDetails + .getMostConcreteMethodWithTag(VERSION_ACCESSOR_METHOD); + final MemberTypeAdditions persistMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + PERSIST_METHOD.name(), javaType, idType, + LAYER_POSITION, entityParameter); + final MemberTypeAdditions removeMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + REMOVE_METHOD.name(), javaType, idType, LAYER_POSITION, + entityParameter); + final MemberTypeAdditions mergeMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + MERGE_METHOD.name(), javaType, idType, LAYER_POSITION, + entityParameter); + final MemberTypeAdditions findAllMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_ALL_METHOD.name(), javaType, idType, + LAYER_POSITION); + final MemberTypeAdditions findAllSortedMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_ALL_SORTED_METHOD.name(), javaType, idType, + LAYER_POSITION, SORT_FIELDNAME_PARAMETER, + SORT_ORDER_PARAMETER); + final MemberTypeAdditions findMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_METHOD.name(), javaType, idType, LAYER_POSITION, + idParameter); + final MemberTypeAdditions countMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + COUNT_ALL_METHOD.name(), javaType, idType, + LAYER_POSITION); + final MemberTypeAdditions findEntriesMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_ENTRIES_METHOD.name(), javaType, idType, + LAYER_POSITION, FIRST_RESULT_PARAMETER, + MAX_RESULTS_PARAMETER); + final MemberTypeAdditions findEntriesSortedMethod = layerService + .getMemberTypeAdditions(metadataIdentificationString, + FIND_ENTRIES_SORTED_METHOD.name(), javaType, idType, + LAYER_POSITION, FIRST_RESULT_PARAMETER, + MAX_RESULTS_PARAMETER, SORT_FIELDNAME_PARAMETER, + SORT_ORDER_PARAMETER); + + final List dynamicFinderNames = memberDetails + .getDynamicFinderNames(); + + return new JavaTypePersistenceMetadataDetails(idType, idField, + idAccessor, versionAccessor, persistMethod, mergeMethod, + removeMethod, findAllMethod, findAllSortedMethod, findMethod, countMethod, + findEntriesMethod, findEntriesSortedMethod, dynamicFinderNames, isRooIdentifier( + javaType, memberDetails), + getPersistenceMemberLocator().getEmbeddedIdentifierFields(javaType)); + } + + public MemberDetails getMemberDetails(final JavaType javaType) { + + if(memberDetailsScanner == null){ + memberDetailsScanner = getMemberDetailsScanner(); + } + Validate.notNull(memberDetailsScanner, "MemberDetailsScanner is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(javaType); + Validate.notNull(cid, + "Unable to obtain physical type metadata for type %s", + javaType.getFullyQualifiedTypeName()); + return memberDetailsScanner.getMemberDetails( + WebMetadataServiceImpl.class.getName(), cid); + } + + private String getPlural(final JavaType javaType, + final String metadataIdentificationString) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + Validate.notNull(metadataService, "MetadataService is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + Validate.notNull(javaType, "Java type required"); + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(javaType); + Validate.notNull( + javaTypeDetails, + "Class or interface type details isn't available for type '%s'", + javaType); + final LogicalPath logicalPath = PhysicalTypeIdentifier + .getPath(javaTypeDetails.getDeclaredByMetadataId()); + final String pluralMetadataKey = PluralMetadata.createIdentifier( + javaType, logicalPath); + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(pluralMetadataKey); + if (pluralMetadata != null) { + registerDependency(pluralMetadata.getId(), + metadataIdentificationString); + final String plural = pluralMetadata.getPlural(); + if (plural.equalsIgnoreCase(javaType.getSimpleTypeName())) { + return plural + "Items"; + } + else { + return plural; + } + } + return javaType.getSimpleTypeName() + "s"; + } + + public SortedMap getRelatedApplicationTypeMetadata( + final JavaType baseType, final MemberDetails baseTypeDetails, + final String metadataIdentificationString) { + + Validate.notNull(baseType, "Java type required"); + Validate.notNull(baseTypeDetails, "Member details required"); + Validate.isTrue(isApplicationType(baseType), + "The type %s does not belong to this application", baseType); + + final SortedMap specialTypes = new TreeMap(); + specialTypes.put( + baseType, + getJavaTypeMetadataDetails(baseType, baseTypeDetails, + metadataIdentificationString)); + + for (final JavaType fieldType : baseTypeDetails + .getPersistentFieldTypes(baseType, getPersistenceMemberLocator())) { + if (isApplicationType(fieldType)) { + final MemberDetails fieldTypeDetails = getMemberDetails(fieldType); + specialTypes.put( + fieldType, + getJavaTypeMetadataDetails(fieldType, fieldTypeDetails, + metadataIdentificationString)); + } + } + + return specialTypes; + } + + public List getScaffoldEligibleFieldMetadata( + final JavaType javaType, final MemberDetails memberDetails, + final String metadataIdentificationString) { + + Validate.notNull(javaType, "Java type required"); + Validate.notNull(memberDetails, "Member details required"); + + final MethodMetadata identifierAccessor = getPersistenceMemberLocator() + .getIdentifierAccessor(javaType); + final MethodMetadata versionAccessor = getPersistenceMemberLocator() + .getVersionAccessor(javaType); + + final Map fields = new LinkedHashMap(); + final List methods = memberDetails.getMethods(); + + for (final MethodMetadata method : methods) { + // Only interested in accessors + if (!BeanInfoUtils.isAccessorMethod(method) + || method.hasSameName(identifierAccessor, versionAccessor)) { + continue; + } + + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field == null + || !BeanInfoUtils.hasAccessorAndMutator(field, + memberDetails)) { + continue; + } + final JavaSymbolName fieldName = field.getFieldName(); + registerDependency(method.getDeclaredByMetadataId(), + metadataIdentificationString); + if (!fields.containsKey(fieldName)) { + fields.put(fieldName, field); + } + } + return Collections.unmodifiableList(new ArrayList(fields + .values())); + } + + public boolean isApplicationType(final JavaType javaType) { + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + return typeLocationService.isInProject(javaType); + } + + private boolean isEnumType(final JavaType javaType) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + Validate.notNull(metadataService, "MetadataService is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + Validate.notNull(javaType, "Java type required"); + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(javaType); + if (javaTypeDetails != null) { + if (javaTypeDetails.getPhysicalTypeCategory().equals( + PhysicalTypeCategory.ENUMERATION)) { + return true; + } + } + return false; + } + + public boolean isRooIdentifier(final JavaType javaType, + final MemberDetails memberDetails) { + Validate.notNull(javaType, "Java type required"); + Validate.notNull(memberDetails, "Member details required"); + return MemberFindingUtils.getMemberHoldingTypeDetailsWithTag( + memberDetails, IDENTIFIER_TYPE).size() > 0; + } + + private void registerDependency(final String upstreamDependency, + final String downStreamDependency) { + + if(metadataDependencyRegistry == null){ + metadataDependencyRegistry = getMetadataDependencyRegistry(); + } + Validate.notNull(metadataDependencyRegistry, "MetadataDependencyRegistry is required"); + + if (metadataDependencyRegistry != null + && StringUtils.isNotBlank(upstreamDependency) + && StringUtils.isNotBlank(downStreamDependency) + && !upstreamDependency.equals(downStreamDependency) + && !MetadataIdentificationUtils.getMetadataClass( + downStreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(upstreamDependency))) { + metadataDependencyRegistry.registerDependency(upstreamDependency, + downStreamDependency); + } + } + + public LayerService getLayerService(){ + // Get all Services implement LayerService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(LayerService.class.getName(), null); + + for(ServiceReference ref : references){ + return (LayerService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load LayerService on WebMetadataServiceImpl."); + return null; + } + } + + public MemberDetailsScanner getMemberDetailsScanner(){ + // Get all Services implement MemberDetailsScanner interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MemberDetailsScanner.class.getName(), null); + + for(ServiceReference ref : references){ + return (MemberDetailsScanner) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MemberDetailsScanner on WebMetadataServiceImpl."); + return null; + } + } + + public MetadataDependencyRegistry getMetadataDependencyRegistry(){ + // Get all Services implement MetadataDependencyRegistry interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataDependencyRegistry.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataDependencyRegistry) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataDependencyRegistry on WebMetadataServiceImpl."); + return null; + } + } + + public MetadataService getMetadataService(){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on WebMetadataServiceImpl."); + return null; + } + } + + public PersistenceMemberLocator getPersistenceMemberLocator(){ + if(persistenceMemberLocator == null){ + // Get all Services implement PersistenceMemberLocator interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PersistenceMemberLocator.class.getName(), null); + + for(ServiceReference ref : references){ + return (PersistenceMemberLocator) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PersistenceMemberLocator on WebMetadataServiceImpl."); + return null; + } + }else{ + return persistenceMemberLocator; + } + + } + + public TypeLocationService getTypeLocationService(){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on WebMetadataServiceImpl."); + return null; + } + } + + +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/RooWebFinder.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/RooWebFinder.java new file mode 100644 index 000000000..de4ed7193 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/RooWebFinder.java @@ -0,0 +1,20 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires ROO controller support. + *

    + * This annotation will cause ROO to produce code that will expose dynamic + * finders to the Web UI. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooWebFinder { +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderCommands.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderCommands.java new file mode 100644 index 000000000..42fe49159 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderCommands.java @@ -0,0 +1,43 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands which provide finder functionality through Spring MVC controllers. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class WebFinderCommands implements CommandMarker { + + @Reference private WebFinderOperations webFinderOperations; + + @CliCommand(value = "web mvc finder add", help = "Adds @RooWebFinder annotation to MVC controller type") + public void add( + @CliOption(key = "formBackingType", mandatory = true, help = "The finder-enabled type") final JavaType finderType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The controller java type to apply this annotation to") final JavaType controllerType) { + + webFinderOperations.annotateType(controllerType, finderType); + } + + @CliCommand(value = "web mvc finder all", help = "Adds @RooWebFinder annotation to existing MVC controllers") + public void all() { + webFinderOperations.annotateAll(); + } + + @CliAvailabilityIndicator({ "web mvc finder add", "web mvc finder all" }) + public boolean isCommandAvailable() { + return webFinderOperations.isWebFinderInstallationPossible(); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadata.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadata.java new file mode 100644 index 000000000..38a9ce624 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadata.java @@ -0,0 +1,477 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; +import static org.springframework.roo.model.SpringJavaType.MODEL; +import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING; +import static org.springframework.roo.model.SpringJavaType.REQUEST_METHOD; +import static org.springframework.roo.model.SpringJavaType.REQUEST_PARAM; + +import java.beans.Introspector; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.web.mvc.controller.details.DateTimeFormatDetails; +import org.springframework.roo.addon.web.mvc.controller.details.FinderMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypePersistenceMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.scaffold.RooWebScaffold; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for finder functionality provided via {@link RooWebScaffold}. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +public class WebFinderMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String PROVIDES_TYPE_STRING = WebFinderMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private WebScaffoldAnnotationValues annotationValues; + private String controllerPath; + private JavaType formBackingType; + private JavaTypeMetadataDetails javaTypeMetadataHolder; + private Map specialDomainTypes; + + private Map dateTypes; + + /** + * Constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalTypeMetadata + * @param annotationValues + * @param specialDomainTypes + * @param dynamicFinderMethods + */ + public WebFinderMetadata( + final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final WebScaffoldAnnotationValues annotationValues, + final SortedMap specialDomainTypes, + final Set dynamicFinderMethods, + final Map dateTypes) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(specialDomainTypes, "Special domain type map required"); + Validate.notNull(dynamicFinderMethods, + "Dynamoic finder methods required"); + + this.dateTypes = dateTypes; + + if (!isValid()) { + return; + } + + this.annotationValues = annotationValues; + controllerPath = annotationValues.getPath(); + formBackingType = annotationValues.getFormBackingObject(); + this.specialDomainTypes = specialDomainTypes; + + if (dynamicFinderMethods.isEmpty()) { + valid = false; + return; + } + + javaTypeMetadataHolder = specialDomainTypes.get(formBackingType); + Validate.notNull(javaTypeMetadataHolder, + "Metadata holder required for form backing type %s", + formBackingType); + + for (final FinderMetadataDetails finder : dynamicFinderMethods) { + builder.addMethod(getFinderFormMethod(finder)); + builder.addMethod(getFinderMethod(finder)); + } + + itdTypeDetails = builder.build(); + } + + public WebScaffoldAnnotationValues getAnnotationValues() { + return annotationValues; + } + + private MethodMetadataBuilder getFinderFormMethod( + final FinderMetadataDetails finder) { + Validate.notNull(finder, "Method metadata required for finder"); + final JavaSymbolName finderFormMethodName = new JavaSymbolName(finder + .getFinderMethodMetadata().getMethodName().getSymbolName() + + "Form"); + + final List methodParameterTypes = new ArrayList(); + final List methodParameterNames = new ArrayList(); + final List finderParameterTypes = AnnotatedJavaType + .convertFromAnnotatedJavaTypes(finder.getFinderMethodMetadata() + .getParameterTypes()); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + + boolean needModel = false; + for (final JavaType finderParameterType : finderParameterTypes) { + JavaTypeMetadataDetails typeMd = specialDomainTypes + .get(finderParameterType); + JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = null; + if (finderParameterType.isCommonCollectionType()) { + typeMd = specialDomainTypes.get(finderParameterType + .getParameters().get(0)); + if (typeMd != null && typeMd.isApplicationType()) { + javaTypePersistenceMetadataHolder = typeMd + .getPersistenceDetails(); + } + } + else if (typeMd != null && typeMd.isEnumType()) { + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + typeMd.getPlural().toLowerCase() + + "\", java.util.Arrays.asList(" + + getShortName(finderParameterType) + + ".class.getEnumConstants()));"); + } + else if (typeMd != null && typeMd.isApplicationType()) { + javaTypePersistenceMetadataHolder = typeMd + .getPersistenceDetails(); + } + if (typeMd != null + && javaTypePersistenceMetadataHolder != null + && javaTypePersistenceMetadataHolder.getFindAllMethod() != null) { + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + typeMd.getPlural().toLowerCase() + + "\", " + + javaTypePersistenceMetadataHolder.getFindAllMethod() + .getMethodCall() + ");"); + } + needModel = true; + } + if (finderParameterTypes.contains(DATE) + || finderParameterTypes.contains(CALENDAR)) { + bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);"); + } + bodyBuilder.appendFormalLine("return \"" + + controllerPath + + "/" + + finder.getFinderMethodMetadata().getMethodName() + .getSymbolName() + "\";"); + + if (needModel) { + methodParameterTypes.add(MODEL); + methodParameterNames.add(new JavaSymbolName("uiModel")); + } + + if (governorHasMethod(finderFormMethodName, methodParameterTypes)) { + return null; + } + + final List> requestMappingAttributes = new ArrayList>(); + final List arrayValues = new ArrayList(); + arrayValues.add(new StringAttributeValue(new JavaSymbolName("value"), + "find=" + + finder.getFinderMethodMetadata() + .getMethodName() + .getSymbolName() + .replaceFirst( + "find" + + javaTypeMetadataHolder + .getPlural(), ""))); + arrayValues.add(new StringAttributeValue(new JavaSymbolName("value"), + "form")); + requestMappingAttributes + .add(new ArrayAttributeValue( + new JavaSymbolName("params"), arrayValues)); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "GET")))); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, finderFormMethodName, + JavaType.STRING, + AnnotatedJavaType.convertFromJavaTypes(methodParameterTypes), + methodParameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getFinderMethod( + final FinderMetadataDetails finderMetadataDetails) { + Validate.notNull(finderMetadataDetails, + "Method metadata required for finder"); + final JavaSymbolName finderMethodName = new JavaSymbolName( + finderMetadataDetails.getFinderMethodMetadata().getMethodName() + .getSymbolName()); + + final List parameterTypes = new ArrayList(); + final List parameterNames = new ArrayList(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final StringBuilder methodParams = new StringBuilder(); + + boolean dateFieldPresent = !dateTypes.isEmpty(); + for (final FieldMetadata field : finderMetadataDetails + .getFinderMethodParamFields()) { + final JavaSymbolName fieldName = field.getFieldName(); + final List annotations = new ArrayList(); + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue( + new JavaSymbolName("value"), uncapitalize(fieldName + .getSymbolName()))); + if (field.getFieldType().equals(JavaType.BOOLEAN_PRIMITIVE) + || field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)) { + attributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + } + final AnnotationMetadataBuilder requestParamAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, attributes); + annotations.add(requestParamAnnotation.build()); + if (field.getFieldType().equals(DATE) + || field.getFieldType().equals(CALENDAR)) { + dateFieldPresent = true; + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), + DATE_TIME_FORMAT); + if (annotation != null) { + getShortName(DATE_TIME_FORMAT); + annotations.add(annotation); + } + } + parameterNames.add(fieldName); + parameterTypes.add(new AnnotatedJavaType(field.getFieldType(), + annotations)); + + if (field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)) { + methodParams.append(fieldName + " == null ? Boolean.FALSE : " + + fieldName + ", "); + } + else { + methodParams.append(fieldName + ", "); + } + } + + if (methodParams.length() > 0) { + methodParams.delete(methodParams.length() - 2, + methodParams.length()); + } + + final List> firstResultAttributes = new ArrayList>(); + firstResultAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "page")); + firstResultAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder firstResultAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, firstResultAttributes); + + final List> maxResultsAttributes = new ArrayList>(); + maxResultsAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "size")); + maxResultsAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder maxResultAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, maxResultsAttributes); + + final List> sortFieldNameAttributes = new ArrayList>(); + sortFieldNameAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "sortFieldName")); + sortFieldNameAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder sortFieldNameAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, sortFieldNameAttributes); + + final List> sortOrderAttributes = new ArrayList>(); + sortOrderAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "sortOrder")); + sortOrderAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder sortOrderAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, sortOrderAttributes); + + + parameterTypes.add(new AnnotatedJavaType( + new JavaType(Integer.class.getName()), + firstResultAnnotation.build())); + parameterTypes.add(new AnnotatedJavaType( + new JavaType(Integer.class.getName()), + maxResultAnnotation.build())); + parameterTypes.add(new AnnotatedJavaType( + new JavaType(String.class.getName()), + sortFieldNameAnnotation.build())); + parameterTypes.add(new AnnotatedJavaType( + new JavaType(String.class.getName()), + sortOrderAnnotation.build())); + + parameterTypes.add(new AnnotatedJavaType(MODEL)); + if (getGovernorMethod(finderMethodName, + AnnotatedJavaType.convertFromAnnotatedJavaTypes(parameterTypes)) != null) { + return null; + } + + final List newParamNames = new ArrayList(); + newParamNames.addAll(parameterNames); + newParamNames.add(new JavaSymbolName("page")); + newParamNames.add(new JavaSymbolName("size")); + newParamNames.add(new JavaSymbolName("sortFieldName")); + newParamNames.add(new JavaSymbolName("sortOrder")); + newParamNames.add(new JavaSymbolName("uiModel")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("params"), "find=" + + finderMetadataDetails + .getFinderMethodMetadata() + .getMethodName() + .getSymbolName() + .replaceFirst( + "find" + + javaTypeMetadataHolder + .getPlural(), ""))); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "GET")))); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + bodyBuilder.appendFormalLine("if (page != null || size != null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("int sizeNo = size == null ? 10 : size.intValue();"); + bodyBuilder + .appendFormalLine("final int firstResult = page == null ? 0 : (page.intValue() - 1) * sizeNo;"); + String methodParamsString = methodParams.toString(); + if(StringUtils.isNotBlank(methodParamsString)){ + methodParamsString.concat(", "); + } + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + javaTypeMetadataHolder.getPlural().toLowerCase() + + "\", " + + getShortName(formBackingType) + + "." + + finderMetadataDetails.getFinderMethodMetadata() + .getMethodName().getSymbolName() + "(" + + methodParamsString + ", sortFieldName, sortOrder).setFirstResult(firstResult).setMaxResults(sizeNo).getResultList());"); + + char[] methodNameArray = finderMetadataDetails.getFinderMethodMetadata() + .getMethodName().getSymbolName().toCharArray(); + methodNameArray[0] = Character.toUpperCase(methodNameArray[0]); + String countMethodName = "count" + new String(methodNameArray); + + bodyBuilder.appendFormalLine("float nrOfPages = (float) " + + getShortName(formBackingType) + + "." + + countMethodName + "(" + + methodParamsString + ") / sizeNo;"); + bodyBuilder + .appendFormalLine("uiModel.addAttribute(\"maxPages\", (int) ((nrOfPages > (int) nrOfPages || nrOfPages == 0.0) ? nrOfPages + 1 : nrOfPages));"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + javaTypeMetadataHolder.getPlural().toLowerCase() + + "\", " + + getShortName(formBackingType) + + "." + + finderMetadataDetails.getFinderMethodMetadata() + .getMethodName().getSymbolName() + "(" + + methodParamsString + ", sortFieldName, sortOrder).getResultList());"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + if (dateFieldPresent) { + bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);"); + } + bodyBuilder.appendFormalLine("return \"" + controllerPath + "/list\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, finderMethodName, JavaType.STRING, + parameterTypes, newParamNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private String getShortName(final JavaType type) { + return type.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } + + private String uncapitalize(final String term) { + // [ROO-1790] this is needed to adhere to the JavaBean naming + // conventions (see JavaBean spec section 8.8) + return Introspector.decapitalize(StringUtils.capitalize(term)); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadataProvider.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadataProvider.java new file mode 100644 index 000000000..ff2672f97 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link WebFinderMetadata}. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +public interface WebFinderMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadataProviderImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadataProviderImpl.java new file mode 100644 index 000000000..7181ee94e --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderMetadataProviderImpl.java @@ -0,0 +1,171 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import static org.springframework.roo.model.RooJavaType.ROO_WEB_FINDER; + +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.web.mvc.controller.details.DateTimeFormatDetails; +import org.springframework.roo.addon.web.mvc.controller.details.FinderMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.WebMetadataService; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + + +/** + * Implementation of {@link WebFinderMetadataProvider}. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +@Component +@Service +public class WebFinderMetadataProviderImpl extends AbstractItdMetadataProvider + implements WebFinderMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(WebFinderMetadataProviderImpl.class); + + private WebMetadataService webMetadataService; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_WEB_FINDER); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return WebFinderMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_WEB_FINDER); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = WebFinderMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = WebFinderMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Controller_Finder"; + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + if(webMetadataService == null){ + webMetadataService = getWebMetadataService(); + } + Validate.notNull(webMetadataService, "WebMetadataService is required"); + + // We need to parse the annotation, which we expect to be present + final WebScaffoldAnnotationValues annotationValues = new WebScaffoldAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound() + || !annotationValues.isExposeFinders() + || annotationValues.getFormBackingObject() == null + || governorPhysicalTypeMetadata.getMemberHoldingTypeDetails() == null) { + return null; + } + + // Lookup the form backing object's metadata + final JavaType formBackingType = annotationValues + .getFormBackingObject(); + final ClassOrInterfaceTypeDetails formBackingTypeDetails = getTypeLocationService() + .getTypeDetails(formBackingType); + if (formBackingTypeDetails == null + || !formBackingTypeDetails.getCustomData().keySet() + .contains(CustomDataKeys.PERSISTENT_TYPE)) { + return null; + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + formBackingTypeDetails.getDeclaredByMetadataId(), + metadataIdentificationString); + + final MemberDetails formBackingObjectMemberDetails = getMemberDetails(formBackingTypeDetails); + final Set dynamicFinderMethods = webMetadataService + .getDynamicFinderMethodsAndFields(formBackingType, + formBackingObjectMemberDetails, + metadataIdentificationString); + if (dynamicFinderMethods == null) { + return null; + } + + + final SortedMap relatedApplicationTypeMetadata = webMetadataService + .getRelatedApplicationTypeMetadata(formBackingType, + formBackingObjectMemberDetails, + metadataIdentificationString); + + final Map datePatterns = webMetadataService + .getDatePatterns(formBackingType, + formBackingObjectMemberDetails, + metadataIdentificationString); + + return new WebFinderMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues, + relatedApplicationTypeMetadata, dynamicFinderMethods, datePatterns); + } + + public String getProvidesType() { + return WebFinderMetadata.getMetadataIdentiferType(); + } + + public WebMetadataService getWebMetadataService(){ + // Get all Services implement WebMetadataService interface + try { + ServiceReference[] references = context.getAllServiceReferences(WebMetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (WebMetadataService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load WebMetadataService on WebFinderMetadataProviderImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderOperations.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderOperations.java new file mode 100644 index 000000000..88a7f8bde --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderOperations.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import org.springframework.roo.model.JavaType; + +/** + * Provides operations for Web MVC finder functionality. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface WebFinderOperations { + + void annotateAll(); + + void annotateType(JavaType controllerType, JavaType entityType); + + boolean isWebFinderInstallationPossible(); +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderOperationsImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderOperationsImpl.java new file mode 100644 index 000000000..55075eeae --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/finder/WebFinderOperationsImpl.java @@ -0,0 +1,117 @@ +package org.springframework.roo.addon.web.mvc.controller.finder; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.controller.ControllerOperations; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.classpath.PhysicalTypeDetails; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Implementation of {@link WebFinderOperations} + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class WebFinderOperationsImpl implements WebFinderOperations { + + @Reference private ControllerOperations controllerOperations; + @Reference private MetadataService metadataService; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void annotateAll() { + // First, find all entities with finders. + final Set finderEntities = new HashSet(); + for (final ClassOrInterfaceTypeDetails cod : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_JPA_ACTIVE_RECORD)) { + if (MemberFindingUtils.getAnnotationOfType(cod.getAnnotations(), + RooJavaType.ROO_JPA_ACTIVE_RECORD).getAttribute("finders") != null) { + finderEntities.add(cod.getName()); + } + } + + // Second, find controllers for those entities. + for (final ClassOrInterfaceTypeDetails cod : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_WEB_SCAFFOLD)) { + final PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService + .get(typeLocationService.getPhysicalTypeIdentifier(cod + .getName())); + Validate.notNull(ptm, "Java source code unavailable for type %s", + cod.getName().getFullyQualifiedTypeName()); + final WebScaffoldAnnotationValues webScaffoldAnnotationValues = new WebScaffoldAnnotationValues( + ptm); + for (final JavaType finderEntity : finderEntities) { + if (finderEntity.equals(webScaffoldAnnotationValues + .getFormBackingObject())) { + annotateType(cod.getName(), finderEntity); + break; + } + } + } + } + + public void annotateType(final JavaType controllerType, + final JavaType entityType) { + Validate.notNull(controllerType, "Controller type required"); + Validate.notNull(entityType, "Entity type required"); + + final String id = typeLocationService + .getPhysicalTypeIdentifier(controllerType); + if (id == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + controllerType.getFullyQualifiedTypeName() + "'"); + } + + // Obtain the physical type and itd mutable details + final PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService + .get(id); + Validate.notNull(ptm, "Java source code unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(id)); + final WebScaffoldAnnotationValues webScaffoldAnnotationValues = new WebScaffoldAnnotationValues( + ptm); + if (!webScaffoldAnnotationValues.isAnnotationFound() + || !webScaffoldAnnotationValues.getFormBackingObject().equals( + entityType)) { + throw new IllegalArgumentException( + "Aborting, this controller type does not manage the " + + entityType.getSimpleTypeName() + + " form backing type."); + } + + final PhysicalTypeDetails ptd = ptm.getMemberHoldingTypeDetails(); + Validate.notNull(ptd, + "Java source code details unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(id)); + final ClassOrInterfaceTypeDetails cid = (ClassOrInterfaceTypeDetails) ptd; + if (null == MemberFindingUtils.getAnnotationOfType( + cid.getAnnotations(), RooJavaType.ROO_WEB_FINDER)) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + RooJavaType.ROO_WEB_FINDER)); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + } + + public boolean isWebFinderInstallationPossible() { + return controllerOperations.isControllerInstallationPossible(); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/RooWebJson.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/RooWebJson.java new file mode 100644 index 000000000..535cbe54a --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/RooWebJson.java @@ -0,0 +1,136 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires ROO controller support. + *

    + * This annotation will cause ROO to produce code that would typically appear in + * MVC JSON-enabled controllers. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooWebJson { + + /** + * The default prefix of the "create" method + */ + String CREATE_FROM_JSON = "createFromJson"; + + /** + * The default prefix of the "create from array" method + */ + String CREATE_FROM_JSON_ARRAY = "createFromJsonArray"; + + /** + * The default prefix of the "delete" method + */ + String DELETE_FROM_JSON_ARRAY = "deleteFromJson"; + + /** + * Expose finders by default + */ + boolean EXPOSE_FINDERS = true; + + /** + * The default prefix of the "find all" method + */ + String LIST_JSON = "listJson"; + + /** + * The default prefix of the "show" method + */ + String SHOW_JSON = "showJson"; + + /** + * The default prefix of the "update" method + */ + String UPDATE_FROM_JSON = "updateFromJson"; + + /** + * The default prefix of the "update from array" method + */ + String UPDATE_FROM_JSON_ARRAY = "updateFromJsonArray"; + + /** + * Creates a createFromJsonArray() method which finds all objects. Set + * methodName to "" to prevent its generation. + * + * @return indicates the method name for the createFromJsonArray() method + * (optional) + */ + String createFromJsonArrayMethod() default CREATE_FROM_JSON_ARRAY; + + /** + * Creates a createFromJson() method which finds all objects. Set methodName + * to "" to prevent its generation. + * + * @return indicates the method name for the createFromJson() method + * (optional) + */ + String createFromJsonMethod() default CREATE_FROM_JSON; + + /** + * Creates a deleteFromJson() method which finds all objects. Set methodName + * to "" to prevent its generation. + * + * @return indicates the method name for the deleteFromJson() method + * (optional) + */ + String deleteFromJsonMethod() default DELETE_FROM_JSON_ARRAY; + + /** + * Will scan the formBackingObjects for installed finder methods and expose + * them when configured. + * + * @return indicates if the finders methods should be provided (defaults to + * "true"; optional) + */ + boolean exposeFinders() default EXPOSE_FINDERS; + + /** + * Every controller is responsible for a single JSON-enabled object. The + * backing object defined here class will be exposed in a RESTful way. + */ + Class jsonObject(); + + /** + * Creates a listJson() method which finds all objects. Set methodName to "" + * to prevent its generation. + * + * @return indicates the method name for the listJson() method (optional) + */ + String listJsonMethod() default LIST_JSON; + + /** + * Creates a showJson() method which finds an object for a given id. Set + * methodName to "" to prevent its generation. + * + * @return indicates the method name for the showJson() method (optional) + */ + String showJsonMethod() default SHOW_JSON; + + /** + * Creates a updateFromJsonArray() method which finds all objects. Set + * methodName to "" to prevent its generation. + * + * @return indicates the method name for the updateFromJsonArray() method + * (optional) + */ + String updateFromJsonArrayMethod() default UPDATE_FROM_JSON_ARRAY; + + /** + * Creates a updateFromJson() method which finds all objects. Set methodName + * to "" to prevent its generation. + * + * @return indicates the method name for the updateFromJson() method + * (optional) + */ + String updateFromJsonMethod() default UPDATE_FROM_JSON; +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonAnnotationValues.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonAnnotationValues.java new file mode 100644 index 000000000..b4edbe3ef --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonAnnotationValues.java @@ -0,0 +1,83 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.CREATE_FROM_JSON; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.CREATE_FROM_JSON_ARRAY; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.DELETE_FROM_JSON_ARRAY; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.EXPOSE_FINDERS; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.LIST_JSON; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.SHOW_JSON; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.UPDATE_FROM_JSON; +import static org.springframework.roo.addon.web.mvc.controller.json.RooWebJson.UPDATE_FROM_JSON_ARRAY; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooWebJson} annotation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class WebJsonAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate String createFromJsonArrayMethod = CREATE_FROM_JSON_ARRAY; + @AutoPopulate String createFromJsonMethod = CREATE_FROM_JSON; + @AutoPopulate String deleteFromJsonMethod = DELETE_FROM_JSON_ARRAY; + @AutoPopulate boolean exposeFinders = EXPOSE_FINDERS; + @AutoPopulate JavaType jsonObject; + @AutoPopulate String listJsonMethod = LIST_JSON; + @AutoPopulate String showJsonMethod = SHOW_JSON; + @AutoPopulate String updateFromJsonArrayMethod = UPDATE_FROM_JSON_ARRAY; + @AutoPopulate String updateFromJsonMethod = UPDATE_FROM_JSON; + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public WebJsonAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_WEB_JSON); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public String getCreateFromJsonArrayMethod() { + return createFromJsonArrayMethod; + } + + public String getCreateFromJsonMethod() { + return createFromJsonMethod; + } + + public String getDeleteFromJsonMethod() { + return deleteFromJsonMethod; + } + + public JavaType getJsonObject() { + return jsonObject; + } + + public String getListJsonMethod() { + return listJsonMethod; + } + + public String getShowJsonMethod() { + return showJsonMethod; + } + + public String getUpdateFromJsonArrayMethod() { + return updateFromJsonArrayMethod; + } + + public String getUpdateFromJsonMethod() { + return updateFromJsonMethod; + } + + public boolean isExposeFinders() { + return exposeFinders; + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonCommands.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonCommands.java new file mode 100644 index 000000000..0b3dbe919 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonCommands.java @@ -0,0 +1,57 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import static org.springframework.roo.shell.OptionContexts.UPDATE; +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands which provide JSON functionality through Spring MVC controllers. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class WebJsonCommands implements CommandMarker { + + @Reference private WebJsonOperations webJsonOperations; + + @CliCommand(value = "web mvc json add", help = "Adds @RooJson annotation to target type") + public void add( + @CliOption(key = "jsonObject", mandatory = true, help = "The JSON-enabled object which backs this Spring MVC controller.") final JavaType jsonObject, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The java type to apply this annotation to") final JavaType target) { + + webJsonOperations.annotateType(target, jsonObject); + } + + @CliCommand(value = "web mvc json all", help = "Adds or creates MVC controllers annotated with @RooWebJson annotation") + public void all( + @CliOption(key = "package", mandatory = false, optionContext = UPDATE, help = "The package in which new controllers will be placed") final JavaPackage javaPackage) { + + webJsonOperations.annotateAll(javaPackage); + } + + @CliAvailabilityIndicator({ "web mvc json add", "web mvc json all" }) + public boolean isCommandAvailable() { + return webJsonOperations.isWebJsonCommandAvailable(); + } + + @CliAvailabilityIndicator({ "web mvc json setup" }) + public boolean isSetupAvailable() { + return webJsonOperations.isWebJsonInstallationPossible(); + } + + @CliCommand(value = "web mvc json setup", help = "Set up Spring MVC to support JSON") + public void setup() { + webJsonOperations.setup(); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadata.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadata.java new file mode 100644 index 000000000..c7acf557c --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadata.java @@ -0,0 +1,855 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import static java.lang.reflect.Modifier.PUBLIC; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; +import static org.springframework.roo.model.SpringJavaType.HTTP_HEADERS; +import static org.springframework.roo.model.SpringJavaType.HTTP_STATUS; +import static org.springframework.roo.model.SpringJavaType.PATH_VARIABLE; +import static org.springframework.roo.model.SpringJavaType.REQUEST_BODY; +import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING; +import static org.springframework.roo.model.SpringJavaType.REQUEST_METHOD; +import static org.springframework.roo.model.SpringJavaType.REQUEST_PARAM; +import static org.springframework.roo.model.SpringJavaType.RESPONSE_BODY; +import static org.springframework.roo.model.SpringJavaType.RESPONSE_ENTITY; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.json.JsonMetadata; +import org.springframework.roo.addon.web.mvc.controller.details.FinderMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.scaffold.RooWebScaffold; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for Json functionality provided through {@link RooWebScaffold}. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +public class WebJsonMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final String CONTENT_TYPE = "application/json"; + private static final String PROVIDES_TYPE_STRING = WebJsonMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + private static final JavaType RESPONSE_ENTITY_STRING = new JavaType( + RESPONSE_ENTITY.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(JavaType.STRING)); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private WebJsonAnnotationValues annotationValues; + private boolean introduceLayerComponents; + private JavaType jsonEnabledType; + private String jsonEnabledTypeShortName; + private JsonMetadata jsonMetadata; + private String jsonBeanName; + + /** + * Constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalTypeMetadata + * @param annotationValues + * @param persistenceAdditions + * @param identifierField + * @param plural + * @param finderDetails (required) + * @param jsonMetadata + * @param introduceLayerComponents whether to introduce any required layer + * components (services, repositories, etc.) + */ + public WebJsonMetadata( + final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final WebJsonAnnotationValues annotationValues, + final Map persistenceAdditions, + final FieldMetadata identifierField, final String plural, + final Set finderDetails, + final JsonMetadata jsonMetadata, + final boolean introduceLayerComponents) { + super(identifier, aspectName, governorPhysicalTypeMetadata); + Validate.isTrue(isValid(identifier), + "Metadata identification string '%s' is invalid", identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(persistenceAdditions, "Persistence additions required"); + Validate.notNull(finderDetails, + "Set of dynamic finder methods cannot be null"); + Validate.notNull(jsonMetadata, "Json metadata required"); + + if (!isValid()) { + return; + } + + this.annotationValues = annotationValues; + jsonEnabledType = annotationValues.getJsonObject(); + jsonEnabledTypeShortName = getShortName(jsonEnabledType); + jsonBeanName = JavaSymbolName.getReservedWordSafeName(jsonEnabledType) + .getSymbolName(); + + this.introduceLayerComponents = introduceLayerComponents; + this.jsonMetadata = jsonMetadata; + + final MemberTypeAdditions findMethod = persistenceAdditions + .get(FIND_METHOD); + builder.addMethod(getShowJsonMethod(identifierField, findMethod)); + + final MemberTypeAdditions findAllMethod = persistenceAdditions + .get(FIND_ALL_METHOD); + builder.addMethod(getListJsonMethod(findAllMethod)); + + final MemberTypeAdditions persistMethod = persistenceAdditions + .get(PERSIST_METHOD); + builder.addMethod(getCreateFromJsonMethod(persistMethod)); + builder.addMethod(getCreateFromJsonArrayMethod(persistMethod)); + + final MemberTypeAdditions mergeMethod = persistenceAdditions + .get(MERGE_METHOD); + builder.addMethod(getUpdateFromJsonMethod(identifierField, mergeMethod)); +// builder.addMethod(getUpdateFromJsonArrayMethod(mergeMethod)); + + final MemberTypeAdditions removeMethod = persistenceAdditions + .get(REMOVE_METHOD); + builder.addMethod(getDeleteFromJsonMethod(removeMethod, + identifierField, findMethod)); + + if (annotationValues.isExposeFinders()) { + for (final FinderMetadataDetails finder : finderDetails) { + builder.addMethod(getJsonFindMethod(finder, plural)); + } + } + + itdTypeDetails = builder.build(); + } + + private void openTry(InvocableMemberBodyBuilder bodyBuilder) { + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + } + + private void closeTry(InvocableMemberBodyBuilder bodyBuilder, + String responseEntityShortName, String httpStatusShortName) { + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} catch (Exception e) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return new " + responseEntityShortName + + "(\"{\\\"ERROR\\\":\"+e.getMessage()+\"\\\"}\", headers, " + + httpStatusShortName + ".INTERNAL_SERVER_ERROR);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + } + + private MethodMetadataBuilder getCreateFromJsonArrayMethod( + final MemberTypeAdditions persistMethod) { + if (StringUtils + .isBlank(annotationValues.getCreateFromJsonArrayMethod()) + || persistMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getCreateFromJsonArrayMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName fromJsonArrayMethodName = jsonMetadata + .getFromJsonArrayMethodName(); + + final AnnotationMetadataBuilder requestBodyAnnotation = new AnnotationMetadataBuilder( + REQUEST_BODY); + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(JavaType.STRING, + requestBodyAnnotation.build())); + final List parameterNames = Arrays + .asList(new JavaSymbolName("json")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), "/jsonArray")); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "POST")))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final List params = new ArrayList(); + params.add(jsonEnabledType); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "\");"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine("for (" + jsonEnabledTypeShortName + " " + + jsonBeanName + ": " + jsonEnabledTypeShortName + "." + + fromJsonArrayMethodName.getSymbolName() + "(json)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".CREATED);"); + closeTry(bodyBuilder, getShortName(RESPONSE_ENTITY), + getShortName(HTTP_STATUS)); + + if (introduceLayerComponents) { + persistMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getCreateFromJsonMethod( + final MemberTypeAdditions persistMethod) { + if (StringUtils.isBlank(annotationValues.getCreateFromJsonMethod()) + || persistMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getCreateFromJsonMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName fromJsonMethodName = jsonMetadata + .getFromJsonMethodName(); + + final AnnotationMetadataBuilder requestBodyAnnotation = new AnnotationMetadataBuilder( + REQUEST_BODY); + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(JavaType.STRING, + requestBodyAnnotation.build()), + AnnotatedJavaType.convertFromJavaType(new JavaType( + "org.springframework.web.util.UriComponentsBuilder"))); + final List parameterNames = Arrays + .asList( + new JavaSymbolName("json"), new JavaSymbolName("uriBuilder")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "POST")))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "\");"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine(jsonEnabledTypeShortName + " " + + jsonBeanName + " = " + jsonEnabledTypeShortName + "." + + fromJsonMethodName.getSymbolName() + "(json);"); + bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine( + "RequestMapping a = (RequestMapping) getClass().getAnnotation(RequestMapping.class);"); + bodyBuilder.appendFormalLine( + "headers.add(\"Location\",uriBuilder.path(a.value()[0]+\"/\"+" + + jsonBeanName + + ".getId().toString()).build().toUriString());"); + bodyBuilder + .appendFormalLine("return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".CREATED);"); + closeTry(bodyBuilder, getShortName(RESPONSE_ENTITY), + getShortName(HTTP_STATUS)); + + if (introduceLayerComponents) { + persistMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getDeleteFromJsonMethod( + final MemberTypeAdditions removeMethod, + final FieldMetadata identifierField, + final MemberTypeAdditions findMethod) { + if (StringUtils.isBlank(annotationValues.getDeleteFromJsonMethod()) + || removeMethod == null || identifierField == null + || findMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getDeleteFromJsonMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue(new JavaSymbolName("value"), + identifierField.getFieldName().getSymbolName())); + final AnnotationMetadataBuilder pathVariableAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE, attributes); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(identifierField.getFieldType(), + pathVariableAnnotation.build())); + final List parameterNames = Arrays + .asList(identifierField.getFieldName()); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes + .add(new StringAttributeValue(new JavaSymbolName("value"), "/{" + + identifierField.getFieldName().getSymbolName() + "}")); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "DELETE")))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "\");"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine(jsonEnabledTypeShortName + " " + + jsonBeanName + " = " + findMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("if (" + jsonBeanName + " == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return new " + + getShortName(RESPONSE_ENTITY) + "(headers, " + + getShortName(HTTP_STATUS) + ".NOT_FOUND);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine(removeMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine( + "return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".OK);"); + closeTry(bodyBuilder, getShortName(RESPONSE_ENTITY), + getShortName(HTTP_STATUS)); + + if (introduceLayerComponents) { + removeMethod.copyAdditionsTo(builder, governorTypeDetails); + findMethod.copyAdditionsTo(builder, governorTypeDetails); + } + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getJsonFindMethod( + final FinderMetadataDetails finderDetails, final String plural) { + if (finderDetails == null + || jsonMetadata.getToJsonArrayMethodName() == null) { + return null; + } + final JavaSymbolName finderMethodName = new JavaSymbolName("json" + + StringUtils.capitalize(finderDetails + .getFinderMethodMetadata().getMethodName() + .getSymbolName())); + if (governorHasMethodWithSameName(finderMethodName)) { + return null; + } + + final List parameterTypes = new ArrayList(); + final List parameterNames = new ArrayList(); + final StringBuilder methodParams = new StringBuilder(); + + for (final FieldMetadata field : finderDetails + .getFinderMethodParamFields()) { + final JavaSymbolName fieldName = field.getFieldName(); + final List annotations = new ArrayList(); + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue( + new JavaSymbolName("value"), StringUtils + .uncapitalize(fieldName.getSymbolName()))); + if (field.getFieldType().equals(JavaType.BOOLEAN_PRIMITIVE) + || field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)) { + attributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + } + final AnnotationMetadataBuilder requestParamAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, attributes); + annotations.add(requestParamAnnotation.build()); + if (field.getFieldType().equals(DATE) + || field.getFieldType().equals(CALENDAR)) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), + DATE_TIME_FORMAT); + if (annotation != null) { + annotations.add(annotation); + } + } + parameterNames.add(fieldName); + parameterTypes.add(new AnnotatedJavaType(field.getFieldType(), + annotations)); + + if (field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)) { + methodParams.append(field.getFieldName() + + " == null ? Boolean.FALSE : " + field.getFieldName() + + ", "); + } + else { + methodParams.append(field.getFieldName() + ", "); + } + } + + if (methodParams.length() > 0) { + methodParams.delete(methodParams.length() - 2, + methodParams.length()); + } + + final List newParamNames = new ArrayList(); + newParamNames.addAll(parameterNames); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("params"), "find=" + + finderDetails.getFinderMethodMetadata() + .getMethodName().getSymbolName() + .replaceFirst("find" + plural, ""))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + annotations.add(new AnnotationMetadataBuilder(RESPONSE_BODY)); + + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + final String responseEntityShortName = getShortName(RESPONSE_ENTITY); + final String httpStatusShortName = getShortName(HTTP_STATUS); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "; charset=utf-8\");"); + bodyBuilder.appendFormalLine("return new " + + responseEntityShortName + + "(" + + jsonEnabledTypeShortName + + "." + + jsonMetadata.getToJsonArrayMethodName().getSymbolName() + .toString() + + "(" + + jsonEnabledTypeShortName + + "." + + finderDetails.getFinderMethodMetadata().getMethodName() + .getSymbolName() + "(" + methodParams.toString() + + ").getResultList()), headers, " + httpStatusShortName + + ".OK);"); + closeTry(bodyBuilder, responseEntityShortName, httpStatusShortName); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, finderMethodName, RESPONSE_ENTITY_STRING, + parameterTypes, newParamNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getListJsonMethod( + final MemberTypeAdditions findAllMethod) { + if (StringUtils.isBlank(annotationValues.getListJsonMethod()) + || findAllMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getListJsonMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName toJsonArrayMethodName = jsonMetadata + .getToJsonArrayMethodName(); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + annotations.add(new AnnotationMetadataBuilder(RESPONSE_BODY)); + + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + final String responseEntityShortName = getShortName(RESPONSE_ENTITY); + final String httpStatusShortName = getShortName(HTTP_STATUS); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "; charset=utf-8\");"); + openTry(bodyBuilder); + final JavaType list = new JavaType(List.class.getName(), 0, + DataType.TYPE, null, Arrays.asList(jsonEnabledType)); + bodyBuilder.appendFormalLine(getShortName(list) + " result = " + + findAllMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("return new " + responseEntityShortName + + "(" + jsonEnabledTypeShortName + "." + + toJsonArrayMethodName.getSymbolName() + "(result), headers, " + + httpStatusShortName + ".OK);"); + closeTry(bodyBuilder, responseEntityShortName, httpStatusShortName); + + if (introduceLayerComponents) { + findAllMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private String getShortName(final JavaType type) { + return type.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + } + + private MethodMetadataBuilder getShowJsonMethod( + final FieldMetadata identifierField, + final MemberTypeAdditions findMethod) { + if (StringUtils.isBlank(annotationValues.getShowJsonMethod()) + || identifierField == null || findMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getShowJsonMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName toJsonMethodName = jsonMetadata + .getToJsonMethodName(); + + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue(new JavaSymbolName("value"), + identifierField.getFieldName().getSymbolName())); + final AnnotationMetadataBuilder pathVariableAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE, attributes); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(identifierField.getFieldType(), + pathVariableAnnotation.build())); + final List parameterNames = Arrays + .asList(new JavaSymbolName(identifierField.getFieldName() + .getSymbolName())); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes + .add(new StringAttributeValue(new JavaSymbolName("value"), "/{" + + identifierField.getFieldName().getSymbolName() + "}")); + requestMappingAttributes + .add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "GET")))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + + final List annotations = new ArrayList(); + annotations.add(requestMapping); + annotations.add(new AnnotationMetadataBuilder(RESPONSE_BODY)); + + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + final String responseEntityShortName = getShortName(RESPONSE_ENTITY); + final String httpStatusShortName = getShortName(HTTP_STATUS); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "; charset=utf-8\");"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine(jsonEnabledTypeShortName + " " + + jsonBeanName + " = " + findMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("if (" + jsonBeanName + " == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("return new " + responseEntityShortName + + "(headers, " + httpStatusShortName + ".NOT_FOUND);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("return new " + responseEntityShortName + + "(" + jsonBeanName + "." + + toJsonMethodName.getSymbolName() + "(), headers, " + + httpStatusShortName + ".OK);"); + closeTry(bodyBuilder, responseEntityShortName, httpStatusShortName); + + if (introduceLayerComponents) { + findMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getUpdateFromJsonArrayMethod( + final MemberTypeAdditions mergeMethod) { + if (StringUtils + .isBlank(annotationValues.getUpdateFromJsonArrayMethod()) + || mergeMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getUpdateFromJsonArrayMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName fromJsonArrayMethodName = jsonMetadata + .getFromJsonArrayMethodName(); + + final AnnotationMetadataBuilder requestBodyAnnotation = new AnnotationMetadataBuilder( + REQUEST_BODY); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(JavaType.STRING, + requestBodyAnnotation.build())); + final List parameterNames = Arrays + .asList(new JavaSymbolName("json")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), "/jsonArray")); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "PUT")))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final List params = new ArrayList(); + params.add(jsonEnabledType); + + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + final String responseEntityShortName = getShortName(RESPONSE_ENTITY); + final String httpStatusShortName = getShortName(HTTP_STATUS); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "\");"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine("for (" + jsonEnabledTypeShortName + " " + + jsonBeanName + ": " + jsonEnabledTypeShortName + "." + + fromJsonArrayMethodName.getSymbolName() + "(json)) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("if (" + mergeMethod.getMethodCall() + + " == null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".NOT_FOUND);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".OK);"); + closeTry(bodyBuilder, responseEntityShortName, httpStatusShortName); + + if (introduceLayerComponents) { + mergeMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getUpdateFromJsonMethod( + final FieldMetadata identifierField, + final MemberTypeAdditions mergeMethod) { + if (StringUtils.isBlank(annotationValues.getUpdateFromJsonMethod()) + || mergeMethod == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName( + annotationValues.getUpdateFromJsonMethod()); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName fromJsonMethodName = jsonMetadata + .getFromJsonMethodName(); + final JavaSymbolName identifierFieldName = identifierField.getFieldName(); + + final AnnotationMetadataBuilder requestBodyAnnotation = new AnnotationMetadataBuilder( + REQUEST_BODY); + + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue(new JavaSymbolName("value"), + identifierFieldName.getSymbolName())); + final AnnotationMetadataBuilder pathVariableAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE, attributes); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(JavaType.STRING, + requestBodyAnnotation.build()), + new AnnotatedJavaType(identifierField.getFieldType(), + pathVariableAnnotation.build())); + final List parameterNames = Arrays + .asList(new JavaSymbolName("json"), + identifierFieldName); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue(new JavaSymbolName("value"), + "/{" + identifierFieldName.getSymbolName() + "}" )); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "PUT")))); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("headers"), "Accept=application/json")); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final String httpHeadersShortName = getShortName(HTTP_HEADERS); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(httpHeadersShortName + " headers = new " + + httpHeadersShortName + "();"); + bodyBuilder.appendFormalLine("headers.add(\"Content-Type\", \"" + + CONTENT_TYPE + "\");"); + openTry(bodyBuilder); + bodyBuilder.appendFormalLine(jsonEnabledTypeShortName + " " + + jsonBeanName + " = " + jsonEnabledTypeShortName + "." + + fromJsonMethodName.getSymbolName() + "(json);"); + bodyBuilder.appendFormalLine(jsonBeanName + "." + + getMutatorMethod(identifierFieldName, jsonEnabledType).getMethodName() + + "(" + identifierFieldName.getSymbolName() + ");"); + bodyBuilder.appendFormalLine("if (" + mergeMethod.getMethodCall() + + " == null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".NOT_FOUND);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder + .appendFormalLine("return new ResponseEntity(headers, " + + getShortName(HTTP_STATUS) + ".OK);"); + closeTry(bodyBuilder, getShortName(RESPONSE_ENTITY), + getShortName(HTTP_STATUS)); + + if (introduceLayerComponents) { + mergeMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), PUBLIC, methodName, RESPONSE_ENTITY_STRING, + parameterTypes, parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadataProvider.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadataProvider.java new file mode 100644 index 000000000..298c688f4 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides Json functionality for {@link WebJsonMetadata}. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +public interface WebJsonMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadataProviderImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadataProviderImpl.java new file mode 100644 index 000000000..0f86847e6 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonMetadataProviderImpl.java @@ -0,0 +1,295 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import static org.springframework.roo.model.RooJavaType.ROO_WEB_JSON; +import static org.springframework.roo.model.RooJavaType.ROO_WEB_SCAFFOLD; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.json.JsonMetadata; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.addon.web.mvc.controller.details.FinderMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypePersistenceMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.WebMetadataService; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link WebJsonMetadataProvider}. + * + * @author Stefan Schmidt + * @since 1.1.3 + */ +@Component +@Service +public class WebJsonMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + WebJsonMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(WebJsonMetadataProviderImpl.class); + + private WebMetadataService webMetadataService; + + // Maps entities to the IDs of their WebJsonMetadata + private final Map managedEntityTypes = new HashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_WEB_JSON); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return WebJsonMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_WEB_JSON); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = WebJsonMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = WebJsonMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Controller_Json"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + final ClassOrInterfaceTypeDetails governorTypeDetails = getTypeLocationService() + .getTypeDetails(itdTypeDetails.getName()); + if (governorTypeDetails == null) { + return null; + } + + // Check whether a relevant layer component has appeared, changed, or + // disappeared + final String localMidForLayerManagedEntity = getWebJsonMidIfLayerComponent(governorTypeDetails); + if (StringUtils.isNotBlank(localMidForLayerManagedEntity)) { + return localMidForLayerManagedEntity; + } + + // Check whether the relevant MVC controller has appeared, changed, or + // disappeared + return getWebJsonMidIfMvcController(governorTypeDetails); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata, + final String itdFilename) { + + // We need to parse the annotation, which we expect to be present + final WebJsonAnnotationValues annotationValues = new WebJsonAnnotationValues( + governorPhysicalTypeMetadata); + if (!annotationValues.isAnnotationFound() + || annotationValues.getJsonObject() == null + || governorPhysicalTypeMetadata.getMemberHoldingTypeDetails() == null) { + return null; + } + + // Lookup the form backing object's metadata + final JavaType jsonObject = annotationValues.getJsonObject(); + final ClassOrInterfaceTypeDetails jsonTypeDetails = getTypeLocationService() + .getTypeDetails(jsonObject); + if (jsonTypeDetails == null) { + return null; + } + final LogicalPath jsonObjectPath = PhysicalTypeIdentifier + .getPath(jsonTypeDetails.getDeclaredByMetadataId()); + final JsonMetadata jsonMetadata = (JsonMetadata) getMetadataService() + .get(JsonMetadata.createIdentifier(jsonObject, jsonObjectPath)); + if (jsonMetadata == null) { + return null; + } + + final PhysicalTypeMetadata backingObjectPhysicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService() + .get(PhysicalTypeIdentifier.createIdentifier(jsonObject, + getTypeLocationService().getTypePath(jsonObject))); + Validate.notNull(backingObjectPhysicalTypeMetadata, + "Unable to obtain physical type metadata for type %s", + jsonObject.getFullyQualifiedTypeName()); + final MemberDetails formBackingObjectMemberDetails = getMemberDetails(backingObjectPhysicalTypeMetadata); + final MemberHoldingTypeDetails backingMemberHoldingTypeDetails = MemberFindingUtils + .getMostConcreteMemberHoldingTypeDetailsWithTag( + formBackingObjectMemberDetails, + CustomDataKeys.PERSISTENT_TYPE); + if (backingMemberHoldingTypeDetails == null) { + return null; + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + backingMemberHoldingTypeDetails.getDeclaredByMetadataId(), + metadataIdentificationString); + + final Set finderDetails = getWebMetadataService() + .getDynamicFinderMethodsAndFields(jsonObject, + formBackingObjectMemberDetails, + metadataIdentificationString); + if (finderDetails == null) { + return null; + } + final Map persistenceAdditions = getWebMetadataService() + .getCrudAdditions(jsonObject, metadataIdentificationString); + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataDetails = getWebMetadataService() + .getJavaTypePersistenceMetadataDetails(jsonObject, + getMemberDetails(jsonObject), + metadataIdentificationString); + final PluralMetadata pluralMetadata = (PluralMetadata) getMetadataService() + .get(PluralMetadata.createIdentifier(jsonObject, + getTypeLocationService().getTypePath(jsonObject))); + if (persistenceAdditions.isEmpty() + || javaTypePersistenceMetadataDetails == null + || pluralMetadata == null) { + return null; + } + + // Maintain a list of entities that are being tested + managedEntityTypes.put(jsonObject, metadataIdentificationString); + + return new WebJsonMetadata(metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, annotationValues, + persistenceAdditions, + javaTypePersistenceMetadataDetails.getIdentifierField(), + pluralMetadata.getPlural(), finderDetails, jsonMetadata, + introduceLayerComponents(governorPhysicalTypeMetadata)); + } + + public String getProvidesType() { + return WebJsonMetadata.getMetadataIdentiferType(); + } + + /** + * If the given type is a layer component (e.g. repository or service), + * returns the ID of the WebJsonMetadata for the first (!) domain type it + * manages, in case this is a new layer component that the JSON ITD needs to + * use. + * + * @param governorTypeDetails the type to check (required) + */ + private String getWebJsonMidIfLayerComponent( + final ClassOrInterfaceTypeDetails governorTypeDetails) { + for (final JavaType domainType : governorTypeDetails.getLayerEntities()) { + final String webJsonMetadataId = managedEntityTypes.get(domainType); + if (webJsonMetadataId != null) { + return webJsonMetadataId; + } + } + return null; + } + + /** + * If the given type is a web MVC controller, returns the ID of the + * WebJsonMetadata for its form backing type, to ensure that any required + * layer components are injected. This is a workaround to AspectJ not + * handling multiple ITDs introducing the same field (in our case the layer + * component) into one Java class. + * + * @param governorTypeDetails the type to check (required) + */ + private String getWebJsonMidIfMvcController( + final ClassOrInterfaceTypeDetails governorTypeDetails) { + final AnnotationMetadata controllerAnnotation = governorTypeDetails + .getAnnotation(ROO_WEB_SCAFFOLD); + if (controllerAnnotation != null) { + final JavaType formBackingType = (JavaType) controllerAnnotation + .getAttribute("formBackingObject").getValue(); + final String webJsonMetadataId = managedEntityTypes + .get(formBackingType); + if (webJsonMetadataId != null) { + /* + * We've been notified of a change to an MVC controller for + * whose backing object we produce WebJsonMetadata; refresh that + * MD to ensure our ITD does or does not introduce any required + * layer components, as appropriate. + */ + getMetadataService().get(webJsonMetadataId); + } + } + return null; + } + + /** + * Indicates whether the web JSON ITD should introduce any required layer + * components (services, repositories, etc.). This information is necessary + * for so long as AspectJ does not allow the same field to be introduced + * into a given Java class by more than one ITD. + * + * @param governor the governor, i.e. the controller (required) + * @return see above + */ + private boolean introduceLayerComponents(final PhysicalTypeMetadata governor) { + // If no MVC ITD is going to be created, we have to introduce any + // required layer components + return MemberFindingUtils.getAnnotationOfType(governor + .getMemberHoldingTypeDetails().getAnnotations(), + ROO_WEB_SCAFFOLD) == null; + } + + public WebMetadataService getWebMetadataService(){ + if(webMetadataService == null){ + // Get all Services implement WebMetadataService interface + try { + ServiceReference[] references = context.getAllServiceReferences(WebMetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (WebMetadataService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load WebMetadataService on WebJsonMetadataProviderImpl."); + return null; + } + }else{ + return webMetadataService; + } + + } +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonOperations.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonOperations.java new file mode 100644 index 000000000..345463f9b --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonOperations.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Provides operations for Web MVC Json functionality. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface WebJsonOperations { + + void annotateAll(JavaPackage javaPackage); + + void annotateType(JavaType type, JavaType jsonType); + + boolean isWebJsonCommandAvailable(); + + boolean isWebJsonInstallationPossible(); + + void setup(); +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonOperationsImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonOperationsImpl.java new file mode 100644 index 000000000..c32368197 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/json/WebJsonOperationsImpl.java @@ -0,0 +1,245 @@ +package org.springframework.roo.addon.web.mvc.controller.json; + +import static org.springframework.roo.model.SpringJavaType.CHARACTER_ENCODING_FILTER; +import static org.springframework.roo.model.SpringJavaType.CONTEXT_LOADER_LISTENER; +import static org.springframework.roo.model.SpringJavaType.DISPATCHER_SERVLET; +import static org.springframework.roo.model.SpringJavaType.HIDDEN_HTTP_METHOD_FILTER; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.plural.PluralMetadata; +import org.springframework.roo.addon.web.mvc.controller.WebMvcOperations; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.model.SpringJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.ProjectType; +import org.springframework.roo.support.util.WebXmlUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Implementation of {@link WebJsonOperations}. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class WebJsonOperationsImpl implements WebJsonOperations { + + @Reference private FileManager fileManager; + @Reference private MetadataService metadataService; + @Reference private WebMvcOperations mvcOperations; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + public void annotateAll(JavaPackage javaPackage) { + if (javaPackage == null) { + javaPackage = projectOperations + .getTopLevelPackage(projectOperations + .getFocusedModuleName()); + } + for (final ClassOrInterfaceTypeDetails cod : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_JSON)) { + if (Modifier.isAbstract(cod.getModifier())) { + continue; + } + final JavaType jsonType = cod.getName(); + JavaType mvcType = null; + for (final ClassOrInterfaceTypeDetails mvcCod : typeLocationService + .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_WEB_SCAFFOLD)) { + // We know this physical type exists given type location service + // just found it. + final PhysicalTypeMetadata mvcMd = (PhysicalTypeMetadata) metadataService + .get(mvcCod.getDeclaredByMetadataId()); + final WebScaffoldAnnotationValues webScaffoldAnnotationValues = new WebScaffoldAnnotationValues( + mvcMd); + if (webScaffoldAnnotationValues.isAnnotationFound() + && webScaffoldAnnotationValues.getFormBackingObject() + .equals(jsonType)) { + mvcType = mvcCod.getName(); + break; + } + } + if (mvcType == null) { + createNewType( + new JavaType(javaPackage.getFullyQualifiedPackageName() + + "." + jsonType.getSimpleTypeName() + + "Controller"), jsonType); + } + else { + appendToExistingType(mvcType, jsonType); + } + } + } + + public void annotateType(final JavaType type, final JavaType jsonEntity) { + Validate.notNull(type, "Target type required"); + Validate.notNull(jsonEntity, "Json entity required"); + final String id = typeLocationService.getPhysicalTypeIdentifier(type); + if (id == null) { + createNewType(type, jsonEntity); + } + else { + appendToExistingType(type, jsonEntity); + } + } + + private void appendToExistingType(final JavaType type, + final JavaType jsonEntity) { + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(type); + if (cid == null) { + throw new IllegalArgumentException("Cannot locate source for '" + + type.getFullyQualifiedTypeName() + "'"); + } + + if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), + RooJavaType.ROO_WEB_JSON) != null) { + return; + } + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + cid); + cidBuilder.addAnnotation(getAnnotation(jsonEntity)); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + private void createNewType(final JavaType type, final JavaType jsonEntity) { + final PluralMetadata pluralMetadata = (PluralMetadata) metadataService + .get(PluralMetadata.createIdentifier(jsonEntity, + typeLocationService.getTypePath(jsonEntity))); + if (pluralMetadata == null) { + return; + } + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(type, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, type, + PhysicalTypeCategory.CLASS); + cidBuilder.addAnnotation(getAnnotation(jsonEntity)); + cidBuilder.addAnnotation(new AnnotationMetadataBuilder( + SpringJavaType.CONTROLLER)); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + SpringJavaType.REQUEST_MAPPING); + requestMapping.addAttribute(new StringAttributeValue( + new JavaSymbolName("value"), "/" + + pluralMetadata.getPlural().toLowerCase())); + cidBuilder.addAnnotation(requestMapping); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + private AnnotationMetadataBuilder getAnnotation(final JavaType type) { + // Create annotation @RooWebJson(jsonObject = MyObject.class) + final List> rooJsonAttributes = new ArrayList>(); + rooJsonAttributes.add(new ClassAttributeValue(new JavaSymbolName( + "jsonObject"), type)); + return new AnnotationMetadataBuilder(RooJavaType.ROO_WEB_JSON, + rooJsonAttributes); + } + + public boolean isWebJsonCommandAvailable() { + return projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.MVC) + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + public boolean isWebJsonInstallationPossible() { + return !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.MVC) + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + public void setup() { + mvcOperations.installMinimalWebArtifacts(); + + // Verify that the web.xml already exists + final String webXmlPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/web.xml"); + Validate.isTrue(fileManager.exists(webXmlPath), "'%s' does not exist", + webXmlPath); + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(webXmlPath)); + + WebXmlUtils.addContextParam(new WebXmlUtils.WebXmlParam( + "contextConfigLocation", + "classpath*:META-INF/spring/applicationContext*.xml"), + document, null); + WebXmlUtils.addFilter(WebMvcOperations.CHARACTER_ENCODING_FILTER_NAME, + CHARACTER_ENCODING_FILTER.getFullyQualifiedTypeName(), "/*", + document, null, + new WebXmlUtils.WebXmlParam("encoding", "UTF-8"), + new WebXmlUtils.WebXmlParam("forceEncoding", "true")); + WebXmlUtils.addFilter(WebMvcOperations.HTTP_METHOD_FILTER_NAME, + HIDDEN_HTTP_METHOD_FILTER.getFullyQualifiedTypeName(), "/*", + document, null); + WebXmlUtils + .addListener( + CONTEXT_LOADER_LISTENER.getFullyQualifiedTypeName(), + document, + "Creates the Spring Container shared by all Servlets and Filters"); + WebXmlUtils.addServlet(projectOperations.getFocusedProjectName(), + DISPATCHER_SERVLET.getFullyQualifiedTypeName(), "/", 1, + document, "Handles Spring requests", + new WebXmlUtils.WebXmlParam("contextConfigLocation", + "WEB-INF/spring/webmvc-config.xml")); + + fileManager.createOrUpdateTextFileIfRequired(webXmlPath, + XmlUtils.nodeToString(document), false); + + updateConfiguration(); + } + + private void updateConfiguration() { + final Element configuration = XmlUtils.getConfiguration(getClass()); + + final List dependencies = new ArrayList(); + final List springDependencies = XmlUtils.findElements( + "/configuration/springWebJson/dependencies/dependency", + configuration); + for (final Element dependencyElement : springDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + projectOperations.addDependencies( + projectOperations.getFocusedModuleName(), dependencies); + + projectOperations.updateProjectType( + projectOperations.getFocusedModuleName(), ProjectType.WAR); + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/RooWebScaffold.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/RooWebScaffold.java new file mode 100644 index 000000000..17e60a09f --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/RooWebScaffold.java @@ -0,0 +1,118 @@ +package org.springframework.roo.addon.web.mvc.controller.scaffold; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a type that requires ROO controller support. + *

    + * This annotation will cause ROO to produce code that would typically appear in + * MVC controllers. Importantly, such code does NOT depend on any singletons and + * is intended to safely serialise. In the current release this code will be + * emitted to an ITD. + *

    + * The following functionality will be introduced in the ITD: + *

      + *
    • The Spring MVC org.springframework.stereotype.Controller annotation will + * be declared on the controller type if not exists
    • + *
    • Setting this annotation will also generate JSP view pages corresponding + * to the functionalities included
    • + *
    • The {@link RooWebScaffold#formBackingObject()} property defines the + * persistent type which is exposed through this controller
    • + *
    + *

    + * There are two cases in which ROO will not emit one or more of the above + * artifacts: + *

      + *
    • The user provides the equivalent methods on the controller object itself
    • + *
    • A specific {@link RooWebScaffold} annotation value indicates the desired + * output type should not be emitted (all emit by default)
    • + *
    + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface RooWebScaffold { + + /** + * Creates a create() method which allows the creation of a new entity. + * + * @return indicates if the create() method should be provided (defaults to + * "true"; optional) + */ + boolean create() default true; + + /** + * Creates a delete() method which deletes an entity for a given id. + * + * @return indicates if the delete() method should be provided (defaults to + * "true"; optional) + */ + boolean delete() default true; + + /** + * This flag is not used any more as of Roo 1.2.0. Please annotate + * controller types with + * {@link org.springframework.roo.addon.web.mvc.controller.finder.RooWebFinder} + * instead. (Was: Will scan the formBackingObjects for installed finder + * methods and expose them when configured.) + * + * @return indicates if the finders methods should be provided (defaults to + * "true"; optional) + */ + @Deprecated + boolean exposeFinders() default true; + + /** + * This flag is not used any more as of Roo 1.2.0. Please annotate + * controller types with + * {@link org.springframework.roo.addon.web.mvc.controller.json.RooWebJson} + * instead. (Was: Will scan the formBackingObjects for + * org.springframework.roo.addon.json.RooJson annotation and expose json + * when configured.) + * + * @return indicates if the json methods should be provided (defaults to + * "true"; optional) + */ + @Deprecated + boolean exposeJson() default true; + + /** + * Every controller is responsible for a single form backing object. The + * form backing object defined here class will be exposed in a RESTful way. + */ + Class formBackingObject(); + + /** + * All view-related artifacts for a specific controller are stored in a + * sub-directory under WEB-INF/views/path. The path parameter + * defines the name of this sub-directory or path. This path is also used to + * define the restful resource in the URL to which the controller is mapped. + * + * @return The view path. + */ + String path(); + + /** + * Indicate if Roo should create data population methods used for model + * attributes required for the Spring MVC forms. If this flag is set to + * false the developer is expected to manage the population of the model + * attributes by himself. + * + * @return indicates if the populateXXX() methods should be provided + * (defaults to "true"; optional) + */ + boolean populateMethods() default true; + + /** + * Creates an update() method which allows alteration of an existing entity. + * + * @return indicates if the update() method should be provided (defaults to + * "true"; optional) + */ + boolean update() default true; +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldAnnotationValues.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldAnnotationValues.java new file mode 100644 index 000000000..9d93df0e4 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldAnnotationValues.java @@ -0,0 +1,88 @@ +package org.springframework.roo.addon.web.mvc.controller.scaffold; + +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.annotations.populator.AbstractAnnotationValues; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulate; +import org.springframework.roo.classpath.details.annotations.populator.AutoPopulationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.RooJavaType; + +/** + * Represents a parsed {@link RooWebScaffold} annotation. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +public class WebScaffoldAnnotationValues extends AbstractAnnotationValues { + + @AutoPopulate boolean create = true; + @AutoPopulate boolean delete = true; + @AutoPopulate boolean exposeFinders = true; + @AutoPopulate JavaType formBackingObject; + @AutoPopulate String path; + @AutoPopulate boolean populateMethods = true; + @AutoPopulate boolean registerConverters = true; + @AutoPopulate boolean update = true; + + public WebScaffoldAnnotationValues( + final ClassOrInterfaceTypeDetails governorPhysicalTypeDetails) { + super(governorPhysicalTypeDetails, RooJavaType.ROO_WEB_SCAFFOLD); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + /** + * Constructor + * + * @param governorPhysicalTypeMetadata + */ + public WebScaffoldAnnotationValues( + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(governorPhysicalTypeMetadata, RooJavaType.ROO_WEB_SCAFFOLD); + AutoPopulationUtils.populate(this, annotationMetadata); + } + + public JavaType getFormBackingObject() { + return formBackingObject; + } + + public String getPath() { + return path; + } + + public boolean isCreate() { + return create; + } + + public boolean isDelete() { + return delete; + } + + public boolean isExposeFinders() { + return exposeFinders; + } + + public boolean isPopulateMethods() { + return populateMethods; + } + + public boolean isRegisterConverters() { + return registerConverters; + } + + public boolean isUpdate() { + return update; + } + + @Override + public String toString() { + // For debugging + return "WebScaffoldAnnotationValues [" + "create=" + create + + ", delete=" + delete + ", exposeFinders=" + exposeFinders + + ", populateMethods=" + populateMethods + + ", registerConverters=" + registerConverters + ", update=" + + update + ", formBackingObject=" + formBackingObject + + ", path=" + path + "]"; + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadata.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadata.java new file mode 100644 index 000000000..cf55735b9 --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadata.java @@ -0,0 +1,988 @@ +package org.springframework.roo.addon.web.mvc.controller.scaffold; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_SORTED_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MERGE_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD; +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JavaType.VOID_PRIMITIVE; +import static org.springframework.roo.model.JdkJavaType.ARRAYS; +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.UNSUPPORTED_ENCODING_EXCEPTION; +import static org.springframework.roo.model.Jsr303JavaType.VALID; +import static org.springframework.roo.model.SpringJavaType.AUTOWIRED; +import static org.springframework.roo.model.SpringJavaType.BINDING_RESULT; +import static org.springframework.roo.model.SpringJavaType.CONVERSION_SERVICE; +import static org.springframework.roo.model.SpringJavaType.LOCALE_CONTEXT_HOLDER; +import static org.springframework.roo.model.SpringJavaType.MODEL; +import static org.springframework.roo.model.SpringJavaType.PATH_VARIABLE; +import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING; +import static org.springframework.roo.model.SpringJavaType.REQUEST_METHOD; +import static org.springframework.roo.model.SpringJavaType.REQUEST_PARAM; +import static org.springframework.roo.model.SpringJavaType.URI_UTILS; +import static org.springframework.roo.model.SpringJavaType.WEB_UTILS; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.web.mvc.controller.details.DateTimeFormatDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypePersistenceMetadataDetails; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Metadata for {@link RooWebScaffold}. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.0 + */ +public class WebScaffoldMetadata extends + AbstractItdTypeDetailsProvidingMetadataItem { + + private static final JavaSymbolName CS_FIELD = new JavaSymbolName( + "conversionService"); + private static final JavaType HTTP_SERVLET_REQUEST = new JavaType( + "javax.servlet.http.HttpServletRequest"); + private static final StringAttributeValue PRODUCES_HTML = new StringAttributeValue( + new JavaSymbolName("produces"), "text/html"); + private static final String PROVIDES_TYPE_STRING = WebScaffoldMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + private WebScaffoldAnnotationValues annotationValues; + private boolean compositePk; + private String controllerPath; + private Map dateTypes; + private String entityName; + private JavaType formBackingType; + private JavaTypeMetadataDetails javaTypeMetadataHolder; + private TypeLocationService typeLocationService; + + /** + * Constructor + * + * @param identifier + * @param aspectName + * @param governorPhysicalType + * @param annotationValues + * @param idField + * @param specialDomainTypes + * @param dependentTypes + * @param dateTypes + * @param crudAdditions + * @param editableFieldTypes + * @param typeLocationService + */ + public WebScaffoldMetadata( + final String identifier, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalType, + final WebScaffoldAnnotationValues annotationValues, + final FieldMetadata idField, + final SortedMap specialDomainTypes, + final List dependentTypes, + final Map dateTypes, + final Map crudAdditions, + final Collection editableFieldTypes, + TypeLocationService typeLocationService) { + super(identifier, aspectName, governorPhysicalType); + Validate.isTrue(isValid(identifier), + "Metadata identification string '%s' is invalid", identifier); + Validate.notNull(annotationValues, "Annotation values required"); + Validate.notNull(specialDomainTypes, + "Special domain types map required"); + Validate.notNull(dependentTypes, "Dependent types list required"); + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + if (!isValid()) { + return; + } + + this.annotationValues = annotationValues; + this.typeLocationService = typeLocationService; + controllerPath = annotationValues.getPath(); + this.dateTypes = dateTypes; + formBackingType = annotationValues.getFormBackingObject(); + entityName = JavaSymbolName.getReservedWordSafeName(formBackingType) + .getSymbolName(); + + javaTypeMetadataHolder = specialDomainTypes.get(formBackingType); + + Validate.notNull(javaTypeMetadataHolder, + "Metadata holder required for form backing type %s", + formBackingType); + + if (javaTypeMetadataHolder.getPersistenceDetails() != null + && !javaTypeMetadataHolder.getPersistenceDetails() + .getRooIdentifierFields().isEmpty()) { + compositePk = true; + builder.addField(getField(CS_FIELD, CONVERSION_SERVICE)); + builder.addConstructor(getConstructor()); + } + + // "create" methods + final MemberTypeAdditions persistMethod = crudAdditions + .get(PERSIST_METHOD); + if (annotationValues.isCreate() && persistMethod != null) { + builder.addMethod(getCreateMethod(persistMethod)); + builder.addMethod(getCreateFormMethod(dependentTypes)); + persistMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + final MemberTypeAdditions countAllMethod = crudAdditions + .get(COUNT_ALL_METHOD); + final MemberTypeAdditions findMethod = crudAdditions.get(FIND_METHOD); + final MemberTypeAdditions findAllMethod = crudAdditions + .get(FIND_ALL_METHOD); + final MemberTypeAdditions findEntriesMethod = crudAdditions + .get(FIND_ENTRIES_METHOD); + final MemberTypeAdditions findAllSortedMethod = crudAdditions + .get(FIND_ALL_SORTED_METHOD); + final MemberTypeAdditions findEntriesSortedMethod = crudAdditions + .get(FIND_ENTRIES_SORTED_METHOD); + + // "show" method + if (findMethod != null) { + builder.addMethod(getShowMethod(idField, findMethod)); + findMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + // sorted "list" method + if (countAllMethod != null && findAllSortedMethod != null + && findEntriesSortedMethod != null) { + builder.addMethod(getListMethod(findAllSortedMethod, + countAllMethod, findEntriesSortedMethod)); + countAllMethod.copyAdditionsTo(builder, governorTypeDetails); + findAllSortedMethod.copyAdditionsTo(builder, governorTypeDetails); + findEntriesSortedMethod.copyAdditionsTo(builder, + governorTypeDetails); + } + // or "list" method + else if (countAllMethod != null && findAllMethod != null + && findEntriesMethod != null) { + builder.addMethod(getListMethod(findAllMethod, countAllMethod, + findEntriesMethod)); + countAllMethod.copyAdditionsTo(builder, governorTypeDetails); + findAllMethod.copyAdditionsTo(builder, governorTypeDetails); + findEntriesMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + // "update" methods + final MemberTypeAdditions updateMethod = crudAdditions + .get(MERGE_METHOD); + if (annotationValues.isUpdate() && updateMethod != null + && findMethod != null) { + builder.addMethod(getUpdateMethod(updateMethod)); + builder.addMethod(getUpdateFormMethod(idField, findMethod)); + updateMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + // "delete" method + final MemberTypeAdditions deleteMethod = crudAdditions + .get(REMOVE_METHOD); + if (annotationValues.isDelete() && deleteMethod != null + && findMethod != null) { + builder.addMethod(getDeleteMethod(idField, deleteMethod, findMethod)); + deleteMethod.copyAdditionsTo(builder, governorTypeDetails); + } + + if (!dateTypes.isEmpty()) { + builder.addMethod(getDateTimeFormatHelperMethod()); + } + + if (annotationValues.isCreate() || annotationValues.isUpdate()) { + builder.addMethod(getPopulateEditFormMethod(formBackingType, + specialDomainTypes.values(), editableFieldTypes)); + builder.addMethod(getEncodeUrlPathSegmentMethod()); + } + + itdTypeDetails = builder.build(); + } + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public WebScaffoldAnnotationValues getAnnotationValues() { + return annotationValues; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("destinationType", destination); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } + + private ConstructorMetadataBuilder getConstructor() { + final ConstructorMetadata constructor = governorTypeDetails + .getDeclaredConstructor(Arrays.asList(CONVERSION_SERVICE)); + if (constructor != null) { + return null; + } + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("super();"); + bodyBuilder.appendFormalLine("this." + CS_FIELD + " = " + CS_FIELD + + ";"); + + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + getId()); + constructorBuilder.addAnnotation(new AnnotationMetadataBuilder( + AUTOWIRED)); + constructorBuilder.addParameterType(AnnotatedJavaType + .convertFromJavaType(CONVERSION_SERVICE)); + constructorBuilder.addParameterName(CS_FIELD); + constructorBuilder.setModifier(Modifier.PUBLIC); + constructorBuilder.setBodyBuilder(bodyBuilder); + return constructorBuilder; + } + + private MethodMetadataBuilder getCreateFormMethod( + final List dependentTypes) { + + // Getting entity fields names + ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(formBackingType); + + final JavaSymbolName methodName = new JavaSymbolName("createForm"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + List fieldNamesList = getEntityFields(dependentTypes, + cid); + + final List parameterTypes = Arrays.asList(MODEL); + final List parameterNames = Arrays + .asList(new JavaSymbolName("uiModel")); + + final List annotations = new ArrayList(); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("params"), "form")); + requestMappingAttributes.add(PRODUCES_HTML); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("populateEditForm(uiModel, new " + + getShortName(formBackingType) + "());"); + boolean listAdded = false; + int fieldPosition = 0; + for (final JavaTypeMetadataDetails dependentType : dependentTypes) { + MemberTypeAdditions countMethod = dependentType.getPersistenceDetails(). + getCountMethod(); + if (countMethod == null) { + fieldPosition++; + continue; + } + if (!listAdded) { + final JavaType stringArrayType = new JavaType( + STRING.getFullyQualifiedTypeName(), 1, DataType.TYPE, + null, null); + + final JavaType listType = new JavaType( + LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(stringArrayType)); + + final JavaType arrayListType = new JavaType( + ARRAY_LIST.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, Arrays.asList(stringArrayType)); + bodyBuilder.appendFormalLine(getShortName(listType) + + " dependencies = new " + getShortName(arrayListType) + + "();"); + listAdded = true; + } + builder.getImportRegistrationResolver().addImport( + dependentType.getJavaType()); + bodyBuilder.appendFormalLine("if (" + + countMethod.getMethodCall() + " == 0) {"); + bodyBuilder.indent(); + + countMethod.copyAdditionsTo(builder, governorTypeDetails); + + // Adding string array which has the fieldName at position 0 and the + // path at position 1 + String dependentTypeName = fieldNamesList.get(fieldPosition) + .getFieldName().getSymbolName(); + bodyBuilder.appendFormalLine("dependencies.add(new String[] { \"" + + dependentTypeName + "\", \"" + + dependentType.getControllerPath() + "\" });"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + + fieldPosition++; + } + if (listAdded) { + bodyBuilder + .appendFormalLine("uiModel.addAttribute(\"dependencies\", dependencies);"); + } + bodyBuilder.appendFormalLine("return \"" + controllerPath + + "/create\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + /** + * This method returns all fields in entity that match with dependentTypes + * + * @param dependentTypes + * @param cid + */ + private List getEntityFields( + final List dependentTypes, + ClassOrInterfaceTypeDetails cid) { + List fieldsNamesList = new ArrayList(); + // Getting all declared fields in entity + List fieldList = cid.getDeclaredFields(); + Iterator it = fieldList.iterator(); + while (it.hasNext()) { + FieldMetadata currentField = it.next(); + if (typeLocationService.isInProject(currentField.getFieldType())) { + fieldsNamesList.add(currentField); + } + } + + return fieldsNamesList; + } + + private MethodMetadataBuilder getCreateMethod( + final MemberTypeAdditions persistMethod) { + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = javaTypeMetadataHolder + .getPersistenceDetails(); + if (javaTypePersistenceMetadataHolder == null + || javaTypePersistenceMetadataHolder + .getIdentifierAccessorMethod() == null) { + return null; + } + + final JavaSymbolName methodName = new JavaSymbolName("create"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final AnnotationMetadataBuilder validAnnotation = new AnnotationMetadataBuilder( + VALID); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(formBackingType, validAnnotation + .build()), new AnnotatedJavaType(BINDING_RESULT), + new AnnotatedJavaType(MODEL), new AnnotatedJavaType( + HTTP_SERVLET_REQUEST)); + final List parameterNames = Arrays.asList( + new JavaSymbolName(entityName), new JavaSymbolName( + "bindingResult"), new JavaSymbolName("uiModel"), + new JavaSymbolName("httpServletRequest")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "POST")))); + requestMappingAttributes.add(PRODUCES_HTML); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (bindingResult.hasErrors()) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("populateEditForm(uiModel, " + entityName + + ");"); + bodyBuilder.appendFormalLine("return \"" + controllerPath + + "/create\";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("uiModel.asMap().clear();"); + bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("return \"redirect:/" + + controllerPath + + "/\" + encodeUrlPathSegment(" + + (compositePk ? "conversionService.convert(" : "") + + entityName + + "." + + javaTypePersistenceMetadataHolder + .getIdentifierAccessorMethod().getMethodName() + "()" + + (compositePk ? ", String.class)" : ".toString()") + + ", httpServletRequest);"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getDateTimeFormatHelperMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + "addDateTimeFormatPatterns"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List parameterTypes = Arrays.asList(MODEL); + final List parameterNames = Arrays + .asList(new JavaSymbolName("uiModel")); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + for (final Entry javaSymbolNameDateTimeFormatDetailsEntry : dateTypes + .entrySet()) { + String pattern; + if (javaSymbolNameDateTimeFormatDetailsEntry.getValue().pattern != null) { + pattern = "\"" + + javaSymbolNameDateTimeFormatDetailsEntry.getValue().pattern + + "\""; + } else { + final JavaType dateTimeFormat = new JavaType( + "org.joda.time.format.DateTimeFormat"); + pattern = getShortName(dateTimeFormat) + + ".patternForStyle(\"" + + javaSymbolNameDateTimeFormatDetailsEntry.getValue().style + + "\", " + getShortName(LOCALE_CONTEXT_HOLDER) + + ".getLocale())"; + } + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + entityName + + "_" + + javaSymbolNameDateTimeFormatDetailsEntry.getKey() + .getSymbolName().toLowerCase() + "_date_format\", " + + pattern + ");"); + } + + return new MethodMetadataBuilder(getId(), 0, methodName, + VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + private MethodMetadataBuilder getDeleteMethod(final FieldMetadata idField, + final MemberTypeAdditions deleteMethodAdditions, + final MemberTypeAdditions findMethod) { + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = javaTypeMetadataHolder + .getPersistenceDetails(); + if (javaTypePersistenceMetadataHolder == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName("delete"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final JavaSymbolName idFieldName = idField.getFieldName(); + String fieldName = entityName; + String deleteMethodCall = deleteMethodAdditions.getMethodCall(); + if (idFieldName.getSymbolName().equals(entityName)) { + fieldName += "_"; + deleteMethodCall = fieldName + "." + + deleteMethodAdditions.getMethodName() + "()"; + } + + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue(new JavaSymbolName("value"), + idFieldName.getSymbolName())); + final AnnotationMetadataBuilder pathVariableAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE, attributes); + + final List> firstResultAttributes = new ArrayList>(); + firstResultAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "page")); + firstResultAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder firstResultAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, firstResultAttributes); + + final List> maxResultsAttributes = new ArrayList>(); + maxResultsAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "size")); + maxResultsAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder maxResultAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, maxResultsAttributes); + + final List parameterTypes = Arrays.asList( + new AnnotatedJavaType(javaTypePersistenceMetadataHolder + .getIdentifierType(), pathVariableAnnotation.build()), + new AnnotatedJavaType(new JavaType(Integer.class.getName()), + firstResultAnnotation.build()), new AnnotatedJavaType( + new JavaType(Integer.class.getName()), + maxResultAnnotation.build()), new AnnotatedJavaType( + MODEL)); + final List parameterNames = Arrays.asList(idFieldName, + new JavaSymbolName("page"), new JavaSymbolName("size"), + new JavaSymbolName("uiModel")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), "/{" + idFieldName.getSymbolName() + + "}")); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "DELETE")))); + requestMappingAttributes.add(PRODUCES_HTML); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine(getShortName(formBackingType) + " " + + fieldName + " = " + findMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine(deleteMethodCall + ";"); + bodyBuilder.appendFormalLine("uiModel.asMap().clear();"); + bodyBuilder + .appendFormalLine("uiModel.addAttribute(\"page\", (page == null) ? \"1\" : page.toString());"); + bodyBuilder + .appendFormalLine("uiModel.addAttribute(\"size\", (size == null) ? \"10\" : size.toString());"); + bodyBuilder.appendFormalLine("return \"redirect:/" + controllerPath + + "\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getEncodeUrlPathSegmentMethod() { + final JavaSymbolName methodName = new JavaSymbolName( + "encodeUrlPathSegment"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List parameterTypes = Arrays.asList(STRING, + HTTP_SERVLET_REQUEST); + final List parameterNames = Arrays.asList( + new JavaSymbolName("pathSegment"), new JavaSymbolName( + "httpServletRequest")); + + builder.getImportRegistrationResolver().addImport( + UNSUPPORTED_ENCODING_EXCEPTION); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder + .appendFormalLine("String enc = httpServletRequest.getCharacterEncoding();"); + bodyBuilder.appendFormalLine("if (enc == null) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("enc = " + getShortName(WEB_UTILS) + + ".DEFAULT_CHARACTER_ENCODING;"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("try {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("pathSegment = " + getShortName(URI_UTILS) + + ".encodePathSegment(pathSegment, enc);"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} catch (" + + getShortName(UNSUPPORTED_ENCODING_EXCEPTION) + " uee) {}"); + bodyBuilder.appendFormalLine("return pathSegment;"); + + return new MethodMetadataBuilder(getId(), 0, methodName, STRING, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + /** + * Returns the metadata for the "list" method that this ITD introduces into + * the controller. + * + * @param findAllAdditions + * @param countAllAdditions + * @param findEntriesAdditions + * @return null if no such method is to be introduced + */ + private MethodMetadataBuilder getListMethod( + final MemberTypeAdditions findAllAdditions, + final MemberTypeAdditions countAllAdditions, + final MemberTypeAdditions findEntriesAdditions) { + final JavaSymbolName methodName = new JavaSymbolName("list"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List> firstResultAttributes = new ArrayList>(); + firstResultAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "page")); + firstResultAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder firstResultAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, firstResultAttributes); + + final List> maxResultsAttributes = new ArrayList>(); + maxResultsAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "size")); + maxResultsAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder maxResultAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, maxResultsAttributes); + + final List> sortFieldNameAttributes = new ArrayList>(); + sortFieldNameAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), "sortFieldName")); + sortFieldNameAttributes.add(new BooleanAttributeValue( + new JavaSymbolName("required"), false)); + final AnnotationMetadataBuilder sortFieldNameAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, sortFieldNameAttributes); + + final List> sortOrderAttributes = new ArrayList>(); + sortOrderAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "sortOrder")); + sortOrderAttributes.add(new BooleanAttributeValue(new JavaSymbolName( + "required"), false)); + final AnnotationMetadataBuilder sortOrderAnnotation = new AnnotationMetadataBuilder( + REQUEST_PARAM, sortOrderAttributes); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(INT_OBJECT, firstResultAnnotation + .build()), + new AnnotatedJavaType(INT_OBJECT, maxResultAnnotation + .build()), + new AnnotatedJavaType(STRING, sortFieldNameAnnotation + .build()), + new AnnotatedJavaType(STRING, sortOrderAnnotation + .build()), new AnnotatedJavaType(MODEL)); + final List parameterNames = Arrays.asList( + new JavaSymbolName("page"), new JavaSymbolName("size"), + new JavaSymbolName("sortFieldName"), new JavaSymbolName( + "sortOrder"), new JavaSymbolName("uiModel")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(PRODUCES_HTML); + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(REQUEST_MAPPING, + requestMappingAttributes)); + + final String plural = javaTypeMetadataHolder.getPlural().toLowerCase(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (page != null || size != null) {"); + bodyBuilder.indent(); + bodyBuilder + .appendFormalLine("int sizeNo = size == null ? 10 : size.intValue();"); + bodyBuilder + .appendFormalLine("final int firstResult = page == null ? 0 : (page.intValue() - 1) * sizeNo;"); + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + plural + + "\", " + findEntriesAdditions.getMethodCall() + ");"); + bodyBuilder.appendFormalLine("float nrOfPages = (float) " + + countAllAdditions.getMethodCall() + " / sizeNo;"); + bodyBuilder + .appendFormalLine("uiModel.addAttribute(\"maxPages\", (int) ((nrOfPages > (int) nrOfPages || nrOfPages == 0.0) ? nrOfPages + 1 : nrOfPages));"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("} else {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + plural + + "\", " + findAllAdditions.getMethodCall() + ");"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + if (!dateTypes.isEmpty()) { + bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);"); + } + bodyBuilder.appendFormalLine("return \"" + controllerPath + "/list\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadata getPopulateEditFormMethod(final JavaType entity, + final Collection specialDomainTypes, + final Collection editableFieldTypes) { + final JavaSymbolName methodName = new JavaSymbolName("populateEditForm"); + final JavaType[] parameterTypes = { MODEL, entity }; + final List parameterNames = Arrays.asList( + new JavaSymbolName("uiModel"), new JavaSymbolName(entityName)); + if (governorHasMethod(methodName, parameterTypes)) { + return null; + } + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + entityName + + "\", " + entityName + ");"); + if (!dateTypes.isEmpty()) { + bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);"); + } + if (annotationValues.isPopulateMethods()) { + for (final JavaTypeMetadataDetails domainType : specialDomainTypes) { + if (editableFieldTypes.contains(domainType.getJavaType())) { + final JavaTypePersistenceMetadataDetails persistenceDetails = domainType + .getPersistenceDetails(); + final String modelAttribute = domainType.getPlural() + .toLowerCase(); + if (persistenceDetails != null + && persistenceDetails.getFindAllMethod() != null) { + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + modelAttribute + + "\", " + + persistenceDetails.getFindAllMethod() + .getMethodCall() + ");"); + persistenceDetails.getFindAllMethod().copyAdditionsTo( + builder, governorTypeDetails); + } else if (domainType.isEnumType()) { + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + modelAttribute + "\", " + + getShortName(ARRAYS) + ".asList(" + + getShortName(domainType.getJavaType()) + + ".values())" + ");"); + } + } + } + } + return new MethodMetadataBuilder(getId(), 0, methodName, + VOID_PRIMITIVE, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder).build(); + } + + private String getShortName(final JavaType type) { + return type.getNameIncludingTypeParameters(false, + builder.getImportRegistrationResolver()); + } + + private MethodMetadataBuilder getShowMethod(final FieldMetadata idField, + final MemberTypeAdditions findMethod) { + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = javaTypeMetadataHolder + .getPersistenceDetails(); + if (javaTypePersistenceMetadataHolder == null) { + return null; + } + + final JavaSymbolName methodName = new JavaSymbolName("show"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final List> attributes = new ArrayList>(); + final String idFieldName = idField.getFieldName().getSymbolName(); + attributes.add(new StringAttributeValue(new JavaSymbolName("value"), + idFieldName)); + final AnnotationMetadataBuilder pathVariableAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE, attributes); + + final List parameterTypes = Arrays.asList( + new AnnotatedJavaType(javaTypePersistenceMetadataHolder + .getIdentifierType(), pathVariableAnnotation.build()), + new AnnotatedJavaType(MODEL)); + final List parameterNames = Arrays.asList( + new JavaSymbolName(idFieldName), new JavaSymbolName("uiModel")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), "/{" + idFieldName + "}")); + requestMappingAttributes.add(PRODUCES_HTML); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + if (!dateTypes.isEmpty()) { + bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);"); + } + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"" + + entityName.toLowerCase() + "\", " + + findMethod.getMethodCall() + ");"); + bodyBuilder.appendFormalLine("uiModel.addAttribute(\"itemId\", " + + (compositePk ? "conversionService.convert(" : "") + + idFieldName + (compositePk ? ", String.class)" : "") + ");"); + bodyBuilder.appendFormalLine("return \"" + controllerPath + "/show\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getUpdateFormMethod( + final FieldMetadata idField, final MemberTypeAdditions findMethod) { + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = javaTypeMetadataHolder + .getPersistenceDetails(); + if (javaTypePersistenceMetadataHolder == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName("updateForm"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final String idFieldName = idField.getFieldName().getSymbolName(); + final List> attributes = new ArrayList>(); + attributes.add(new StringAttributeValue(new JavaSymbolName("value"), + idFieldName)); + final AnnotationMetadataBuilder pathVariableAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE, attributes); + + final List parameterTypes = Arrays.asList( + new AnnotatedJavaType(javaTypePersistenceMetadataHolder + .getIdentifierType(), pathVariableAnnotation.build()), + new AnnotatedJavaType(MODEL)); + final List parameterNames = Arrays.asList( + new JavaSymbolName(idFieldName), new JavaSymbolName("uiModel")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), "/{" + idFieldName + "}")); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("params"), "form")); + requestMappingAttributes.add(PRODUCES_HTML); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("populateEditForm(uiModel, " + + findMethod.getMethodCall() + ");"); + bodyBuilder.appendFormalLine("return \"" + controllerPath + + "/update\";"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } + + private MethodMetadataBuilder getUpdateMethod( + final MemberTypeAdditions updateMethod) { + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = javaTypeMetadataHolder + .getPersistenceDetails(); + if (javaTypePersistenceMetadataHolder == null + || javaTypePersistenceMetadataHolder.getMergeMethod() == null) { + return null; + } + final JavaSymbolName methodName = new JavaSymbolName("update"); + if (governorHasMethodWithSameName(methodName)) { + return null; + } + + final AnnotationMetadataBuilder validAnnotation = new AnnotationMetadataBuilder( + VALID); + + final List parameterTypes = Arrays + .asList(new AnnotatedJavaType(formBackingType, validAnnotation + .build()), new AnnotatedJavaType(BINDING_RESULT), + new AnnotatedJavaType(MODEL), new AnnotatedJavaType( + HTTP_SERVLET_REQUEST)); + final List parameterNames = Arrays.asList( + new JavaSymbolName(entityName), new JavaSymbolName( + "bindingResult"), new JavaSymbolName("uiModel"), + new JavaSymbolName("httpServletRequest")); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "PUT")))); + requestMappingAttributes.add(PRODUCES_HTML); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + final List annotations = new ArrayList(); + annotations.add(requestMapping); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("if (bindingResult.hasErrors()) {"); + bodyBuilder.indent(); + bodyBuilder.appendFormalLine("populateEditForm(uiModel, " + entityName + + ");"); + bodyBuilder.appendFormalLine("return \"" + controllerPath + + "/update\";"); + bodyBuilder.indentRemove(); + bodyBuilder.appendFormalLine("}"); + bodyBuilder.appendFormalLine("uiModel.asMap().clear();"); + bodyBuilder.appendFormalLine(updateMethod.getMethodCall() + ";"); + bodyBuilder.appendFormalLine("return \"redirect:/" + + controllerPath + + "/\" + encodeUrlPathSegment(" + + (compositePk ? "conversionService.convert(" : "") + + entityName + + "." + + javaTypePersistenceMetadataHolder + .getIdentifierAccessorMethod().getMethodName() + "()" + + (compositePk ? ", String.class)" : ".toString()") + + ", httpServletRequest);"); + + final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder( + getId(), Modifier.PUBLIC, methodName, STRING, parameterTypes, + parameterNames, bodyBuilder); + methodBuilder.setAnnotations(annotations); + return methodBuilder; + } +} diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadataProvider.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadataProvider.java new file mode 100644 index 000000000..959b0aece --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadataProvider.java @@ -0,0 +1,13 @@ +package org.springframework.roo.addon.web.mvc.controller.scaffold; + +import org.springframework.roo.classpath.itd.ItdTriggerBasedMetadataProvider; + +/** + * Provides {@link WebScaffoldMetadata}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface WebScaffoldMetadataProvider extends + ItdTriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadataProviderImpl.java b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadataProviderImpl.java new file mode 100644 index 000000000..63bc213bc --- /dev/null +++ b/addon-web-mvc-controller/src/main/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldMetadataProviderImpl.java @@ -0,0 +1,252 @@ +package org.springframework.roo.addon.web.mvc.controller.scaffold; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSISTENT_TYPE; +import static org.springframework.roo.model.RooJavaType.ROO_WEB_SCAFFOLD; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.web.mvc.controller.details.DateTimeFormatDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.WebMetadataService; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.layers.MemberTypeAdditions; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.support.util.CollectionUtils; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link WebScaffoldMetadataProvider}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class WebScaffoldMetadataProviderImpl extends + AbstractMemberDiscoveringItdMetadataProvider implements + WebScaffoldMetadataProvider { + + protected final static Logger LOGGER = HandlerUtils.getLogger(WebScaffoldMetadataProviderImpl.class); + + private WebMetadataService webMetadataService; + + private final Map entityToWebScaffoldMidMap = new LinkedHashMap(); + private final Map webScaffoldMidToEntityMap = new LinkedHashMap(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + getMetadataDependencyRegistry().addNotificationListener(this); + getMetadataDependencyRegistry().registerDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + addMetadataTrigger(ROO_WEB_SCAFFOLD); + } + + @Override + protected String createLocalIdentifier(final JavaType javaType, + final LogicalPath path) { + return WebScaffoldMetadata.createIdentifier(javaType, path); + } + + protected void deactivate(final ComponentContext context) { + getMetadataDependencyRegistry().removeNotificationListener(this); + getMetadataDependencyRegistry().deregisterDependency( + PhysicalTypeIdentifier.getMetadataIdentiferType(), + getProvidesType()); + removeMetadataTrigger(ROO_WEB_SCAFFOLD); + } + + @Override + protected String getGovernorPhysicalTypeIdentifier( + final String metadataIdentificationString) { + final JavaType javaType = WebScaffoldMetadata + .getJavaType(metadataIdentificationString); + final LogicalPath path = WebScaffoldMetadata + .getPath(metadataIdentificationString); + return PhysicalTypeIdentifier.createIdentifier(javaType, path); + } + + public String getItdUniquenessFilenameSuffix() { + return "Controller"; + } + + @Override + protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) { + final JavaType governor = itdTypeDetails.getName(); + + // If the governor is a form backing object, refresh its local metadata + final String localMid = entityToWebScaffoldMidMap.get(governor); + if (localMid != null) { + return localMid; + } + + // If the governor is a layer component that manages a form backing + // object, refresh that object's local metadata + return getWebScaffoldMidIfLayerComponent(governor); + } + + @Override + protected ItdTypeDetailsProvidingMetadataItem getMetadata( + final String metadataIdentificationString, + final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalType, + final String itdFilename) { + + if(webMetadataService == null){ + webMetadataService = getWebMetadataService(); + } + Validate.notNull(webMetadataService, "WebMetadataService is required"); + + // We need to parse the annotation, which we expect to be present + final WebScaffoldAnnotationValues annotationValues = new WebScaffoldAnnotationValues( + governorPhysicalType); + final JavaType formBackingType = annotationValues + .getFormBackingObject(); + if (!annotationValues.isAnnotationFound() || formBackingType == null) { + return null; + } + + final MemberDetails formBackingObjectMemberDetails = getMemberDetails(formBackingType); + if (formBackingObjectMemberDetails == null) { + return null; + } + + final MemberHoldingTypeDetails formBackingMemberHoldingTypeDetails = MemberFindingUtils + .getMostConcreteMemberHoldingTypeDetailsWithTag( + formBackingObjectMemberDetails, PERSISTENT_TYPE); + if (formBackingMemberHoldingTypeDetails == null) { + return null; + } + + final Map crudAdditions = webMetadataService + .getCrudAdditions(formBackingType, metadataIdentificationString); + if (CollectionUtils.isEmpty(crudAdditions)) { + return null; + } + + // We need to be informed if our dependent metadata changes + getMetadataDependencyRegistry().registerDependency( + formBackingMemberHoldingTypeDetails.getDeclaredByMetadataId(), + metadataIdentificationString); + + // Remember that this entity JavaType matches up with this metadata + // identification string + // Start by clearing any previous association + final JavaType oldEntity = webScaffoldMidToEntityMap + .get(metadataIdentificationString); + if (oldEntity != null) { + entityToWebScaffoldMidMap.remove(oldEntity); + } + entityToWebScaffoldMidMap.put(formBackingType, + metadataIdentificationString); + webScaffoldMidToEntityMap.put(metadataIdentificationString, + formBackingType); + + final FieldMetadata idField = webMetadataService + .getIdentifierField(formBackingType); + final SortedMap relatedApplicationTypeMetadata = webMetadataService + .getRelatedApplicationTypeMetadata(formBackingType, + formBackingObjectMemberDetails, + metadataIdentificationString); + final List dependentApplicationTypeMetadata = webMetadataService + .getDependentApplicationTypeMetadata(formBackingType, + formBackingObjectMemberDetails, + metadataIdentificationString); + final Map datePatterns = webMetadataService + .getDatePatterns(formBackingType, + formBackingObjectMemberDetails, + metadataIdentificationString); + final Collection editableFieldTypes = formBackingObjectMemberDetails + .getPersistentFieldTypes(formBackingType, + getPersistenceMemberLocator()); + + return new WebScaffoldMetadata(metadataIdentificationString, + aspectName, governorPhysicalType, annotationValues, idField, + relatedApplicationTypeMetadata, + dependentApplicationTypeMetadata, datePatterns, crudAdditions, + editableFieldTypes, getTypeLocationService()); + } + + public String getProvidesType() { + return WebScaffoldMetadata.getMetadataIdentiferType(); + } + + /** + * If the given governor is a layer component (service, repository, etc.) + * that manages an entity for which we maintain web scaffold metadata, + * returns the ID of that metadata, otherwise returns null. + * TODO doesn't handle the case where the governor is a component that + * manages multiple entities, as it always returns the MID for the first + * entity found (in annotation order) for which we provide web metadata. We + * would need to enhance + * {@link AbstractMemberDiscoveringItdMetadataProvider#getLocalMidToRequest} + * to return a list of MIDs, rather than only one. + * + * @param governor the governor to check (required) + * @return see above + */ + private String getWebScaffoldMidIfLayerComponent(final JavaType governor) { + final ClassOrInterfaceTypeDetails governorTypeDetails = getTypeLocationService() + .getTypeDetails(governor); + if (governorTypeDetails != null) { + for (final JavaType type : governorTypeDetails.getLayerEntities()) { + final String localMid = entityToWebScaffoldMidMap.get(type); + if (localMid != null) { + /* + * The ITD's governor is a layer component that manages an + * entity for which we maintain web scaffold metadata => + * refresh that MD in case a layer has appeared or gone + * away. + */ + return localMid; + } + } + } + return null; + } + + + public WebMetadataService getWebMetadataService(){ + // Get all Services implement WebMetadataService interface + try { + ServiceReference[] references = context.getAllServiceReferences(WebMetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (WebMetadataService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load WebMetadataService on WebScaffoldMetadataProviderImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/configuration.xml b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/configuration.xml new file mode 100644 index 000000000..c27a4b163 --- /dev/null +++ b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/configuration.xml @@ -0,0 +1,62 @@ + + + + + + org.springframework + spring-webmvc + ${spring.version} + + + org.springframework.webflow + spring-js-resources + 2.2.1.RELEASE + + + commons-digester + commons-digester + 2.1 + + + commons-logging + commons-logging + + + + + commons-fileupload + commons-fileupload + 1.2.2 + + + javax.servlet.jsp.jstl + jstl-api + 1.2 + + + org.glassfish.web + jstl-impl + 1.2 + + + javax.el + el-api + 2.2 + provided + + + + + javax.servlet.jsp + jsp-api + 2.1 + provided + + + commons-codec + commons-codec + 1.5 + + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/converter/ApplicationConversionServiceFactoryBean-template._java b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/converter/ApplicationConversionServiceFactoryBean-template._java new file mode 100644 index 000000000..1dec130e7 --- /dev/null +++ b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/converter/ApplicationConversionServiceFactoryBean-template._java @@ -0,0 +1,18 @@ +package __PACKAGE__; + +import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.roo.addon.web.mvc.controller.converter.RooConversionService; + +/** + * A central place to register application converters and formatters. + */ +@RooConversionService +public class ApplicationConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean { + + @Override + protected void installFormatters(FormatterRegistry registry) { + super.installFormatters(registry); + // Register application converters and formatters + } +} diff --git a/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/json/configuration.xml b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/json/configuration.xml new file mode 100644 index 000000000..89dd40cde --- /dev/null +++ b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/json/configuration.xml @@ -0,0 +1,12 @@ + + + + + + org.springframework + spring-webmvc + ${spring.version} + + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/web-template.xml b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/web-template.xml new file mode 100644 index 000000000..6ca96a4cd --- /dev/null +++ b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/web-template.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-additions.xml b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-additions.xml new file mode 100644 index 000000000..854d0f059 --- /dev/null +++ b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-additions.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dataAccessFailure + resourceNotFound + resourceNotFound + resourceNotFound + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config.xml b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config.xml new file mode 100644 index 000000000..5011982c6 --- /dev/null +++ b/addon-web-mvc-controller/src/main/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/test/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypeMetadataDetailsTest.java b/addon-web-mvc-controller/src/test/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypeMetadataDetailsTest.java new file mode 100644 index 000000000..9a244fe13 --- /dev/null +++ b/addon-web-mvc-controller/src/test/java/org/springframework/roo/addon/web/mvc/controller/details/JavaTypeMetadataDetailsTest.java @@ -0,0 +1,57 @@ +package org.springframework.roo.addon.web.mvc.controller.details; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link JavaTypeMetadataDetails} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JavaTypeMetadataDetailsTest { + + /** + * Creates a test instance with the given {@link JavaType} + * + * @param javaType + * @return a non-null instance + */ + private JavaTypeMetadataDetails getTestInstance(final JavaType javaType) { + return new JavaTypeMetadataDetails(javaType, "the-plural", false, + false, null, "the-controller-path"); + } + + @Test + public void testInstancesWithDifferentJavaTypesAreNotEqual() { + // Set up + final JavaType mockJavaType1 = mock(JavaType.class); + final JavaType mockJavaType2 = mock(JavaType.class); + final JavaTypeMetadataDetails javaTypeMetadataDetails = getTestInstance(mockJavaType1); + final JavaTypeMetadataDetails otherJavaTypeMetadataDetails = mock(JavaTypeMetadataDetails.class); + when(otherJavaTypeMetadataDetails.getJavaType()).thenReturn( + mockJavaType2); + + // Invoke and check + assertFalse(javaTypeMetadataDetails + .equals(otherJavaTypeMetadataDetails)); + } + + @Test + public void testInstancesWithSameJavaTypesAreEqual() { + // Set up + final JavaType mockJavaType = mock(JavaType.class); + final JavaTypeMetadataDetails javaTypeMetadataDetails = getTestInstance(mockJavaType); + final JavaTypeMetadataDetails otherJavaTypeMetadataDetails = mock(JavaTypeMetadataDetails.class); + when(otherJavaTypeMetadataDetails.getJavaType()).thenReturn( + mockJavaType); + + // Invoke and check + assertTrue(javaTypeMetadataDetails.equals(otherJavaTypeMetadataDetails)); + } +} diff --git a/addon-web-mvc-controller/src/test/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldAnnotationValuesTest.java b/addon-web-mvc-controller/src/test/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldAnnotationValuesTest.java new file mode 100644 index 000000000..c17ee634e --- /dev/null +++ b/addon-web-mvc-controller/src/test/java/org/springframework/roo/addon/web/mvc/controller/scaffold/WebScaffoldAnnotationValuesTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.web.mvc.controller.scaffold; + +import org.springframework.roo.classpath.details.annotations.populator.AnnotationValuesTestCase; + +/** + * Unit test of {@link WebScaffoldAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class WebScaffoldAnnotationValuesTest extends + AnnotationValuesTestCase { + + @Override + protected Class getAnnotationClass() { + return RooWebScaffold.class; + } + + @Override + protected Class getValuesClass() { + return WebScaffoldAnnotationValues.class; + } +} diff --git a/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-conversionService.xml b/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-conversionService.xml new file mode 100644 index 000000000..a308802c7 --- /dev/null +++ b/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-conversionService.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-customConversionService.xml b/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-customConversionService.xml new file mode 100644 index 000000000..4d38008bd --- /dev/null +++ b/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config-customConversionService.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config.xml b/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config.xml new file mode 100644 index 000000000..8e9ede95c --- /dev/null +++ b/addon-web-mvc-controller/src/test/resources/org/springframework/roo/addon/web/mvc/controller/webmvc-config.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dataAccessFailure + resourceNotFound + resourceNotFound + resourceNotFound + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-embedded/pom.xml b/addon-web-mvc-embedded/pom.xml new file mode 100644 index 000000000..7dd4ac7b5 --- /dev/null +++ b/addon-web-mvc-embedded/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.web.mvc.embedded + bundle + Spring Roo - Addon - Web MVC Embedded Extensions + Extension to the MVC add-on which allows addition of embedded features such as maps, videos, etc into web pages. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.bootstrap + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + + + org.springframework.roo + org.springframework.roo.url.stream + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.1 + + + copy-dependencies + package + + copy-dependencies + + + + + ${project.build.directory}/all + true + compile + org.apache.felix.scr.annotations + org.osgi + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2-beta-5 + + + src/main/assembly/assembly.xml + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/AbstractEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/AbstractEmbeddedProvider.java new file mode 100644 index 000000000..b8bc014c0 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/AbstractEmbeddedProvider.java @@ -0,0 +1,205 @@ +package org.springframework.roo.addon.web.mvc.embedded; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.springframework.roo.addon.web.mvc.jsp.JspOperations; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.url.stream.UrlInputStreamService; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Convenience class for the implementation of a {@link EmbeddedProvider}. + * Offers methods for installing tagx, jspx files and making HTTP requests. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component(componentAbstract = true) +public abstract class AbstractEmbeddedProvider implements EmbeddedProvider { + + private static final Logger LOGGER = Logger + .getLogger(AbstractEmbeddedProvider.class.getName()); + + @Reference private FileManager fileManager; + @Reference private UrlInputStreamService urlInputStreamService; + @Reference private JspOperations jspOperations; + @Reference private PathResolver pathResolver; + + /** + * Convenience method to create a readable title for a EmbeddedCompletor + * enum value + * + * @param providerName the original String + * @return the readable String + */ + private String getTitle(final String providerName) { + final String[] names = providerName.split("_"); + final StringBuilder sb = new StringBuilder(); + for (final String name : names) { + sb.append(StringUtils.capitalize(name.toLowerCase())); + sb.append(" "); + } + return sb.toString().trim(); + } + + /** + * Convenience method to clean view name of a jspx. + * + * @param viewName the view name to clean + * @param defaultName the default page name + * @return the cleaned name + */ + public String getViewName(String viewName, final String defaultName) { + if (viewName == null || viewName.length() == 0) { + viewName = defaultName; + } + if (viewName.endsWith(".jspx")) { + viewName = viewName.substring(0, viewName.indexOf(".") - 1); + } + return viewName.toLowerCase(); + } + + /** + * Method to install jspx file into /WEB-INF/views/embed/ of target project. + * + * @param viewName the jspx file name to install (required) + * @param title the title of the page to be displayed (not required, + * viewName is used alternatively) + * @param contentElement the DOM element to include into the page. + */ + public void installJspx(String viewName, String title, + final Element contentElement) { + Validate.notBlank(viewName, "View name required"); + Validate.notNull(contentElement, "Content element required"); + if (StringUtils.isBlank(title)) { + title = getTitle(viewName); + } + viewName = getViewName(viewName, "default"); + final String jspx = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/views/embed/" + viewName + + ".jspx"); + final Document document = contentElement.getOwnerDocument(); + if (!fileManager.exists(jspx)) { + // Add document namespaces + final Element div = new XmlElementBuilder("div", document) + .addAttribute("xmlns:util", + "urn:jsptagdir:/WEB-INF/tags/util") + .addAttribute("xmlns:embed", + "urn:jsptagdir:/WEB-INF/tags/embed") + .addAttribute("xmlns:jsp", "http://java.sun.com/JSP/Page") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", "yes") + .build()).build(); + document.appendChild(div); + + div.appendChild(new XmlElementBuilder("util:panel", document) + .addAttribute("id", "title").addAttribute("title", title) + .addChild(contentElement).build()); + + jspOperations + .installView("/embed", viewName, title, "Embedded", + document, + pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP)); + + } + else { + LOGGER.warning("Could not install jspx with name " + + viewName + + " because it exists already. Use the --viewName attribute to specify unique name."); + } + } + + /** + * Method to install tagx file into /WEB-INF/tags/embed/ of target project. + * + * @param tagName + */ + public void installTagx(String tagName) { + Validate.notBlank(tagName, "Tag name required"); + + if (!tagName.endsWith(".tagx")) { + tagName = tagName.concat(".tagx"); + } + final String tagx = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/tags/embed/" + tagName); + if (!fileManager.exists(tagx)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), "tags/" + + tagName); + outputStream = fileManager.createFile(tagx).getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Could not install " + tagx); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + /** + * Helper method to determine if a given provider is supported by this + * implementation. + * + * @param candidate the provider name + * @param completors the completors offered by this implementation + * @return true if the provider is supported + */ + public boolean isProviderSupported(final String candidate, + final EmbeddedCompletor[] completors) { + for (final EmbeddedCompletor completor : completors) { + if (completor.name().equalsIgnoreCase(candidate)) { + return true; + } + } + return false; + } + + /** + * Method to send a HTTP GET request through the Roo provided + * infrastructure. + * + * @param urlStr the URL + * @return the result of the GET request. + */ + public String sendHttpGetRequest(final String urlStr) { + Validate.notBlank(urlStr, "URL required"); + + String result = null; + if (urlStr.startsWith("http://")) { + InputStream inputStream = null; + try { + final URL url = new URL(urlStr); + inputStream = urlInputStreamService.openConnection(url); + return IOUtils.toString(inputStream); + } + catch (final IOException e) { + LOGGER.warning("Unable to connect to " + urlStr); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + return result; + } +} \ No newline at end of file diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedCommands.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedCommands.java new file mode 100644 index 000000000..547e39946 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedCommands.java @@ -0,0 +1,160 @@ +package org.springframework.roo.addon.web.mvc.embedded; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.web.mvc.embedded.provider.DocumentEmbeddedProvider.DocumentProvider; +import org.springframework.roo.addon.web.mvc.embedded.provider.PhotoEmbeddedProvider.PhotoProvider; +import org.springframework.roo.addon.web.mvc.embedded.provider.VideoEmbeddedProvider.VideoProvider; +import org.springframework.roo.addon.web.mvc.embedded.provider.VideoStreamEmbeddedProvider.VideoStreamProvider; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Commands for 'web mvc embed'. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class EmbeddedCommands implements CommandMarker { + + @Reference private EmbeddedOperations embeddedOperations; + @Reference private StaticFieldConverter staticFieldConverter; + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(VideoProvider.class); + staticFieldConverter.add(DocumentProvider.class); + staticFieldConverter.add(VideoStreamProvider.class); + staticFieldConverter.add(PhotoProvider.class); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(VideoProvider.class); + staticFieldConverter.remove(DocumentProvider.class); + staticFieldConverter.remove(VideoStreamProvider.class); + staticFieldConverter.remove(PhotoProvider.class); + } + + @CliCommand(value = "web mvc embed generic", help = "Embed media by URL into your WEB MVC application") + public void embed( + @CliOption(key = "url", mandatory = true, help = "The url of the source to be embedded") final String url, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + embeddedOperations.embed(url, viewName); + } + + @CliCommand(value = "web mvc embed document", help = "Embed a document for your WEB MVC application") + public void embedDocument( + @CliOption(key = "provider", mandatory = true, help = "The id of the document") final DocumentProvider provider, + @CliOption(key = "documentId", mandatory = true, help = "The id of the document") final String id, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", provider.name()); + options.put("id", id); + embeddedOperations.install(viewName, options); + } + + @CliCommand(value = "web mvc embed map", help = "Embed a map for your WEB MVC application") + public void embedMap( + @CliOption(key = "location", mandatory = true, help = "The location of the map (ie \"Sydney, Australia\")") final String location, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", "GOOGLE_MAPS"); + options.put("location", location); + embeddedOperations.install(viewName, options); + } + + @CliCommand(value = "web mvc embed photos", help = "Embed a photo gallery for your WEB MVC application") + public void embedPhotos( + @CliOption(key = "provider", mandatory = true, help = "The provider of the photo gallery") final PhotoProvider provider, + @CliOption(key = "userId", mandatory = true, help = "The user id") final String userId, + @CliOption(key = "albumId", mandatory = true, help = "The album id") final String albumId, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", provider.name()); + options.put("userId", userId); + options.put("albumId", albumId); + embeddedOperations.install(viewName, options); + } + + @CliCommand(value = "web mvc embed twitter", help = "Embed twitter messages into your WEB MVC application") + public void embedTwitter( + @CliOption(key = "searchTerm", mandatory = true, help = "The search term to display results for") final String searchTerm, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", "TWITTER"); + options.put("searchTerm", searchTerm); + embeddedOperations.install(viewName, options); + } + + @CliCommand(value = "web mvc embed video", help = "Embed a video for your WEB MVC application") + public void embedVideo( + @CliOption(key = "provider", mandatory = true, help = "The id of the video") final VideoProvider provider, + @CliOption(key = "videoId", mandatory = true, help = "The id of the video") final String id, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", provider.name()); + options.put("id", id); + embeddedOperations.install(viewName, options); + } + + @CliCommand(value = "web mvc embed stream video", help = "Embed a video stream into your WEB MVC application") + public void embedVideoStream( + @CliOption(key = "provider", mandatory = true, help = "The provider of the video stream") final VideoStreamProvider provider, + @CliOption(key = "streamId", mandatory = true, help = "The stream id") final String streamId, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", provider.name()); + options.put("id", streamId); + embeddedOperations.install(viewName, options); + } + + @CliCommand(value = "web mvc embed wave", help = "Embed Google wave integration for your WEB MVC application") + public void embedWave( + @CliOption(key = "waveId", mandatory = true, help = "The key of the wave") final String key, + @CliOption(key = "viewName", mandatory = false, help = "The name of the jspx view") final String viewName) { + + final Map options = new HashMap(); + options.put("provider", "GOOGLE_WAVE"); + options.put("id", key); + embeddedOperations.install(viewName, options); + } + + // TODO: disabled due to ROO-2562 + // @CliCommand(value="web mvc embed finances", + // help="Embed a stock ticker into your WEB MVC application") + // public void embedFinance(@CliOption(key="stockSymbol", mandatory=true, + // help="The stock symbol") String stockSymbol, + // @CliOption(key="viewName", mandatory=false, + // help="The name of the jspx view") String viewName) { + // + // Map options = new HashMap(); + // options.put("provider", "FINANCES"); + // options.put("stockSymbol", stockSymbol); + // embeddedOperations.install(viewName, options); + // } + + @CliAvailabilityIndicator({ "web mvc embed generic", "web mvc embed wave", + "web mvc embed map", "web mvc embed document", + "web mvc embed video", "web mvc embed photos", + "web mvc embed stream video", "web mvc embed finances", + "web mvc embed twitter" }) + public boolean isPropertyAvailable() { + return embeddedOperations.isEmbeddedInstallationPossible(); + } +} \ No newline at end of file diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedCompletor.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedCompletor.java new file mode 100644 index 000000000..19f6ddab7 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedCompletor.java @@ -0,0 +1,12 @@ +package org.springframework.roo.addon.web.mvc.embedded; + +/** + * Simple marker interface used for command completions. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface EmbeddedCompletor { + + String name(); +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedOperations.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedOperations.java new file mode 100644 index 000000000..221d5a1cd --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedOperations.java @@ -0,0 +1,18 @@ +package org.springframework.roo.addon.web.mvc.embedded; + +import java.util.Map; + +/** + * Provides operations for the web mvc embed addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface EmbeddedOperations { + + boolean embed(String url, String viewName); + + boolean install(String viewName, Map options); + + boolean isEmbeddedInstallationPossible(); +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedOperationsImpl.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedOperationsImpl.java new file mode 100644 index 000000000..518dc5558 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedOperationsImpl.java @@ -0,0 +1,84 @@ +package org.springframework.roo.addon.web.mvc.embedded; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link EmbeddedOperations). + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Service +@Component +@Reference(name = "embeddedProvider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = EmbeddedProvider.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class EmbeddedOperationsImpl implements EmbeddedOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(EmbeddedOperationsImpl.class); + + private final Object mutex = new Object(); + + @Reference private ProjectOperations projectOperations; + private final Set providers = new HashSet(); + + protected void bindEmbeddedProvider(final EmbeddedProvider provider) { + synchronized (mutex) { + providers.add(provider); + } + } + + public boolean embed(final String url, final String viewName) { + for (final EmbeddedProvider provider : getEmbeddedProviders()) { + if (provider.embed(url, viewName)) { + return true; + } + } + LOGGER.warning("Could not find a matching provider for this URL"); + return false; + } + + private Set getEmbeddedProviders() { + synchronized (mutex) { + return Collections.unmodifiableSet(providers); + } + } + + public boolean install(final String viewName, + final Map options) { + for (final EmbeddedProvider provider : getEmbeddedProviders()) { + if (provider.install(viewName, options)) { + return true; + } + } + LOGGER.warning("Could not find a matching implementation for this 'web mvc embed' type"); + return false; + } + + public boolean isEmbeddedInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + protected void unbindEmbeddedProvider(final EmbeddedProvider provider) { + synchronized (mutex) { + if (providers.contains(provider)) { + providers.remove(provider); + } + } + } +} \ No newline at end of file diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedProvider.java new file mode 100644 index 000000000..11e2acf3f --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/EmbeddedProvider.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.embedded; + +import java.util.Map; + +/** + * Addon extension point interface. Implement this to add new embeddable + * providers by taking advantage of the infrastructure provided by this addon. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface EmbeddedProvider { + + /** + * Create a embed page via a generic URL supplied to this command. + * + * @param url the URL to be inspected + * @param viewName the name for the resulting jspx page (optional) + * @return true if this addon can handle the URL offered (otherwise you MUST + * return false so other addons can provide the implementation for + * the URL provided) + */ + boolean embed(String url, String viewName); + + /** + * Implement this method to provide alternative (to the generic URL-based + * approach) offered by embed method. + * + * @param viewName viewName the name for the resulting jspx page (optional) + * @param options a map of options to be consumed by the addon + * @return true if this addon can handle the options offered (otherwise you + * MUST return false so other addons can provide the implementation + * for the options provided) + */ + boolean install(String viewName, Map options); +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/DocumentEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/DocumentEmbeddedProvider.java new file mode 100644 index 000000000..4e06de1b6 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/DocumentEmbeddedProvider.java @@ -0,0 +1,113 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.addon.web.mvc.embedded.EmbeddedCompletor; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Provider to embed documents via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class DocumentEmbeddedProvider extends AbstractEmbeddedProvider { + + public enum DocumentProvider implements EmbeddedCompletor { + GOOGLE_PRESENTATION, SCRIBD, SLIDESHARE; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("provider", name()); + return builder.toString(); + } + } + + public boolean embed(final String url, final String viewName) { + if (url.contains("slideshare.net")) { + // Expected format + // http://www.slideshare.net/schmidtstefan/spring-one2-gx-slides-stefan-schmidt + final Map options = new HashMap(); + options.put("provider", DocumentProvider.SLIDESHARE.name()); + options.put("id", url); + return install(viewName, options); + } + else if (url.contains("scribd.com")) { + // Expected format + // http://www.scribd.com/doc/27766735/Introduction-to-SpringRoo + final String[] split = url.split("/"); + if (split.length > 4) { + final Map options = new HashMap(); + options.put("provider", DocumentProvider.SCRIBD.name()); + options.put("id", split[4]); + return install(viewName, options); + } + return false; + } + else if (url.contains("docs.google.") && url.contains("present")) { + // Expected format + // http://docs.google.com/present/view?id=dd8rf8t9_31c9f2fcgd&revision=_latest&start=0&theme=blank&authkey=CLj5iZwJ&cwj=true + final String qStart = url.substring(url.indexOf("id=") + 3); + final Map options = new HashMap(); + options.put("provider", DocumentProvider.GOOGLE_PRESENTATION.name()); + options.put("id", qStart.substring( + 0, + qStart.indexOf("&") == -1 ? qStart.length() : qStart + .indexOf("&"))); + return install(getViewName(viewName, "googlepresentation"), options); + } + return false; + } + + private String getSlideShareId(final String url) { + final String json = sendHttpGetRequest("http://oohembed.com/oohembed/?url=" + + url.replace(":", "%3A")); + if (json != null) { + final String subDoc = json.substring(json.indexOf("doc=") + 4); + return subDoc.substring( + 0, + subDoc.indexOf("&") == -1 ? subDoc.length() : subDoc + .indexOf("&")); + } + return null; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 2 + || !options.containsKey("provider") + || !options.containsKey("id")) { + return false; + } + final String provider = options.get("provider"); + if (!isProviderSupported(provider, DocumentProvider.values())) { + return false; + } + String id = options.get("id"); + installTagx("document"); + if (DocumentProvider.SLIDESHARE.name().equals(provider) + && id.startsWith("http")) { + id = getSlideShareId(id); + } + final Element document = new XmlElementBuilder("embed:document", + XmlUtils.getDocumentBuilder().newDocument()) + .addAttribute("id", "doc_" + id).addAttribute("documentId", id) + .addAttribute("provider", provider.toLowerCase()).build(); + document.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(document)); + installJspx(getViewName(viewName, provider.toLowerCase()), null, + document); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/FinanceEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/FinanceEmbeddedProvider.java new file mode 100644 index 000000000..4a4a0e320 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/FinanceEmbeddedProvider.java @@ -0,0 +1,53 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; + +/** + * Provider to embed finance charts via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class FinanceEmbeddedProvider extends AbstractEmbeddedProvider { + + // TODO : disabled due to ROO-2562 + public boolean embed(final String url, final String viewName) { + // if (url.contains("wikinvest.com")) { + // // Expected format http://www.wikinvest.com/wiki/Vmw + // Map options = new HashMap(); + // options.put("provider", "FINANCES"); + // options.put("stockSymbol", url.substring(url.indexOf("wiki/") + 5)); + // return install(viewName, options); + // } + return false; + } + + // TODO : disabled due to ROO-2562 + public boolean install(final String viewName, + final Map options) { + // if (options == null || options.size() != 2 || + // !options.containsKey("provider") || + // !options.get("provider").equalsIgnoreCase("finances") || + // !options.containsKey("stockSymbol")) { + // return false; + // } + // String stockSymbol = options.get("stockSymbol"); + // installTagx("finances"); + // Element finances = new XmlElementBuilder("embed:finances", + // XmlUtils.getDocumentBuilder().newDocument()).addAttribute("id", + // "finances_" + stockSymbol).addAttribute("stockSymbol", + // stockSymbol).build(); + // finances.setAttribute("z", + // XmlRoundTripUtils.calculateUniqueKeyFor(finances)); + // installJspx(getViewName(viewName, + // options.get("provider").toLowerCase()), null, finances); + // return true; + return false; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/MapEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/MapEmbeddedProvider.java new file mode 100644 index 000000000..cd771c905 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/MapEmbeddedProvider.java @@ -0,0 +1,69 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Provider to embed maps via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class MapEmbeddedProvider extends AbstractEmbeddedProvider { + + public boolean embed(final String url, final String viewName) { + if (url.contains("maps.google")) { + // Expected format + // http://maps.google.com/maps?q=sydney,+Australia&.. the q= param + // needs to be present + final String qStart = url.substring(url.indexOf("q=") + 2); + + final Map options = new HashMap(); + options.put("provider", "GOOGLE_MAPS"); + options.put("location", qStart.substring( + 0, + !qStart.contains("&") ? qStart.length() : qStart + .indexOf("&"))); + return install(viewName, options); + } + return false; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 2 + || !options.containsKey("provider") + || !options.get("provider").equalsIgnoreCase("GOOGLE_MAPS") + || !options.containsKey("location")) { + return false; + } + String location = options.get("location"); + try { + location = URLDecoder.decode(location, "UTF-8"); + } + catch (final UnsupportedEncodingException ignore) { + } + installTagx("map"); + final Element map = new XmlElementBuilder("embed:map", XmlUtils + .getDocumentBuilder().newDocument()) + .addAttribute("id", "map_" + viewName) + .addAttribute("location", location).build(); + map.setAttribute("z", XmlRoundTripUtils.calculateUniqueKeyFor(map)); + installJspx( + getViewName(viewName, options.get("provider").toLowerCase()), + null, map); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/MicrobloggingEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/MicrobloggingEmbeddedProvider.java new file mode 100644 index 000000000..a9256d476 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/MicrobloggingEmbeddedProvider.java @@ -0,0 +1,63 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Provider to embed micro blog messages via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class MicrobloggingEmbeddedProvider extends AbstractEmbeddedProvider { + + public boolean embed(final String url, final String viewName) { + // Expected format http://twitter.com/#search?q=@SpringRoo + if (url.contains("twitter.com")) { + final Map options = new HashMap(); + options.put("provider", "TWITTER"); + options.put("searchTerm", url.substring(url.indexOf("q=") + 2)); + return install(viewName, options); + } + return false; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 2 + || !options.containsKey("provider") + || !options.get("provider").equalsIgnoreCase("TWITTER") + || !options.containsKey("searchTerm")) { + return false; + } + String searchTerm = options.get("searchTerm"); + try { + searchTerm = URLDecoder.decode(searchTerm, "UTF-8"); + } + catch (final UnsupportedEncodingException ignore) { + } + installTagx("microblogging"); + final Element twitter = new XmlElementBuilder("embed:microblogging", + XmlUtils.getDocumentBuilder().newDocument()) + .addAttribute("id", "twitter_" + searchTerm) + .addAttribute("searchTerm", searchTerm).build(); + twitter.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(twitter)); + installJspx( + getViewName(viewName, options.get("provider").toLowerCase()), + null, twitter); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/PhotoEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/PhotoEmbeddedProvider.java new file mode 100644 index 000000000..b26cfc325 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/PhotoEmbeddedProvider.java @@ -0,0 +1,104 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.addon.web.mvc.embedded.EmbeddedCompletor; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Provider to embed photo galleries via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class PhotoEmbeddedProvider extends AbstractEmbeddedProvider { + + public enum PhotoProvider implements EmbeddedCompletor { + FLIKR, PICASA; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("provider", name()); + return builder.toString(); + } + } + + public boolean embed(final String url, final String viewName) { + // Expected http://picasaweb.google.com.au/stsmedia/SydneyByNight + if (url.contains("picasaweb.google.")) { + final String[] split = url.split("/"); + if (split.length > 4) { + final Map options = new HashMap(); + options.put("provider", PhotoProvider.PICASA.name()); + options.put("userId", split[3]); + options.put("albumId", getPicasaId(url)); + return install(viewName, options); + } + return false; + } + else if (url.contains("flickr.")) { + final String[] split = url.split("/"); + if (split.length > 4) { + final Map options = new HashMap(); + options.put("provider", PhotoProvider.FLIKR.name()); + options.put("userId", split[4]); + options.put("albumId", split.length > 5 ? split[5] : split[4]); + return install(viewName, options); + } + return false; + } + return false; + } + + private String getPicasaId(final String url) { + final String json = sendHttpGetRequest("http://api.embed.ly/v1/api/oembed?url=" + + url); + if (json != null) { + final String subDoc = json + .substring(json.indexOf("albumid%2F") + 10); + return subDoc.substring( + 0, + subDoc.indexOf("%") == 1 ? subDoc.length() : subDoc + .indexOf("%")); + } + return null; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 3 + || !options.containsKey("provider") + || !options.containsKey("userId") + || !options.containsKey("albumId")) { + return false; + } + final String provider = options.get("provider"); + if (!isProviderSupported(provider, PhotoProvider.values())) { + return false; + } + final String userId = options.get("userId"); + final String albumId = options.get("albumId"); + installTagx("photos"); + final Element photos = new XmlElementBuilder("embed:photos", XmlUtils + .getDocumentBuilder().newDocument()) + .addAttribute("id", "photos_" + userId + "_" + albumId) + .addAttribute("albumId", albumId) + .addAttribute("userId", userId) + .addAttribute("provider", provider.toLowerCase()).build(); + photos.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(photos)); + installJspx(getViewName(viewName, provider.toLowerCase()), null, photos); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/VideoEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/VideoEmbeddedProvider.java new file mode 100644 index 000000000..3da7a537f --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/VideoEmbeddedProvider.java @@ -0,0 +1,129 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.io.ByteArrayInputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.addon.web.mvc.embedded.EmbeddedCompletor; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Provider to embed videos via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class VideoEmbeddedProvider extends AbstractEmbeddedProvider { + + public enum VideoProvider implements EmbeddedCompletor { + GOOGLE_VIDEO, SCREENR, VIDDLER, VIMEO, YOUTUBE; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("provider", name()); + return builder.toString(); + } + } + + public boolean embed(final String url, final String viewName) { + if (url.contains("youtube.com")) { + // Expected format: http://www.youtube.com/watch?v=Gb1Z0lfl52I + final Map options = new HashMap(); + options.put("provider", VideoProvider.YOUTUBE.name()); + options.put("id", url.substring(url.indexOf("v=") + 2)); + return install(viewName, options); + } + else if (url.contains("video.google.")) { + // Expected format: + // http://video.google.com/videoplay?docid=1753096859715615067# + final Map options = new HashMap(); + options.put("provider", VideoProvider.GOOGLE_VIDEO.name()); + options.put("id", url.substring(url.indexOf("docid=") + 6)); + return install(viewName, options); + } + else if (url.contains("vimeo.com")) { + // Expected format http://vimeo.com/11262623 + final Map options = new HashMap(); + options.put("provider", VideoProvider.VIMEO.name()); + options.put("id", url.substring(url.indexOf("vimeo.com/") + 10)); + return install(viewName, options); + } + else if (url.contains("viddler.com")) { + // Expected format + // http://www.viddler.com/explore/failblog/videos/715/ + final Map options = new HashMap(); + options.put("provider", VideoProvider.VIDDLER.name()); + options.put("id", getViddlerId(url)); + return install(viewName, options); + } + else if (url.contains("screenr.com")) { + // Expected format http://screenr.com/GlR + final String[] split = url.split("/"); + if (split.length > 3) { + final Map options = new HashMap(); + options.put("provider", VideoProvider.SCREENR.name()); + options.put("id", split[3]); + return install(viewName, options); + } + return false; + } + return false; + } + + private String getViddlerId(final String url) { + final String xml = sendHttpGetRequest("http://lab.viddler.com/services/oembed/?url=" + + url + "&type=simple&format=xml"); + if (xml != null) { + try { + final Document doc = XmlUtils.readXml(new ByteArrayInputStream( + xml.getBytes())); + final Element movie = XmlUtils.findRequiredElement( + "//param[@name='movie']", doc.getDocumentElement()); + final String movieUrl = movie.getAttribute("value"); + return movieUrl.substring(movieUrl.indexOf("simple/") + 7); + } + catch (final Exception e) { + throw new IllegalStateException( + "Could not parse oembed document from viddler.com", e); + } + } + return null; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 2 + || !options.containsKey("provider") + || !options.containsKey("id")) { + return false; + } + final String provider = options.get("provider"); + if (!isProviderSupported(provider, VideoProvider.values())) { + return false; + } + final String id = options.get("id"); + if (StringUtils.isBlank(id)) { + return false; + } + installTagx("video"); + final Element video = new XmlElementBuilder("embed:video", XmlUtils + .getDocumentBuilder().newDocument()) + .addAttribute("id", "video_" + id).addAttribute("videoId", id) + .addAttribute("provider", provider.toLowerCase()).build(); + video.setAttribute("z", XmlRoundTripUtils.calculateUniqueKeyFor(video)); + installJspx(getViewName(viewName, provider.toLowerCase()), null, video); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/VideoStreamEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/VideoStreamEmbeddedProvider.java new file mode 100644 index 000000000..3d6b36583 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/VideoStreamEmbeddedProvider.java @@ -0,0 +1,85 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.addon.web.mvc.embedded.EmbeddedCompletor; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Provider to embed video streams via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class VideoStreamEmbeddedProvider extends AbstractEmbeddedProvider { + + public enum VideoStreamProvider implements EmbeddedCompletor { + LIVESTREAM, USTREAM; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("provider", name()); + return builder.toString(); + } + } + + public boolean embed(final String url, final String viewName) { + if (url.contains("ustream.tv")) { + // Expected format http://www.ustream.tv/flash/live/1/4424524 + final String[] split = url.split("/"); + if (split.length > 6) { + final Map options = new HashMap(); + options.put("provider", VideoStreamProvider.USTREAM.name()); + options.put("id", split[6]); + return install(viewName, options); + } + return false; + } + else if (url.contains("livestream.com")) { + // Expected format http://www.livestream.com/wkrg_oil_spill + final String[] split = url.split("/"); + if (split.length > 3) { + final Map options = new HashMap(); + options.put("provider", VideoStreamProvider.LIVESTREAM.name()); + options.put("id", split[3]); + return install(viewName, options); + } + return false; + } + return false; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 2 + || !options.containsKey("provider") + || !options.containsKey("id")) { + return false; + } + final String provider = options.get("provider"); + if (!isProviderSupported(provider, VideoStreamProvider.values())) { + return false; + } + final String id = options.get("id"); + installTagx("videostream"); + final Element video = new XmlElementBuilder("embed:videostream", + XmlUtils.getDocumentBuilder().newDocument()) + .addAttribute("id", "video_stream_" + id) + .addAttribute("streamId", id) + .addAttribute("provider", provider.toLowerCase()).build(); + video.setAttribute("z", XmlRoundTripUtils.calculateUniqueKeyFor(video)); + installJspx(getViewName(viewName, provider.toLowerCase()), null, video); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/WaveEmbeddedProvider.java b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/WaveEmbeddedProvider.java new file mode 100644 index 000000000..5ca0f73b1 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/java/org/springframework/roo/addon/web/mvc/embedded/provider/WaveEmbeddedProvider.java @@ -0,0 +1,60 @@ +package org.springframework.roo.addon.web.mvc.embedded.provider; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.embedded.AbstractEmbeddedProvider; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Provider to embed a Google wave via a URL or specific install method. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class WaveEmbeddedProvider extends AbstractEmbeddedProvider { + + public boolean embed(final String url, final String viewName) { + // Expected format + // https://wave.google.com/wave/#restored:wave:googlewave.com%252Fw%252B8Hj0sgUxC + if (url.contains("wave.google.")) { + final String qStart = url.substring(url.indexOf("%252B") + 5); + final Map options = new HashMap(); + options.put("provider", "GOOGLE_WAVE"); + options.put("id", qStart.substring( + 0, + !qStart.contains(".") ? qStart.length() : qStart + .indexOf("."))); + return install(viewName, options); + } + return false; + } + + public boolean install(final String viewName, + final Map options) { + if (options == null || options.size() != 2 + || !options.containsKey("provider") + || !options.get("provider").equalsIgnoreCase("GOOGLE_WAVE") + || !options.containsKey("id")) { + return false; + } + final String id = options.get("id"); + installTagx("wave"); + final Element wave = new XmlElementBuilder("embed:wave", XmlUtils + .getDocumentBuilder().newDocument()) + .addAttribute("id", "wave_" + id).addAttribute("waveId", id) + .build(); + wave.setAttribute("z", XmlRoundTripUtils.calculateUniqueKeyFor(wave)); + installJspx( + getViewName(viewName, options.get("provider").toLowerCase()), + null, wave); + return true; + } +} diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/document.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/document.tagx new file mode 100644 index 000000000..1c5e11479 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/document.tagx @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/finances.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/finances.tagx new file mode 100644 index 000000000..1634e7e7e --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/finances.tagx @@ -0,0 +1,21 @@ + + + + + + + + + + ${stockSymbol} + + +
    + +
    diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/map.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/map.tagx new file mode 100644 index 000000000..66d0c6f46 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/map.tagx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/microblogging.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/microblogging.tagx new file mode 100644 index 000000000..97157c20c --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/microblogging.tagx @@ -0,0 +1,46 @@ + + + + + + + + + + ${searchTerm} + + + + diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/photos.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/photos.tagx new file mode 100644 index 000000000..9f4d236c5 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/photos.tagx @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/video.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/video.tagx new file mode 100644 index 000000000..96d013c0f --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/video.tagx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/videostream.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/videostream.tagx new file mode 100644 index 000000000..141e913b7 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/videostream.tagx @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/wave.tagx b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/wave.tagx new file mode 100644 index 000000000..fc8bbe966 --- /dev/null +++ b/addon-web-mvc-embedded/src/main/resources/org/springframework/roo/addon/web/mvc/embedded/provider/tags/wave.tagx @@ -0,0 +1,29 @@ + + + + + + + + + + ${waveId} + +
    + + +
    diff --git a/addon-web-mvc-jsp/legal-addon-web-mvc-jsp.txt b/addon-web-mvc-jsp/legal-addon-web-mvc-jsp.txt new file mode 100644 index 000000000..546e98c29 --- /dev/null +++ b/addon-web-mvc-jsp/legal-addon-web-mvc-jsp.txt @@ -0,0 +1,19 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: FamFamFam Icon sets +Software Web Site: http://www.famfamfam.com/lab/icons/ +Effective License: Creative Commons Attribution 3.0 License +License Info Page: http://www.famfamfam.com/lab/icons/silk/ + +FamFamFam Icon sets are used to enhance the look and feel of +applications generated by Spring Roo. + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/addon-web-mvc-jsp/pom.xml b/addon-web-mvc-jsp/pom.xml new file mode 100644 index 000000000..39a2f70e3 --- /dev/null +++ b/addon-web-mvc-jsp/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.web.mvc.jsp + bundle + Spring Roo - Addon - Web MVC JSP View + Configuration and integration of Spring MVC JSP features in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + + + org.springframework.roo + org.springframework.roo.addon.propfiles + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + org.springframework.roo + org.springframework.roo.uaa + + + org.springframework.roo + org.springframework.roo.addon.backup + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspCommands.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspCommands.java new file mode 100644 index 000000000..9bd1dba9b --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspCommands.java @@ -0,0 +1,180 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.I18n; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.support.logging.HandlerUtils; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentContext; + +/** + * Commands for Web-related add-on to be used by the Roo shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class JspCommands implements CommandMarker { + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private static Logger LOGGER = HandlerUtils.getLogger(JspCommands.class); + + private JspOperations jspOperations; + private PathResolver pathResolver; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + @Deprecated + @CliCommand(value = "web mvc install view", help = "Create a new static view.") + public void installView( + @CliOption(key = "path", mandatory = true, help = "The path the static view to create in (required, ie '/foo/blah')") final String path, + @CliOption(key = "viewName", mandatory = true, help = "The view name the mapping this view should adopt (required, ie 'index')") final String viewName, + @CliOption(key = "title", mandatory = true, help = "The title of the view") final String title) { + + LOGGER.warning("This command has been deprecated and will be disabled soon! Please use 'web mvc setup' followed by 'web mvc install view' instead."); + view(path, viewName, title); + } + + @CliAvailabilityIndicator({ "web mvc controller", "controller class", + "web mvc install view", "web mvc view", "web mvc update tags" }) + public boolean isControllerClassAvailable() { + return getJspOperations().isControllerAvailable(); + } + + @CliAvailabilityIndicator({ "web mvc install language", "web mvc language" }) + public boolean isInstallLanguageAvailable() { + return getJspOperations().isInstallLanguageCommandAvailable(); + } + + @CliAvailabilityIndicator({ "web mvc setup" }) + public boolean isProjectAvailable() { + return getJspOperations().isMvcInstallationPossible(); + } + + @Deprecated + @CliCommand(value = "web mvc install language", help = "Install new internationalization bundle for MVC scaffolded UI.") + public void lang( + @CliOption(key = { "", "code" }, mandatory = true, help = "The language code for the desired bundle") final I18n i18n) { + + if (i18n == null) { + LOGGER.warning("Could not parse language code"); + return; + } + getJspOperations().installI18n(i18n, + getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP)); + } + + @CliCommand(value = "web mvc language", help = "Install new internationalization bundle for MVC scaffolded UI.") + public void language( + @CliOption(key = { "", "code" }, mandatory = true, help = "The language code for the desired bundle") final I18n i18n) { + + if (i18n == null) { + LOGGER.warning("Could not parse language code"); + return; + } + getJspOperations().installI18n(i18n, + getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP)); + } + + @Deprecated + @CliCommand(value = "controller class", help = "Create a new manual Controller (ie where you write the methods) - deprecated, use 'web mvc controller' instead") + public void newController( + @CliOption(key = { "class", "" }, mandatory = true, help = "The path and name of the controller object to be created") final JavaType controller, + @CliOption(key = "preferredMapping", mandatory = false, help = "Indicates a specific request mapping path for this controller (eg /foo/)") final String preferredMapping) { + + newMvcArtifact(controller, preferredMapping); + } + + @CliCommand(value = "web mvc controller", help = "Create a new manual Controller (ie where you write the methods)") + public void newMvcArtifact( + @CliOption(key = { "class", "" }, mandatory = true, help = "The path and name of the controller object to be created") final JavaType controller, + @CliOption(key = "preferredMapping", mandatory = false, help = "Indicates a specific request mapping path for this controller (eg /foo/)") final String preferredMapping) { + + getJspOperations().createManualController(controller, preferredMapping, + getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP)); + } + + @CliCommand(value = "web mvc update tags", help = "Replace an existing application tagx library with the latest version (use --backup option to backup your application first)") + public void update( + @CliOption(key = "backup", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Backup your application before replacing your existing tag library") final boolean backup) { + + getJspOperations().updateTags(backup, + getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP)); + } + + @CliCommand(value = "web mvc view", help = "Create a new static view.") + public void view( + @CliOption(key = "path", mandatory = true, help = "The path the static view to create in (required, ie '/foo/blah')") final String path, + @CliOption(key = "viewName", mandatory = true, help = "The view name the mapping this view should adopt (required, ie 'index')") final String viewName, + @CliOption(key = "title", mandatory = true, help = "The title of the view") final String title) { + + getJspOperations().installView(path, viewName, title, "View", + getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP)); + } + + @CliCommand(value = "web mvc setup", help = "Setup a basic project structure for a Spring MVC / JSP application") + public void webMvcSetup() { + getJspOperations().installCommonViewArtefacts(); + } + + public JspOperations getJspOperations(){ + if(jspOperations == null){ + // Get all Services implement JspOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(JspOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (JspOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load JspOperations on JspCommands."); + return null; + } + }else{ + return jspOperations; + } + + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on JspCommands."); + return null; + } + }else{ + return pathResolver; + } + + } +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspMetadata.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspMetadata.java new file mode 100644 index 000000000..308c46563 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspMetadata.java @@ -0,0 +1,86 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; + +/** + * Metadata built from {@link WebScaffoldMetadata}. A single {@link JspMetadata} + * represents all JSPs for an associated controller. The metadata identifier for + * a {@link JspMetadata} is the fully qualifier name of the controller, and the + * source {@link Path} of the controller. This can be created using + * {@link #createIdentifier(JavaType, Path)}. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +public class JspMetadata extends AbstractMetadataItem { + + private static final String PROVIDES_TYPE_STRING = JspMetadata.class + .getName(); + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(PROVIDES_TYPE_STRING); + + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PROVIDES_TYPE_STRING, javaType, path); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PROVIDES_TYPE_STRING, metadataIdentificationString); + } + + public static String getMetadataIdentiferType() { + return PROVIDES_TYPE; + } + + public static LogicalPath getPath(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, + metadataIdentificationString); + } + + private final WebScaffoldAnnotationValues annotationValues; + + private final WebScaffoldMetadata webScaffoldMetadata; + + public JspMetadata(final String identifier, + final WebScaffoldMetadata webScaffoldMetadata) { + super(identifier); + Validate.isTrue( + isValid(identifier), + "Metadata identification string '%s' does not appear to be a valid", + identifier); + Validate.notNull(webScaffoldMetadata, "Web scaffold metadata required"); + + this.webScaffoldMetadata = webScaffoldMetadata; + annotationValues = webScaffoldMetadata.getAnnotationValues(); + } + + public WebScaffoldAnnotationValues getAnnotationValues() { + return annotationValues; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("web scaffold metadata id", webScaffoldMetadata.getId()); + return builder.toString(); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspMetadataListener.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspMetadataListener.java new file mode 100644 index 000000000..a4dbf8055 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspMetadataListener.java @@ -0,0 +1,586 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.addon.web.mvc.controller.details.FinderMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypePersistenceMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.WebMetadataService; +import org.springframework.roo.addon.web.mvc.controller.finder.WebFinderMetadata; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations; +import org.springframework.roo.addon.web.mvc.jsp.roundtrip.XmlRoundTripFileManager; +import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperations; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JpaJavaType; +import org.springframework.roo.model.RooJavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; + +/** + * Listens for {@link WebScaffoldMetadata} and produces JSPs when requested by + * that metadata. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class JspMetadataListener implements MetadataProvider, + MetadataNotificationListener { + + private static final String WEB_INF_VIEWS = "/WEB-INF/views/"; + + @Reference private FileManager fileManager; + @Reference private JspOperations jspOperations; + @Reference private MenuOperations menuOperations; + @Reference private MetadataDependencyRegistry metadataDependencyRegistry; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private PropFileOperations propFileOperations; + @Reference private TilesOperations tilesOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private WebMetadataService webMetadataService; + @Reference private XmlRoundTripFileManager xmlRoundTripFileManager; + + private final Map formBackingObjectTypesToLocalMids = new HashMap(); + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.registerDependency( + WebScaffoldMetadata.getMetadataIdentiferType(), + getProvidesType()); + metadataDependencyRegistry + .registerDependency( + WebFinderMetadata.getMetadataIdentiferType(), + getProvidesType()); + metadataDependencyRegistry.addNotificationListener(this); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.deregisterDependency( + WebScaffoldMetadata.getMetadataIdentiferType(), + getProvidesType()); + metadataDependencyRegistry + .deregisterDependency( + WebFinderMetadata.getMetadataIdentiferType(), + getProvidesType()); + metadataDependencyRegistry.removeNotificationListener(this); + } + + public MetadataItem get(final String jspMetadataId) { + // Work out the MIDs of the other metadata we depend on + // NB: The JavaType and Path are to the corresponding web scaffold + // controller class + + final String webScaffoldMetadataKey = WebScaffoldMetadata + .createIdentifier(JspMetadata.getJavaType(jspMetadataId), + JspMetadata.getPath(jspMetadataId)); + final WebScaffoldMetadata webScaffoldMetadata = (WebScaffoldMetadata) metadataService + .get(webScaffoldMetadataKey); + if (webScaffoldMetadata == null || !webScaffoldMetadata.isValid()) { + // Can't get the corresponding scaffold, so we certainly don't need + // to manage any JSPs at this time + return null; + } + + final JavaType formBackingType = webScaffoldMetadata + .getAnnotationValues().getFormBackingObject(); + final MemberDetails memberDetails = webMetadataService + .getMemberDetails(formBackingType); + final JavaTypeMetadataDetails formBackingTypeMetadataDetails = webMetadataService + .getJavaTypeMetadataDetails(formBackingType, memberDetails, + jspMetadataId); + Validate.notNull(formBackingTypeMetadataDetails, + "Unable to obtain metadata for type %s", + formBackingType.getFullyQualifiedTypeName()); + + formBackingObjectTypesToLocalMids.put(formBackingType, jspMetadataId); + + final SortedMap relatedTypeMd = webMetadataService + .getRelatedApplicationTypeMetadata(formBackingType, + memberDetails, jspMetadataId); + final JavaTypeMetadataDetails formbackingTypeMetadata = relatedTypeMd + .get(formBackingType); + Validate.notNull(formbackingTypeMetadata, + "Form backing type metadata required"); + final JavaTypePersistenceMetadataDetails formBackingTypePersistenceMetadata = formbackingTypeMetadata + .getPersistenceDetails(); + if (formBackingTypePersistenceMetadata == null) { + return null; + } + final ClassOrInterfaceTypeDetails formBackingTypeDetails = typeLocationService + .getTypeDetails(formBackingType); + final LogicalPath formBackingTypePath = PhysicalTypeIdentifier + .getPath(formBackingTypeDetails.getDeclaredByMetadataId()); + metadataDependencyRegistry.registerDependency(PhysicalTypeIdentifier + .createIdentifier(formBackingType, formBackingTypePath), + JspMetadata.createIdentifier(formBackingType, + formBackingTypePath)); + final LogicalPath path = JspMetadata.getPath(jspMetadataId); + + // Install web artifacts only if Spring MVC config is missing + // TODO: Remove this call when 'controller' commands are gone + final PathResolver pathResolver = projectOperations.getPathResolver(); + final LogicalPath webappPath = LogicalPath.getInstance( + Path.SRC_MAIN_WEBAPP, path.getModule()); + + if (!fileManager.exists(pathResolver.getIdentifier(webappPath, + WEB_INF_VIEWS))) { + jspOperations.installCommonViewArtefacts(path.getModule()); + } + + installImage(webappPath, "images/show.png"); + if (webScaffoldMetadata.getAnnotationValues().isUpdate()) { + installImage(webappPath, "images/update.png"); + } + if (webScaffoldMetadata.getAnnotationValues().isDelete()) { + installImage(webappPath, "images/delete.png"); + } + + final List eligibleFields = webMetadataService + .getScaffoldEligibleFieldMetadata(formBackingType, + memberDetails, jspMetadataId); + if (eligibleFields.isEmpty() + && formBackingTypePersistenceMetadata.getRooIdentifierFields() + .isEmpty()) { + return null; + } + final JspViewManager viewManager = new JspViewManager(eligibleFields, + webScaffoldMetadata.getAnnotationValues(), relatedTypeMd); + + String controllerPath = webScaffoldMetadata.getAnnotationValues() + .getPath(); + if (controllerPath.startsWith("/")) { + controllerPath = controllerPath.substring(1); + } + + // Make the holding directory for this controller + final String destinationDirectory = pathResolver.getIdentifier( + webappPath, WEB_INF_VIEWS + controllerPath); + if (!fileManager.exists(destinationDirectory)) { + fileManager.createDirectory(destinationDirectory); + } + else { + final File file = new File(destinationDirectory); + Validate.isTrue(file.isDirectory(), + "%s is a file, when a directory was expected", + destinationDirectory); + } + + // By now we have a directory to put the JSPs inside + final String listPath1 = destinationDirectory + "/list.jspx"; + xmlRoundTripFileManager.writeToDiskIfNecessary(listPath1, + viewManager.getListDocument()); + tilesOperations.addViewDefinition(controllerPath, webappPath, + controllerPath + "/" + "list", + TilesOperations.DEFAULT_TEMPLATE, WEB_INF_VIEWS + + controllerPath + "/list.jspx"); + + final String showPath = destinationDirectory + "/show.jspx"; + xmlRoundTripFileManager.writeToDiskIfNecessary(showPath, + viewManager.getShowDocument()); + tilesOperations.addViewDefinition(controllerPath, webappPath, + controllerPath + "/" + "show", + TilesOperations.DEFAULT_TEMPLATE, WEB_INF_VIEWS + + controllerPath + "/show.jspx"); + + final Map properties = new LinkedHashMap(); + + final JavaSymbolName categoryName = new JavaSymbolName( + formBackingType.getSimpleTypeName()); + properties.put("menu_category_" + + categoryName.getSymbolName().toLowerCase() + "_label", + categoryName.getReadableSymbolName()); + + final JavaSymbolName newMenuItemId = new JavaSymbolName("new"); + properties.put("menu_item_" + + categoryName.getSymbolName().toLowerCase() + "_" + + newMenuItemId.getSymbolName().toLowerCase() + "_label", + new JavaSymbolName(formBackingType.getSimpleTypeName()) + .getReadableSymbolName()); + + if (webScaffoldMetadata.getAnnotationValues().isCreate()) { + final String listPath = destinationDirectory + "/create.jspx"; + xmlRoundTripFileManager.writeToDiskIfNecessary(listPath, + viewManager.getCreateDocument()); + // Add 'create new' menu item + menuOperations.addMenuItem(categoryName, newMenuItemId, + "global_menu_new", "/" + controllerPath + "?form", + MenuOperations.DEFAULT_MENU_ITEM_PREFIX, webappPath); + tilesOperations.addViewDefinition(controllerPath, webappPath, + controllerPath + "/" + "create", + TilesOperations.DEFAULT_TEMPLATE, WEB_INF_VIEWS + + controllerPath + "/create.jspx"); + } + else { + menuOperations + .cleanUpMenuItem(categoryName, new JavaSymbolName("new"), + MenuOperations.DEFAULT_MENU_ITEM_PREFIX, webappPath); + tilesOperations.removeViewDefinition(controllerPath + "/" + + "create", controllerPath, webappPath); + } + if (webScaffoldMetadata.getAnnotationValues().isUpdate()) { + final String listPath = destinationDirectory + "/update.jspx"; + xmlRoundTripFileManager.writeToDiskIfNecessary(listPath, + viewManager.getUpdateDocument()); + tilesOperations.addViewDefinition(controllerPath, webappPath, + controllerPath + "/" + "update", + TilesOperations.DEFAULT_TEMPLATE, WEB_INF_VIEWS + + controllerPath + "/update.jspx"); + } + else { + tilesOperations.removeViewDefinition(controllerPath + "/" + + "update", controllerPath, webappPath); + } + + // Setup labels for i18n support + final String resourceId = XmlUtils.convertId("label." + + formBackingType.getFullyQualifiedTypeName().toLowerCase()); + properties.put(resourceId, + new JavaSymbolName(formBackingType.getSimpleTypeName()) + .getReadableSymbolName()); + + final String pluralResourceId = XmlUtils.convertId(resourceId + + ".plural"); + final String plural = formBackingTypeMetadataDetails.getPlural(); + properties.put(pluralResourceId, + new JavaSymbolName(plural).getReadableSymbolName()); + + final JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataDetails = formBackingTypeMetadataDetails + .getPersistenceDetails(); + Validate.notNull(javaTypePersistenceMetadataDetails, + "Unable to determine persistence metadata for type %s", + formBackingType.getFullyQualifiedTypeName()); + + if (!javaTypePersistenceMetadataDetails.getRooIdentifierFields() + .isEmpty()) { + for (final FieldMetadata idField : javaTypePersistenceMetadataDetails + .getRooIdentifierFields()) { + properties.put( + XmlUtils.convertId(resourceId + + "." + + javaTypePersistenceMetadataDetails + .getIdentifierField().getFieldName() + .getSymbolName() + + "." + + idField.getFieldName().getSymbolName() + .toLowerCase()), idField.getFieldName() + .getReadableSymbolName()); + } + } + + // If no auto generated value for id, put name in i18n + if (javaTypePersistenceMetadataDetails.getIdentifierField() + .getAnnotation(JpaJavaType.GENERATED_VALUE) == null) { + properties.put( + XmlUtils.convertId(resourceId + + "." + + javaTypePersistenceMetadataDetails + .getIdentifierField().getFieldName() + .getSymbolName().toLowerCase()), + javaTypePersistenceMetadataDetails.getIdentifierField() + .getFieldName().getReadableSymbolName()); + } + + for (final MethodMetadata method : memberDetails.getMethods()) { + if (!BeanInfoUtils.isAccessorMethod(method)) { + continue; + } + + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(memberDetails, method); + if (field == null) { + continue; + } + final JavaSymbolName fieldName = field.getFieldName(); + final String fieldResourceId = XmlUtils.convertId(resourceId + "." + + fieldName.getSymbolName().toLowerCase()); + if (typeLocationService.isInProject(method.getReturnType()) + && webMetadataService.isRooIdentifier(method + .getReturnType(), webMetadataService + .getMemberDetails(method.getReturnType()))) { + final JavaTypePersistenceMetadataDetails typePersistenceMetadataDetails = webMetadataService + .getJavaTypePersistenceMetadataDetails(method + .getReturnType(), webMetadataService + .getMemberDetails(method.getReturnType()), + jspMetadataId); + if (typePersistenceMetadataDetails != null) { + for (final FieldMetadata f : typePersistenceMetadataDetails + .getRooIdentifierFields()) { + final String sb = f.getFieldName() + .getReadableSymbolName(); + properties.put( + XmlUtils.convertId(resourceId + + "." + + javaTypePersistenceMetadataDetails + .getIdentifierField() + .getFieldName().getSymbolName() + + "." + + f.getFieldName().getSymbolName() + .toLowerCase()), + StringUtils.isNotBlank(sb) ? sb : fieldName + .getSymbolName()); + } + } + } + else if (!method.getMethodName().equals( + javaTypePersistenceMetadataDetails + .getIdentifierAccessorMethod().getMethodName()) + || javaTypePersistenceMetadataDetails + .getVersionAccessorMethod() != null + && !method + .getMethodName() + .equals(javaTypePersistenceMetadataDetails + .getVersionAccessorMethod().getMethodName())) { + final String sb = fieldName.getReadableSymbolName(); + properties.put(fieldResourceId, StringUtils.isNotBlank(sb) ? sb + : fieldName.getSymbolName()); + } + } + + if (javaTypePersistenceMetadataDetails.getFindAllMethod() != null) { + // Add 'list all' menu item + final JavaSymbolName listMenuItemId = new JavaSymbolName("list"); + menuOperations + .addMenuItem( + categoryName, + listMenuItemId, + "global_menu_list", + "/" + + controllerPath + + "?page=1&size=${empty param.size ? 10 : param.size}", + MenuOperations.DEFAULT_MENU_ITEM_PREFIX, webappPath); + properties.put("menu_item_" + + categoryName.getSymbolName().toLowerCase() + "_" + + listMenuItemId.getSymbolName().toLowerCase() + "_label", + new JavaSymbolName(plural).getReadableSymbolName()); + } + else { + menuOperations.cleanUpMenuItem(categoryName, new JavaSymbolName( + "list"), MenuOperations.DEFAULT_MENU_ITEM_PREFIX, + webappPath); + } + + final String controllerPhysicalTypeId = PhysicalTypeIdentifier + .createIdentifier(JspMetadata.getJavaType(jspMetadataId), + JspMetadata.getPath(jspMetadataId)); + final PhysicalTypeMetadata controllerPhysicalTypeMd = (PhysicalTypeMetadata) metadataService + .get(controllerPhysicalTypeId); + if (controllerPhysicalTypeMd == null) { + return null; + } + final MemberHoldingTypeDetails mhtd = controllerPhysicalTypeMd + .getMemberHoldingTypeDetails(); + if (mhtd == null) { + return null; + } + final List allowedMenuItems = new ArrayList(); + if (MemberFindingUtils.getAnnotationOfType(mhtd.getAnnotations(), + RooJavaType.ROO_WEB_FINDER) != null) { + // This controller is annotated with @RooWebFinder + final Set finderMethodsDetails = webMetadataService + .getDynamicFinderMethodsAndFields(formBackingType, + memberDetails, jspMetadataId); + if (finderMethodsDetails == null) { + return null; + } + for (final FinderMetadataDetails finderDetails : finderMethodsDetails) { + final String finderName = finderDetails + .getFinderMethodMetadata().getMethodName() + .getSymbolName(); + final String listPath = destinationDirectory + "/" + finderName + + ".jspx"; + // Finders only get scaffolded if the finder name is not too + // long (see ROO-1027) + if (listPath.length() > 244) { + continue; + } + xmlRoundTripFileManager.writeToDiskIfNecessary(listPath, + viewManager.getFinderDocument(finderDetails)); + final JavaSymbolName finderLabel = new JavaSymbolName( + finderName.replace("find" + plural + "By", "")); + + // Add 'Find by' menu item + menuOperations.addMenuItem(categoryName, finderLabel, + "global_menu_find", "/" + controllerPath + "?find=" + + finderName.replace("find" + plural, "") + + "&form" + + "&page=1&size=${empty param.size ? 10 : param.size}", + MenuOperations.FINDER_MENU_ITEM_PREFIX, webappPath); + properties.put("menu_item_" + + categoryName.getSymbolName().toLowerCase() + "_" + + finderLabel.getSymbolName().toLowerCase() + "_label", + finderLabel.getReadableSymbolName()); + allowedMenuItems.add(MenuOperations.FINDER_MENU_ITEM_PREFIX + + categoryName.getSymbolName().toLowerCase() + "_" + + finderLabel.getSymbolName().toLowerCase()); + for (final JavaSymbolName paramName : finderDetails + .getFinderMethodMetadata().getParameterNames()) { + properties.put( + XmlUtils.convertId(resourceId + "." + + paramName.getSymbolName().toLowerCase()), + paramName.getReadableSymbolName()); + } + tilesOperations.addViewDefinition(controllerPath, webappPath, + controllerPath + "/" + finderName, + TilesOperations.DEFAULT_TEMPLATE, WEB_INF_VIEWS + + controllerPath + "/" + finderName + ".jspx"); + } + } + + menuOperations.cleanUpFinderMenuItems(categoryName, allowedMenuItems, + webappPath); + + propFileOperations.addProperties(webappPath, + "WEB-INF/i18n/application.properties", properties, true, false); + + return new JspMetadata(jspMetadataId, webScaffoldMetadata); + } + + public String getProvidesType() { + return JspMetadata.getMetadataIdentiferType(); + } + + private void installImage(final LogicalPath path, final String imagePath) { + final PathResolver pathResolver = projectOperations.getPathResolver(); + final String imageFile = pathResolver.getIdentifier(path, imagePath); + if (!fileManager.exists(imageFile)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), imagePath); + outputStream = fileManager.createFile( + pathResolver.getIdentifier(path, imagePath)) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of resources for MVC JSP addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + public void notify(final String upstreamDependency, + String downstreamDependency) { + if (MetadataIdentificationUtils + .isIdentifyingClass(downstreamDependency)) { + // A physical Java type has changed, and determine what the + // corresponding local metadata identification string would have + // been + if (WebScaffoldMetadata.isValid(upstreamDependency)) { + final JavaType javaType = WebScaffoldMetadata + .getJavaType(upstreamDependency); + final LogicalPath path = WebScaffoldMetadata + .getPath(upstreamDependency); + downstreamDependency = JspMetadata.createIdentifier(javaType, + path); + } + else if (WebFinderMetadata.isValid(upstreamDependency)) { + final JavaType javaType = WebFinderMetadata + .getJavaType(upstreamDependency); + final LogicalPath path = WebFinderMetadata + .getPath(upstreamDependency); + downstreamDependency = JspMetadata.createIdentifier(javaType, + path); + } + + // We only need to proceed if the downstream dependency relationship + // is not already registered + // (if it's already registered, the event will be delivered directly + // later on) + if (metadataDependencyRegistry.getDownstream(upstreamDependency) + .contains(downstreamDependency)) { + return; + } + } + else if (MetadataIdentificationUtils + .isIdentifyingInstance(upstreamDependency)) { + // This is the generic fallback listener, ie from + // MetadataDependencyRegistry.addListener(this) in the activate() + // method + + // Get the metadata that just changed + final MetadataItem metadataItem = metadataService + .get(upstreamDependency); + + // We don't have to worry about physical type metadata, as we + // monitor the relevant .java once the DOD governor is first + // detected + if (metadataItem == null + || !metadataItem.isValid() + || !(metadataItem instanceof ItdTypeDetailsProvidingMetadataItem)) { + // There's something wrong with it or it's not for an ITD, so + // let's gracefully abort + return; + } + + // Let's ensure we have some ITD type details to actually work with + final ItdTypeDetailsProvidingMetadataItem itdMetadata = (ItdTypeDetailsProvidingMetadataItem) metadataItem; + final ItdTypeDetails itdTypeDetails = itdMetadata + .getMemberHoldingTypeDetails(); + if (itdTypeDetails == null) { + return; + } + + final String localMid = formBackingObjectTypesToLocalMids + .get(itdTypeDetails.getGovernor().getName()); + if (localMid != null) { + metadataService.evictAndGet(localMid); + } + return; + } + + if (MetadataIdentificationUtils + .isIdentifyingInstance(downstreamDependency)) { + metadataService.evictAndGet(downstreamDependency); + } + } +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspOperations.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspOperations.java new file mode 100644 index 000000000..7814fa409 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspOperations.java @@ -0,0 +1,96 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import org.springframework.roo.addon.web.mvc.jsp.i18n.I18n; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Feature; +import org.springframework.roo.project.LogicalPath; +import org.w3c.dom.Document; + +/** + * Provides operations to create various view layer resources. + * + * @author Stefan Schmidt + * @author Ben Alex + */ +public interface JspOperations extends Feature { + + /** + * Creates a new Spring MVC controller. + *

    + * Request mappings assigned by this method will always commence with "/" + * and end with "/**". You may present this prefix and/or this suffix if you + * wish, although it will automatically be added should it not be provided. + * + * @param controller the controller class to create (required) + * @param preferredMapping the mapping this controller should adopt + * (optional; if unspecified it will be based on the controller + * name) + * @param webappPath + */ + void createManualController(JavaType controller, String preferredMapping, + LogicalPath webappPath); + + /** + * Installs the common view artifacts needed for MVC scaffolding into the + * currently focused module. + */ + void installCommonViewArtefacts(); + + /** + * Installs all common view artifacts needed for MVC scaffolding into the + * given module. + * + * @param moduleName the name of the module into which to install the + * artifacts; can be empty for the root or only module + */ + void installCommonViewArtefacts(String moduleName); + + /** + * Installs additional languages into Web MVC app. + * + * @param language the language + * @param webappPath + */ + void installI18n(I18n language, LogicalPath webappPath); + + /** + * Installs a new Spring MVC static view. + * + * @param path the static view to create in (required, ie '/foo') + * @param viewName the mapping this view should adopt (required, ie 'index') + * @param title the title of the view (required) + * @param category the menu category name (required) + * @param document the jspx document to use for the view + * @param webappPath + */ + void installView(String path, String viewName, String title, + String category, Document document, LogicalPath webappPath); + + /** + * Creates a new Spring MVC static view. + * + * @param path the static view to create in (required, ie '/foo') + * @param title the title of the view (required) + * @param category the menu category name (required) + * @param viewName the mapping this view should adopt (required, ie 'index') + * @param webappPath + */ + void installView(String path, String viewName, String title, + String category, LogicalPath webappPath); + + boolean isControllerAvailable(); + + boolean isInstallLanguageCommandAvailable(); + + boolean isMvcInstallationPossible(); + + /** + * Replaces an existing tag library with the latest version (set backup flag + * to backup your application first) + * + * @param backup indicates wether your application should be backed up prior + * to replacing the tagx library + * @param webappPath + */ + void updateTags(boolean backup, LogicalPath webappPath); +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspOperationsImpl.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspOperationsImpl.java new file mode 100644 index 000000000..87d13f1b7 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspOperationsImpl.java @@ -0,0 +1,671 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import static org.springframework.roo.model.SpringJavaType.CONTROLLER; +import static org.springframework.roo.model.SpringJavaType.MODEL_MAP; +import static org.springframework.roo.model.SpringJavaType.PATH_VARIABLE; +import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING; +import static org.springframework.roo.model.SpringJavaType.REQUEST_METHOD; +import static org.springframework.roo.project.Path.SRC_MAIN_WEBAPP; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.backup.BackupOperations; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.addon.web.mvc.controller.WebMvcOperations; +import org.springframework.roo.addon.web.mvc.jsp.i18n.I18n; +import org.springframework.roo.addon.web.mvc.jsp.i18n.I18nSupport; +import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations; +import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperations; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.operations.AbstractOperations; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.osgi.BundleFindingUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.springframework.roo.uaa.UaaRegistrationService; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Implementation of {@link JspOperations}. + * + * @author Stefan Schmidt + * @author Jeremy Grelle + * @since 1.0 + */ +@Component +@Service +public class JspOperationsImpl extends AbstractOperations implements + JspOperations { + + private static final JavaType HTTP_SERVLET_REQUEST = new JavaType( + "javax.servlet.http.HttpServletRequest"); + private static final JavaType HTTP_SERVLET_RESPONSE = new JavaType( + "javax.servlet.http.HttpServletResponse"); + + /** + * Returns the folder name and mapping value for the given preferred maaping + * + * @param preferredMapping (can be blank) + * @param controller the associated controller type (required) + * @return a non-null pair + */ + static ImmutablePair getFolderAndMapping( + final String preferredMapping, final JavaType controller) { + if (StringUtils.isNotBlank(preferredMapping)) { + String folderName = StringUtils.removeStart(preferredMapping, "/"); + folderName = StringUtils.removeEnd(folderName, "**"); + folderName = StringUtils.removeEnd(folderName, "/"); + + String mapping = (preferredMapping.startsWith("/") ? "" : "/") + + preferredMapping; + mapping = StringUtils.removeEnd(mapping, "/"); + mapping = mapping + (mapping.endsWith("/**") ? "" : "/**"); + + return new ImmutablePair(folderName, mapping); + } + + // Use sensible defaults + final String typeNameLower = StringUtils.removeEnd( + controller.getSimpleTypeName(), "Controller").toLowerCase(); + return new ImmutablePair(typeNameLower, "/" + + typeNameLower + "/**"); + } + + @Reference private BackupOperations backupOperations; + @Reference private I18nSupport i18nSupport; + @Reference private MenuOperations menuOperations; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + @Reference private PropFileOperations propFileOperations; + @Reference private TilesOperations tilesOperations; + @Reference private TypeManagementService typeManagementService; + @Reference private UaaRegistrationService uaaRegistrationService; + @Reference private WebMvcOperations webMvcOperations; + + private String cleanPath(String path) { + if ("/".equals(path)) { + return ""; + } + path = "/" + path; + if (path.contains(".")) { + path = path.substring(0, path.indexOf(".") - 1); + } + return path; + } + + private String cleanViewName(String viewName) { + if (viewName.startsWith("/")) { + viewName = viewName.substring(1); + } + if (viewName.contains(".")) { + viewName = viewName.substring(0, viewName.indexOf(".") - 1); + } + return viewName; + } + + /** + * Creates a new Spring MVC controller. + *

    + * Request mappings assigned by this method will always commence with "/" + * and end with "/**". You may present this prefix and/or this suffix if you + * wish, although it will automatically be added should it not be provided. + * + * @param controller the controller class to create (required) + * @param preferredMapping the mapping this controller should adopt + * (optional; if unspecified it will be based on the controller + * name) + */ + public void createManualController(final JavaType controller, + final String preferredMapping, final LogicalPath webappPath) { + Validate.notNull(controller, "Controller Java Type required"); + + // Create annotation @RequestMapping("/myobject/**") + final ImmutablePair folderAndMapping = getFolderAndMapping( + preferredMapping, controller); + final String folderName = folderAndMapping.getKey(); + + final String resourceIdentifier = pathResolver.getFocusedCanonicalPath( + Path.SRC_MAIN_JAVA, controller); + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(controller, projectOperations + .getPathResolver().getPath(resourceIdentifier)); + final List methods = new ArrayList(); + + // Add HTTP post method + methods.add(getHttpPostMethod(declaredByMetadataId)); + + // Add index method + methods.add(getIndexMethod(folderName, declaredByMetadataId)); + + // Create Type definition + final List typeAnnotations = new ArrayList(); + + final List> requestMappingAttributes = new ArrayList>(); + requestMappingAttributes.add(new StringAttributeValue( + new JavaSymbolName("value"), folderAndMapping.getValue())); + final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder( + REQUEST_MAPPING, requestMappingAttributes); + typeAnnotations.add(requestMapping); + + // Create annotation @Controller + final List> controllerAttributes = new ArrayList>(); + final AnnotationMetadataBuilder controllerAnnotation = new AnnotationMetadataBuilder( + CONTROLLER, controllerAttributes); + typeAnnotations.add(controllerAnnotation); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, controller, + PhysicalTypeCategory.CLASS); + cidBuilder.setAnnotations(typeAnnotations); + cidBuilder.setDeclaredMethods(methods); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + + installView( + folderName, + "/index", + new JavaSymbolName(controller.getSimpleTypeName()) + .getReadableSymbolName() + " View", "Controller", null, + false, webappPath); + } + + private MethodMetadataBuilder getHttpPostMethod( + final String declaredByMetadataId) { + final List postMethodAnnotations = new ArrayList(); + final List> postMethodAttributes = new ArrayList>(); + postMethodAttributes.add(new EnumAttributeValue(new JavaSymbolName( + "method"), new EnumDetails(REQUEST_METHOD, new JavaSymbolName( + "POST")))); + postMethodAttributes.add(new StringAttributeValue(new JavaSymbolName( + "value"), "{id}")); + postMethodAnnotations.add(new AnnotationMetadataBuilder( + REQUEST_MAPPING, postMethodAttributes)); + + final List postParamTypes = new ArrayList(); + final AnnotationMetadataBuilder idParamAnnotation = new AnnotationMetadataBuilder( + PATH_VARIABLE); + postParamTypes.add(new AnnotatedJavaType( + new JavaType("java.lang.Long"), idParamAnnotation.build())); + postParamTypes.add(new AnnotatedJavaType(MODEL_MAP)); + postParamTypes.add(new AnnotatedJavaType(HTTP_SERVLET_REQUEST)); + postParamTypes.add(new AnnotatedJavaType(HTTP_SERVLET_RESPONSE)); + + final List postParamNames = new ArrayList(); + postParamNames.add(new JavaSymbolName("id")); + postParamNames.add(new JavaSymbolName("modelMap")); + postParamNames.add(new JavaSymbolName("request")); + postParamNames.add(new JavaSymbolName("response")); + + final MethodMetadataBuilder postMethodBuilder = new MethodMetadataBuilder( + declaredByMetadataId, Modifier.PUBLIC, new JavaSymbolName( + "post"), JavaType.VOID_PRIMITIVE, postParamTypes, + postParamNames, new InvocableMemberBodyBuilder()); + postMethodBuilder.setAnnotations(postMethodAnnotations); + return postMethodBuilder; + } + + private MethodMetadataBuilder getIndexMethod(final String folderName, + final String declaredByMetadataId) { + final List indexMethodAnnotations = new ArrayList(); + indexMethodAnnotations.add(new AnnotationMetadataBuilder( + REQUEST_MAPPING, new ArrayList>())); + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("return \"" + folderName + "/index\";"); + final MethodMetadataBuilder indexMethodBuilder = new MethodMetadataBuilder( + declaredByMetadataId, Modifier.PUBLIC, new JavaSymbolName( + "index"), JavaType.STRING, + new ArrayList(), + new ArrayList(), bodyBuilder); + indexMethodBuilder.setAnnotations(indexMethodAnnotations); + return indexMethodBuilder; + } + + public String getName() { + return FeatureNames.MVC; + } + + public void installCommonViewArtefacts() { + installCommonViewArtefacts(projectOperations.getFocusedModuleName()); + } + + public void installCommonViewArtefacts(final String moduleName) { + Validate.isTrue(isProjectAvailable(), "Project metadata required"); + final LogicalPath webappPath = Path.SRC_MAIN_WEBAPP + .getModulePathId(moduleName); + if (!isControllerAvailable()) { + webMvcOperations.installAllWebMvcArtifacts(); + } + + // Install tiles config + updateConfiguration(); + + // Install styles + copyDirectoryContents("images/*.*", + pathResolver.getIdentifier(webappPath, "images"), false); + + // Install styles + copyDirectoryContents("styles/*.css", + pathResolver.getIdentifier(webappPath, "styles"), false); + copyDirectoryContents("styles/*.properties", + pathResolver.getIdentifier(webappPath, "WEB-INF/classes"), + false); + + // Install layout + copyDirectoryContents("tiles/default.jspx", + pathResolver.getIdentifier(webappPath, "WEB-INF/layouts/"), + false); + copyDirectoryContents("tiles/layouts.xml", + pathResolver.getIdentifier(webappPath, "WEB-INF/layouts/"), + false); + copyDirectoryContents("tiles/header.jspx", + pathResolver.getIdentifier(webappPath, "WEB-INF/views/"), false); + copyDirectoryContents("tiles/menu.jspx", + pathResolver.getIdentifier(webappPath, "WEB-INF/views/"), false); + copyDirectoryContents("tiles/footer.jspx", + pathResolver.getIdentifier(webappPath, "WEB-INF/views/"), false); + copyDirectoryContents("tiles/views.xml", + pathResolver.getIdentifier(webappPath, "WEB-INF/views/"), false); + + // Install common view files + copyDirectoryContents("*.jspx", + pathResolver.getIdentifier(webappPath, "WEB-INF/views/"), false); + + // Install tags + copyDirectoryContents("tags/form/*.tagx", + pathResolver.getIdentifier(webappPath, "WEB-INF/tags/form"), + false); + copyDirectoryContents("tags/form/fields/*.tagx", + pathResolver.getIdentifier(webappPath, + "WEB-INF/tags/form/fields"), false); + copyDirectoryContents("tags/menu/*.tagx", + pathResolver.getIdentifier(webappPath, "WEB-INF/tags/menu"), + false); + copyDirectoryContents("tags/util/*.tagx", + pathResolver.getIdentifier(webappPath, "WEB-INF/tags/util"), + false); + + // Install default language 'en' + installI18n(i18nSupport.getLanguage(Locale.ENGLISH), webappPath); + + final String i18nDirectory = pathResolver.getIdentifier(webappPath, + "WEB-INF/i18n/application.properties"); + if (!fileManager.exists(i18nDirectory)) { + try { + final String projectName = projectOperations + .getProjectName(projectOperations + .getFocusedModuleName()); + fileManager.createFile(pathResolver.getIdentifier(webappPath, + "WEB-INF/i18n/application.properties")); + propFileOperations + .addPropertyIfNotExists(webappPath, + "WEB-INF/i18n/application.properties", + "application_name", + projectName.substring(0, 1).toUpperCase() + + projectName.substring(1), true); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of resources for MVC JSP addon.", + e); + } + } + } + + public void installI18n(final I18n i18n, final LogicalPath webappPath) { + Validate.notNull(i18n, "Language choice required"); + + if (i18n.getLocale() == null) { + LOGGER.warning("could not parse language choice"); + return; + } + + final String targetDirectory = pathResolver.getIdentifier(webappPath, + ""); + + // Install message bundle + String messageBundle = targetDirectory + "/WEB-INF/i18n/messages_" + + i18n.getLocale().getLanguage() /* + country */+ ".properties"; + // Special case for english locale (default) + if (i18n.getLocale().equals(Locale.ENGLISH)) { + messageBundle = targetDirectory + + "/WEB-INF/i18n/messages.properties"; + } + if (!fileManager.exists(messageBundle)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = i18n.getMessageBundle(); + outputStream = fileManager.createFile(messageBundle) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of message bundle MVC JSP addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + // Install flag + final String flagGraphic = targetDirectory + "/images/" + + i18n.getLocale().getLanguage() /* + country */+ ".png"; + if (!fileManager.exists(flagGraphic)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = i18n.getFlagGraphic(); + outputStream = fileManager.createFile(flagGraphic) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of flag graphic for MVC JSP addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + // Setup language definition in languages.jspx + final String footerFileLocation = targetDirectory + + "/WEB-INF/views/footer.jspx"; + final Document footer = XmlUtils.readXml(fileManager + .getInputStream(footerFileLocation)); + + if (XmlUtils.findFirstElement( + "//span[@id='language']/language[@locale='" + + i18n.getLocale().getLanguage() + "']", + footer.getDocumentElement()) == null) { + final Element span = XmlUtils.findRequiredElement( + "//span[@id='language']", footer.getDocumentElement()); + span.appendChild(new XmlElementBuilder("util:language", footer) + .addAttribute("locale", i18n.getLocale().getLanguage()) + .addAttribute("label", i18n.getLanguage()).build()); + fileManager.createOrUpdateTextFileIfRequired(footerFileLocation, + XmlUtils.nodeToString(footer), false); + } + + // Record use of add-on (most languages are implemented via public + // add-ons) + final String bundleSymbolicName = BundleFindingUtils + .findFirstBundleForTypeName(context, i18n + .getClass().getName()); + if (bundleSymbolicName != null) { + uaaRegistrationService.registerBundleSymbolicNameUse( + bundleSymbolicName, null); + } + } + + /** + * Creates a new Spring MVC static view. + * + * @param viewName the bare logical name of the new view (required, e.g. + * "index") + * @param folderName the folder in which to create the view; must be empty + * or start with a slash + * @param title the title + * @param category the menu category in which to list the new view + * (required) + * @param registerStaticController whether to register a static controller + * in the Spring MVC configuration file + */ + private void installView(final JavaSymbolName viewName, + final String folderName, final String title, final String category, + final boolean registerStaticController, final LogicalPath webappPath) { + // Probe if common web artifacts exist, and install them if needed + final PathResolver pathResolver = projectOperations.getPathResolver(); + if (!fileManager.exists(pathResolver.getIdentifier(webappPath, + "WEB-INF/layouts/default.jspx"))) { + installCommonViewArtefacts(webappPath.getModule()); + } + + final String lcViewName = viewName.getSymbolName().toLowerCase(); + + // Update the application-specific resource bundle (i.e. default + // translation) + final String messageCode = "label" + + folderName.replace("/", "_").toLowerCase() + "_" + lcViewName; + propFileOperations + .addPropertyIfNotExists( + pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP), + "WEB-INF/i18n/application.properties", messageCode, + title, true); + + // Add the menu item + final String relativeUrl = folderName + "/" + lcViewName; + menuOperations.addMenuItem(new JavaSymbolName(category), + new JavaSymbolName(folderName.replace("/", "_").toLowerCase() + + lcViewName + "_id"), title, "global_generic", + relativeUrl, null, webappPath); + + // Add the view definition + tilesOperations.addViewDefinition(folderName.toLowerCase(), + pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP), relativeUrl, + TilesOperations.DEFAULT_TEMPLATE, + "/WEB-INF/views" + folderName.toLowerCase() + "/" + lcViewName + + ".jspx"); + + if (registerStaticController) { + // Update the Spring MVC config file + registerStaticSpringMvcController(relativeUrl, webappPath); + } + } + + private void installView(final String path, final String viewName, + final String title, final String category, Document document, + final boolean registerStaticController, final LogicalPath webappPath) { + Validate.notBlank(path, "Path required"); + Validate.notBlank(viewName, "View name required"); + Validate.notBlank(title, "Title required"); + + final String cleanedPath = cleanPath(path); + final String cleanedViewName = cleanViewName(viewName); + final String lcViewName = cleanedViewName.toLowerCase(); + + if (document == null) { + try { + document = getDocumentTemplate("index-template.jspx"); + XmlUtils.findRequiredElement("/div/message", + document.getDocumentElement()).setAttribute( + "code", + "label" + cleanedPath.replace("/", "_").toLowerCase() + + "_" + lcViewName); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of resources for controller class.", + e); + } + } + + final String viewFile = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, + "WEB-INF/views" + cleanedPath.toLowerCase() + "/" + lcViewName + + ".jspx"); + fileManager.createOrUpdateTextFileIfRequired(viewFile, + XmlUtils.nodeToString(document), false); + + installView(new JavaSymbolName(lcViewName), cleanedPath, title, + category, registerStaticController, webappPath); + } + + public void installView(final String path, final String viewName, + final String title, final String category, final Document document, + final LogicalPath webappPath) { + installView(path, viewName, title, category, document, true, webappPath); + } + + public void installView(final String path, final String viewName, + final String title, final String category, + final LogicalPath webappPath) { + installView(path, viewName, title, category, null, true, webappPath); + } + + public boolean isControllerAvailable() { + return fileManager.exists(pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/views")) + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + public boolean isInstalledInModule(final String moduleName) { + final LogicalPath webAppPath = LogicalPath.getInstance( + Path.SRC_MAIN_WEBAPP, moduleName); + return fileManager.exists(projectOperations.getPathResolver() + .getIdentifier(webAppPath, "WEB-INF/spring/webmvc-config.xml")); + } + + public boolean isInstallLanguageCommandAvailable() { + return isProjectAvailable() + && fileManager.exists(pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, "WEB-INF/views/footer.jspx")); + } + + public boolean isMvcInstallationPossible() { + return isProjectAvailable() + && !isControllerAvailable() + && !projectOperations + .isFeatureInstalledInFocusedModule(FeatureNames.JSF); + } + + private boolean isProjectAvailable() { + return projectOperations.isFocusedProjectAvailable(); + } + + /** + * Registers a static Spring MVC controller to handle the given relative + * URL. + * + * @param relativeUrl the relative URL to handle (required); a leading slash + * will be added if required + */ + private void registerStaticSpringMvcController(final String relativeUrl, + final LogicalPath webappPath) { + final String mvcConfig = projectOperations.getPathResolver() + .getIdentifier(webappPath, "WEB-INF/spring/webmvc-config.xml"); + if (fileManager.exists(mvcConfig)) { + final Document document = XmlUtils.readXml(fileManager + .getInputStream(mvcConfig)); + final String prefixedUrl = "/" + relativeUrl; + if (XmlUtils.findFirstElement("/beans/view-controller[@path='" + + prefixedUrl + "']", document.getDocumentElement()) == null) { + final Element sibling = XmlUtils + .findFirstElement("/beans/view-controller", + document.getDocumentElement()); + final Element view = new XmlElementBuilder( + "mvc:view-controller", document).addAttribute("path", + prefixedUrl).build(); + if (sibling != null) { + sibling.getParentNode().insertBefore(view, sibling); + } + else { + document.getDocumentElement().appendChild(view); + } + fileManager.createOrUpdateTextFileIfRequired(mvcConfig, + XmlUtils.nodeToString(document), false); + } + } + } + + /** + * Adds Tiles Maven dependencies and updates the MVC config to include Tiles + * view support + */ + private void updateConfiguration() { + // Add tiles dependencies to pom + final Element configuration = XmlUtils.getRootElement(getClass(), + "tiles/configuration.xml"); + + final List dependencies = new ArrayList(); + final List springDependencies = XmlUtils.findElements( + "/configuration/tiles/dependencies/dependency", configuration); + for (final Element dependencyElement : springDependencies) { + dependencies.add(new Dependency(dependencyElement)); + } + projectOperations.addDependencies( + projectOperations.getFocusedModuleName(), dependencies); + + // Add config to MVC app context + final String mvcConfig = pathResolver.getFocusedIdentifier( + SRC_MAIN_WEBAPP, "WEB-INF/spring/webmvc-config.xml"); + final Document mvcConfigDocument = XmlUtils.readXml(fileManager + .getInputStream(mvcConfig)); + final Element beans = mvcConfigDocument.getDocumentElement(); + + if (XmlUtils.findFirstElement("/beans/bean[@id = 'tilesViewResolver']", + beans) != null + || XmlUtils.findFirstElement( + "/beans/bean[@id = 'tilesConfigurer']", beans) != null) { + return; // Tiles is already configured, nothing to do + } + + final Document configDoc = getDocumentTemplate("tiles/tiles-mvc-config-template.xml"); + final Element configElement = configDoc.getDocumentElement(); + final List tilesConfig = XmlUtils.findElements("/config/bean", + configElement); + for (final Element bean : tilesConfig) { + final Node importedBean = mvcConfigDocument.importNode(bean, true); + beans.appendChild(importedBean); + } + fileManager.createOrUpdateTextFileIfRequired(mvcConfig, + XmlUtils.nodeToString(mvcConfigDocument), true); + } + + public void updateTags(final boolean backup, final LogicalPath webappPath) { + if (backup) { + backupOperations.backup(); + } + + // Update tags + copyDirectoryContents("tags/form/*.tagx", + pathResolver.getIdentifier(webappPath, "WEB-INF/tags/form"), + true); + copyDirectoryContents("tags/form/fields/*.tagx", + pathResolver.getIdentifier(webappPath, + "WEB-INF/tags/form/fields"), true); + copyDirectoryContents("tags/menu/*.tagx", + pathResolver.getIdentifier(webappPath, "WEB-INF/tags/menu"), + true); + copyDirectoryContents("tags/util/*.tagx", + pathResolver.getIdentifier(webappPath, "WEB-INF/tags/util"), + true); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspViewManager.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspViewManager.java new file mode 100644 index 000000000..1215d651d --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/JspViewManager.java @@ -0,0 +1,1011 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import static org.springframework.roo.model.JavaType.BOOLEAN_OBJECT; +import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; +import static org.springframework.roo.model.JavaType.DOUBLE_OBJECT; +import static org.springframework.roo.model.JavaType.FLOAT_OBJECT; +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JavaType.SHORT_OBJECT; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.JdkJavaType.BIG_INTEGER; +import static org.springframework.roo.model.JdkJavaType.CALENDAR; +import static org.springframework.roo.model.JdkJavaType.DATE; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MAX; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MIN; +import static org.springframework.roo.model.Jsr303JavaType.FUTURE; +import static org.springframework.roo.model.Jsr303JavaType.MAX; +import static org.springframework.roo.model.Jsr303JavaType.MIN; +import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; +import static org.springframework.roo.model.Jsr303JavaType.PAST; +import static org.springframework.roo.model.Jsr303JavaType.PATTERN; +import static org.springframework.roo.model.Jsr303JavaType.SIZE; + +import java.beans.Introspector; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.addon.web.mvc.controller.details.FinderMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypeMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.details.JavaTypePersistenceMetadataDetails; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldAnnotationValues; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JpaJavaType; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Helper class which generates the contents of the various jsp documents + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class JspViewManager { + + private static final String CREATED = "created"; + private static final JavaSymbolName VALUE = new JavaSymbolName("value"); + private final String controllerPath; + private final String entityName; + private final List fields; + private final JavaType formBackingType; + private final JavaTypeMetadataDetails formBackingTypeMetadata; + private final JavaTypePersistenceMetadataDetails formBackingTypePersistenceMetadata; + private final Map relatedDomainTypes; + private final WebScaffoldAnnotationValues webScaffoldAnnotationValues; + + /** + * Constructor + * + * @param fields can't be null + * @param webScaffoldAnnotationValues can't be null + * @param relatedDomainTypes can't be null + */ + public JspViewManager(final List fields, + final WebScaffoldAnnotationValues webScaffoldAnnotationValues, + final Map relatedDomainTypes) { + Validate.notNull(fields, "List of fields required"); + Validate.notNull(webScaffoldAnnotationValues, + "Web scaffold annotation values required"); + Validate.notNull(relatedDomainTypes, "Related domain types required"); + this.fields = Collections.unmodifiableList(fields); + this.webScaffoldAnnotationValues = webScaffoldAnnotationValues; + formBackingType = webScaffoldAnnotationValues.getFormBackingObject(); + this.relatedDomainTypes = relatedDomainTypes; + entityName = JavaSymbolName.getReservedWordSafeName(formBackingType) + .getSymbolName(); + formBackingTypeMetadata = relatedDomainTypes.get(formBackingType); + Validate.notNull(formBackingTypeMetadata, + "Form backing type metadata required"); + formBackingTypePersistenceMetadata = formBackingTypeMetadata + .getPersistenceDetails(); + Validate.notNull(formBackingTypePersistenceMetadata, + "Persistence metadata required for form backing type"); + Validate.notNull( + webScaffoldAnnotationValues.getPath(), + "Path is not specified in the @RooWebScaffold annotation for '%s'", + webScaffoldAnnotationValues.getGovernorTypeDetails().getName()); + + if (webScaffoldAnnotationValues.getPath().startsWith("/")) { + controllerPath = webScaffoldAnnotationValues.getPath(); + } + else { + controllerPath = "/" + webScaffoldAnnotationValues.getPath(); + } + } + + private void addCommonAttributes(final FieldMetadata field, + final Element fieldElement) { + AnnotationMetadata annotationMetadata; + if (field.getFieldType().equals(INT_OBJECT) + || field.getFieldType().getFullyQualifiedTypeName() + .equals(int.class.getName()) + || field.getFieldType().equals(SHORT_OBJECT) + || field.getFieldType().getFullyQualifiedTypeName() + .equals(short.class.getName()) + || field.getFieldType().equals(LONG_OBJECT) + || field.getFieldType().getFullyQualifiedTypeName() + .equals(long.class.getName()) + || field.getFieldType().equals(BIG_INTEGER)) { + fieldElement.setAttribute("validationMessageCode", + "field_invalid_integer"); + } + else if (isEmailField(field)) { + fieldElement.setAttribute("validationMessageCode", + "field_invalid_email"); + } + else if (field.getFieldType().equals(DOUBLE_OBJECT) + || field.getFieldType().getFullyQualifiedTypeName() + .equals(double.class.getName()) + || field.getFieldType().equals(FLOAT_OBJECT) + || field.getFieldType().getFullyQualifiedTypeName() + .equals(float.class.getName()) + || field.getFieldType().equals(BIG_DECIMAL)) { + fieldElement.setAttribute("validationMessageCode", + "field_invalid_number"); + } + if ("field:input".equals(fieldElement.getTagName()) + && null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), MIN))) { + final AnnotationAttributeValue min = annotationMetadata + .getAttribute(VALUE); + if (min != null) { + fieldElement.setAttribute("min", min.getValue().toString()); + fieldElement.setAttribute("required", "true"); + } + } + if ("field:input".equals(fieldElement.getTagName()) + && null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), MAX)) + && !"field:textarea".equals(fieldElement.getTagName())) { + final AnnotationAttributeValue maxA = annotationMetadata + .getAttribute(VALUE); + if (maxA != null) { + fieldElement.setAttribute("max", maxA.getValue().toString()); + } + } + if ("field:input".equals(fieldElement.getTagName()) + && null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), + DECIMAL_MIN)) + && !"field:textarea".equals(fieldElement.getTagName())) { + final AnnotationAttributeValue decimalMin = annotationMetadata + .getAttribute(VALUE); + if (decimalMin != null) { + fieldElement.setAttribute("decimalMin", decimalMin.getValue() + .toString()); + fieldElement.setAttribute("required", "true"); + } + } + if ("field:input".equals(fieldElement.getTagName()) + && null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), + DECIMAL_MAX))) { + final AnnotationAttributeValue decimalMax = annotationMetadata + .getAttribute(VALUE); + if (decimalMax != null) { + fieldElement.setAttribute("decimalMax", decimalMax.getValue() + .toString()); + } + } + if (null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), PATTERN))) { + final AnnotationAttributeValue regexp = annotationMetadata + .getAttribute(new JavaSymbolName("regexp")); + if (regexp != null) { + fieldElement.setAttribute("validationRegex", regexp.getValue() + .toString()); + } + } + if ("field:input".equals(fieldElement.getTagName()) + && null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), SIZE))) { + final AnnotationAttributeValue max = annotationMetadata + .getAttribute(new JavaSymbolName("max")); + if (max != null) { + fieldElement.setAttribute("max", max.getValue().toString()); + } + final AnnotationAttributeValue min = annotationMetadata + .getAttribute(new JavaSymbolName("min")); + if (min != null) { + fieldElement.setAttribute("min", min.getValue().toString()); + fieldElement.setAttribute("required", "true"); + } + } + if (null != (annotationMetadata = MemberFindingUtils + .getAnnotationOfType(field.getAnnotations(), NOT_NULL))) { + final String tagName = fieldElement.getTagName(); + if (tagName.endsWith("textarea") || tagName.endsWith("input") + || tagName.endsWith("datetime") + || tagName.endsWith("textarea") + || tagName.endsWith("select") + || tagName.endsWith("reference")) { + fieldElement.setAttribute("required", "true"); + } + } + if (field.getCustomData().keySet() + .contains(CustomDataKeys.COLUMN_FIELD)) { + @SuppressWarnings("unchecked") + final Map values = (Map) field + .getCustomData().get(CustomDataKeys.COLUMN_FIELD); + if (values.keySet().contains("nullable") + && (Boolean) values.get("nullable") == false) { + fieldElement.setAttribute("required", "true"); + } + } + // Disable form binding for nested fields (mainly PKs) + if (field.getFieldName().getSymbolName().contains(".")) { + fieldElement.setAttribute("disableFormBinding", "true"); + } + } + + private void createFieldsForCreateAndUpdate( + final List formFields, final Document document, + final Element root, final boolean isCreate) { + for (final FieldMetadata field : formFields) { + final String fieldName = field.getFieldName().getSymbolName(); + JavaType fieldType = field.getFieldType(); + AnnotationMetadata annotationMetadata; + + // Ignoring java.util.Map field types (see ROO-194) + if (fieldType.equals(new JavaType(Map.class.getName()))) { + continue; + } + // Fields contained in the embedded Id type have been added + // separately to the field list + if (field.getCustomData().keySet() + .contains(CustomDataKeys.EMBEDDED_ID_FIELD)) { + continue; + } + + fieldType = getJavaTypeForField(field); + + final JavaTypeMetadataDetails typeMetadataHolder = relatedDomainTypes + .get(fieldType); + JavaTypePersistenceMetadataDetails typePersistenceMetadataHolder = null; + if (typeMetadataHolder != null) { + typePersistenceMetadataHolder = typeMetadataHolder + .getPersistenceDetails(); + } + + Element fieldElement = null; + + if (fieldType.getFullyQualifiedTypeName().equals( + Boolean.class.getName()) + || fieldType.getFullyQualifiedTypeName().equals( + boolean.class.getName())) { + fieldElement = document.createElement("field:checkbox"); + // Handle enum fields + } + else if (typeMetadataHolder != null + && typeMetadataHolder.isEnumType()) { + fieldElement = new XmlElementBuilder("field:select", document) + .addAttribute( + "items", + "${" + + typeMetadataHolder.getPlural() + .toLowerCase() + "}") + .addAttribute("path", getPathForType(fieldType)) + .build(); + } + else if (field.getCustomData().keySet() + .contains(CustomDataKeys.ONE_TO_MANY_FIELD)) { + // OneToMany relationships are managed from the 'many' side of + // the relationship, therefore we provide a link to the relevant + // form the link URL is determined as a best effort attempt + // following Roo REST conventions, this link might be wrong if + // custom paths are used if custom paths are used the developer + // can adjust the path attribute in the field:reference tag + // accordingly + if (typePersistenceMetadataHolder != null) { + fieldElement = new XmlElementBuilder("field:simple", + document) + .addAttribute("messageCode", + "entity_reference_not_managed") + .addAttribute( + "messageCodeAttribute", + new JavaSymbolName(fieldType + .getSimpleTypeName()) + .getReadableSymbolName()).build(); + } + else { + continue; + } + } + else if (field.getCustomData().keySet() + .contains(CustomDataKeys.MANY_TO_ONE_FIELD) + || field.getCustomData().keySet() + .contains(CustomDataKeys.MANY_TO_MANY_FIELD) + || field.getCustomData().keySet() + .contains(CustomDataKeys.ONE_TO_ONE_FIELD)) { + final JavaType referenceType = getJavaTypeForField(field); + final JavaTypeMetadataDetails referenceTypeMetadata = relatedDomainTypes + .get(referenceType); + if (referenceType != null && referenceTypeMetadata != null + && referenceTypeMetadata.isApplicationType() + && typePersistenceMetadataHolder != null) { + fieldElement = new XmlElementBuilder("field:select", + document) + .addAttribute( + "items", + "${" + + referenceTypeMetadata.getPlural() + .toLowerCase() + "}") + .addAttribute( + "itemValue", + typePersistenceMetadataHolder + .getIdentifierField() + .getFieldName().getSymbolName()) + .addAttribute( + "path", + "/" + + getPathForType(getJavaTypeForField(field))) + .build(); + if (field.getCustomData().keySet() + .contains(CustomDataKeys.MANY_TO_MANY_FIELD)) { + fieldElement.setAttribute("multiple", "true"); + } + } + } + else if (fieldType.equals(DATE) || fieldType.equals(CALENDAR)) { + if (fieldName.equals(CREATED)) { + continue; + } + // Only include the date picker for styles supported by Dojo + // (SMALL & MEDIUM) + fieldElement = new XmlElementBuilder("field:datetime", document) + .addAttribute( + "dateTimePattern", + "${" + entityName + "_" + + fieldName.toLowerCase() + + "_date_format}").build(); + if (null != MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), FUTURE)) { + fieldElement.setAttribute("future", "true"); + } + else if (null != MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), PAST)) { + fieldElement.setAttribute("past", "true"); + } + } + else if (field.getCustomData().keySet() + .contains(CustomDataKeys.LOB_FIELD)) { + fieldElement = new XmlElementBuilder("field:textarea", document) + .build(); + } + if ((annotationMetadata = MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), SIZE)) != null) { + final AnnotationAttributeValue max = annotationMetadata + .getAttribute(new JavaSymbolName("max")); + if (max != null) { + final int maxValue = (Integer) max.getValue(); + if (fieldElement == null && maxValue > 30) { + fieldElement = new XmlElementBuilder("field:textarea", + document).build(); + } + } + } + // Use a default input field if no other criteria apply + if (fieldElement == null) { + fieldElement = document.createElement("field:input"); + } + addCommonAttributes(field, fieldElement); + fieldElement.setAttribute("field", fieldName); + fieldElement.setAttribute( + "id", + XmlUtils.convertId("c:" + + formBackingType.getFullyQualifiedTypeName() + "." + + field.getFieldName().getSymbolName())); + + // If identifier manually assigned, then add 'required=true' + if (formBackingTypePersistenceMetadata.getIdentifierField() + .getFieldName().equals(field.getFieldName()) + && field.getAnnotation(JpaJavaType.GENERATED_VALUE) == null) { + fieldElement.setAttribute("required", "true"); + } + + fieldElement.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(fieldElement)); + + root.appendChild(fieldElement); + } + } + + public Document getCreateDocument() { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + + // Add document namespaces + final Element div = (Element) document + .appendChild(new XmlElementBuilder("div", document) + .addAttribute("xmlns:form", + "urn:jsptagdir:/WEB-INF/tags/form") + .addAttribute("xmlns:field", + "urn:jsptagdir:/WEB-INF/tags/form/fields") + .addAttribute("xmlns:jsp", + "http://java.sun.com/JSP/Page") + .addAttribute("xmlns:c", + "http://java.sun.com/jsp/jstl/core") + .addAttribute("xmlns:spring", + "http://www.springframework.org/tags") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:directive.page", + document).addAttribute("contentType", + "text/html;charset=UTF-8").build()) + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", + "yes").build()).build()); + + // Add form create element + final Element formCreate = new XmlElementBuilder("form:create", + document) + .addAttribute( + "id", + XmlUtils.convertId("fc:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute("modelAttribute", entityName) + .addAttribute("path", controllerPath) + .addAttribute("render", "${empty dependencies}").build(); + + if (!controllerPath.equalsIgnoreCase(formBackingType + .getSimpleTypeName())) { + formCreate.setAttribute("path", controllerPath); + } + + final List formFields = new ArrayList(); + final List fieldCopy = new ArrayList( + fields); + + // Handle Roo identifiers + if (!formBackingTypePersistenceMetadata.getRooIdentifierFields() + .isEmpty()) { + final String identifierFieldName = formBackingTypePersistenceMetadata + .getIdentifierField().getFieldName().getSymbolName(); + formCreate.setAttribute("compositePkField", identifierFieldName); + for (final FieldMetadata embeddedField : formBackingTypePersistenceMetadata + .getRooIdentifierFields()) { + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + embeddedField); + fieldBuilder + .setFieldName(new JavaSymbolName(identifierFieldName + + "." + + embeddedField.getFieldName().getSymbolName())); + for (int i = 0; i < fieldCopy.size(); i++) { + // Make sure form fields are not presented twice. + if (fieldCopy.get(i).getFieldName() + .equals(embeddedField.getFieldName())) { + fieldCopy.remove(i); + break; + } + } + formFields.add(fieldBuilder.build()); + } + } + formFields.addAll(fieldCopy); + + // If identifier manually assigned, show it in creation + if (formBackingTypePersistenceMetadata.getIdentifierField() + .getAnnotation(JpaJavaType.GENERATED_VALUE) == null) { + + formFields.add(formBackingTypePersistenceMetadata + .getIdentifierField()); + } + + createFieldsForCreateAndUpdate(formFields, document, formCreate, true); + formCreate.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(formCreate)); + + final Element dependency = new XmlElementBuilder("form:dependency", + document) + .addAttribute( + "id", + XmlUtils.convertId("d:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute("render", "${not empty dependencies}") + .addAttribute("dependencies", "${dependencies}").build(); + dependency.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(dependency)); + + div.appendChild(formCreate); + div.appendChild(dependency); + + return document; + } + + public Document getFinderDocument( + final FinderMetadataDetails finderMetadataDetails) { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + + // Add document namespaces + final Element div = (Element) document + .appendChild(new XmlElementBuilder("div", document) + .addAttribute("xmlns:form", + "urn:jsptagdir:/WEB-INF/tags/form") + .addAttribute("xmlns:field", + "urn:jsptagdir:/WEB-INF/tags/form/fields") + .addAttribute("xmlns:jsp", + "http://java.sun.com/JSP/Page") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:directive.page", + document).addAttribute("contentType", + "text/html;charset=UTF-8").build()) + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", + "yes").build()).build()); + + final Element formFind = new XmlElementBuilder("form:find", document) + .addAttribute( + "id", + XmlUtils.convertId("ff:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute("path", controllerPath) + .addAttribute( + "finderName", + finderMetadataDetails + .getFinderMethodMetadata() + .getMethodName() + .getSymbolName() + .replace( + "find" + + formBackingTypeMetadata + .getPlural(), "")) + .build(); + formFind.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(formFind)); + div.appendChild(formFind); + + for (final FieldMetadata field : finderMetadataDetails + .getFinderMethodParamFields()) { + final JavaType type = field.getFieldType(); + final JavaSymbolName paramName = field.getFieldName(); + JavaSymbolName fieldName = null; + if (paramName.getSymbolName().startsWith("max") + || paramName.getSymbolName() + .startsWith("min")) { + fieldName = new JavaSymbolName( + Introspector.decapitalize(StringUtils + .capitalize(paramName + .getSymbolName().substring(3)))); + } + else { + fieldName = paramName; + } + + + // Ignoring java.util.Map field types (see ROO-194) + if (type.equals(new JavaType(Map.class.getName()))) { + continue; + } + Validate.notNull(paramName, "Could not find field '%s' in '%s'", + paramName, type.getFullyQualifiedTypeName()); + Element fieldElement = null; + + final JavaTypeMetadataDetails typeMetadataHolder = relatedDomainTypes + .get(getJavaTypeForField(field)); + + if (type.isCommonCollectionType() + && relatedDomainTypes + .containsKey(getJavaTypeForField(field))) { + final JavaTypeMetadataDetails collectionTypeMetadataHolder = relatedDomainTypes + .get(getJavaTypeForField(field)); + final JavaTypePersistenceMetadataDetails typePersistenceMetadataHolder = collectionTypeMetadataHolder + .getPersistenceDetails(); + if (typePersistenceMetadataHolder != null) { + fieldElement = new XmlElementBuilder("field:select", + document) + .addAttribute("required", "true") + .addAttribute( + "items", + "${" + + collectionTypeMetadataHolder + .getPlural().toLowerCase() + + "}") + .addAttribute( + "itemValue", + typePersistenceMetadataHolder + .getIdentifierField() + .getFieldName().getSymbolName()) + .addAttribute( + "path", + "/" + + getPathForType(getJavaTypeForField(field))) + .build(); + if (field.getCustomData().keySet() + .contains(CustomDataKeys.MANY_TO_MANY_FIELD)) { + fieldElement.setAttribute("multiple", "true"); + } + } + } + else if (typeMetadataHolder != null + && typeMetadataHolder.isEnumType() + && field.getCustomData().keySet() + .contains(CustomDataKeys.ENUMERATED_FIELD)) { + fieldElement = new XmlElementBuilder("field:select", document) + .addAttribute("required", "true") + .addAttribute( + "items", + "${" + + typeMetadataHolder.getPlural() + .toLowerCase() + "}") + .addAttribute("path", "/" + getPathForType(type)) + .build(); + } + else if (type.equals(BOOLEAN_OBJECT) + || type.equals(BOOLEAN_PRIMITIVE)) { + fieldElement = document.createElement("field:checkbox"); + } + else if (typeMetadataHolder != null + && typeMetadataHolder.isApplicationType()) { + final JavaTypePersistenceMetadataDetails typePersistenceMetadataHolder = typeMetadataHolder + .getPersistenceDetails(); + if (typePersistenceMetadataHolder != null) { + fieldElement = new XmlElementBuilder("field:select", + document) + .addAttribute("required", "true") + .addAttribute( + "items", + "${" + + typeMetadataHolder.getPlural() + .toLowerCase() + "}") + .addAttribute( + "itemValue", + typePersistenceMetadataHolder + .getIdentifierField() + .getFieldName().getSymbolName()) + .addAttribute("path", "/" + getPathForType(type)) + .build(); + } + } + else if (type.equals(DATE) || type.equals(CALENDAR)) { + fieldElement = new XmlElementBuilder("field:datetime", document) + .addAttribute("required", "true") + .addAttribute( + "dateTimePattern", + "${" + + entityName + + "_" + + fieldName.getSymbolName() + .toLowerCase() + + "_date_format}").build(); + } + if (fieldElement == null) { + fieldElement = new XmlElementBuilder("field:input", document) + .addAttribute("required", "true").build(); + } + addCommonAttributes(field, fieldElement); + fieldElement.setAttribute("disableFormBinding", "true"); + fieldElement.setAttribute("field", paramName.getSymbolName()); + fieldElement.setAttribute( + "id", + XmlUtils.convertId("f:" + + formBackingType.getFullyQualifiedTypeName() + "." + + paramName)); + fieldElement.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(fieldElement)); + formFind.appendChild(fieldElement); + } + + DomUtils.removeTextNodes(document); + return document; + } + + private JavaType getJavaTypeForField(final FieldMetadata field) { + if (field.getFieldType().isCommonCollectionType()) { + // Currently there is no scaffolding available for Maps (see + // ROO-194) + if (field.getFieldType().equals(new JavaType(Map.class.getName()))) { + return null; + } + final List parameters = field.getFieldType() + .getParameters(); + if (parameters.isEmpty()) { + throw new IllegalStateException( + "Unable to determine the parameter type for the " + + field.getFieldName().getSymbolName() + + " field in " + + formBackingType.getSimpleTypeName()); + } + return parameters.get(0); + } + return field.getFieldType(); + } + + public Document getListDocument() { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + + // Add document namespaces + final Element div = new XmlElementBuilder("div", document) + .addAttribute("xmlns:page", "urn:jsptagdir:/WEB-INF/tags/form") + .addAttribute("xmlns:table", + "urn:jsptagdir:/WEB-INF/tags/form/fields") + .addAttribute("xmlns:jsp", "http://java.sun.com/JSP/Page") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:directive.page", document) + .addAttribute("contentType", + "text/html;charset=UTF-8").build()) + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", "yes") + .build()).build(); + document.appendChild(div); + + final Element fieldTable = new XmlElementBuilder("table:table", + document) + .addAttribute( + "id", + XmlUtils.convertId("l:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute( + "data", + "${" + + formBackingTypeMetadata.getPlural() + .toLowerCase() + "}") + .addAttribute("path", controllerPath).build(); + + if (!webScaffoldAnnotationValues.isUpdate()) { + fieldTable.setAttribute("update", "false"); + } + if (!webScaffoldAnnotationValues.isDelete()) { + fieldTable.setAttribute("delete", "false"); + } + if (!formBackingTypePersistenceMetadata.getIdentifierField() + .getFieldName().getSymbolName().equals("id")) { + fieldTable.setAttribute("typeIdFieldName", + formBackingTypePersistenceMetadata.getIdentifierField() + .getFieldName().getSymbolName()); + } + fieldTable.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(fieldTable)); + + int fieldCounter = 0; + for (final FieldMetadata field : fields) { + if (++fieldCounter < 7) { + final Element columnElement = new XmlElementBuilder( + "table:column", document) + .addAttribute( + "id", + XmlUtils.convertId("c:" + + formBackingType + .getFullyQualifiedTypeName() + + "." + + field.getFieldName().getSymbolName())) + .addAttribute( + "property", + uncapitalize(field.getFieldName() + .getSymbolName())).build(); + final String fieldName = uncapitalize(field.getFieldName() + .getSymbolName()); + if (field.getFieldType().equals(DATE)) { + columnElement.setAttribute("date", "true"); + columnElement.setAttribute("dateTimePattern", "${" + + entityName + "_" + fieldName.toLowerCase() + + "_date_format}"); + } + else if (field.getFieldType().equals(CALENDAR)) { + columnElement.setAttribute("calendar", "true"); + columnElement.setAttribute("dateTimePattern", "${" + + entityName + "_" + fieldName.toLowerCase() + + "_date_format}"); + } + else if (field.getFieldType().isCommonCollectionType() + && field.getCustomData().get( + CustomDataKeys.ONE_TO_MANY_FIELD) != null) { + continue; + } + columnElement.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(columnElement)); + fieldTable.appendChild(columnElement); + } + } + + // Create page:list element + final Element pageList = new XmlElementBuilder("page:list", document) + .addAttribute( + "id", + XmlUtils.convertId("pl:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute( + "items", + "${" + + formBackingTypeMetadata.getPlural() + .toLowerCase() + "}") + .addChild(fieldTable).build(); + pageList.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(pageList)); + div.appendChild(pageList); + + return document; + } + + private String getPathForType(final JavaType type) { + final JavaTypeMetadataDetails javaTypeMetadataHolder = relatedDomainTypes + .get(type); + Validate.notNull(javaTypeMetadataHolder, + "Unable to obtain metadata for type %s", + type.getFullyQualifiedTypeName()); + return javaTypeMetadataHolder.getControllerPath(); + } + + public Document getShowDocument() { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + + // Add document namespaces + final Element div = (Element) document + .appendChild(new XmlElementBuilder("div", document) + .addAttribute("xmlns:page", + "urn:jsptagdir:/WEB-INF/tags/form") + .addAttribute("xmlns:field", + "urn:jsptagdir:/WEB-INF/tags/form/fields") + .addAttribute("xmlns:jsp", + "http://java.sun.com/JSP/Page") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:directive.page", + document).addAttribute("contentType", + "text/html;charset=UTF-8").build()) + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", + "yes").build()).build()); + + final Element pageShow = new XmlElementBuilder("page:show", document) + .addAttribute( + "id", + XmlUtils.convertId("ps:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute("object", "${" + entityName.toLowerCase() + "}") + .addAttribute("path", controllerPath).build(); + if (!webScaffoldAnnotationValues.isCreate()) { + pageShow.setAttribute("create", "false"); + } + if (!webScaffoldAnnotationValues.isUpdate()) { + pageShow.setAttribute("update", "false"); + } + if (!webScaffoldAnnotationValues.isDelete()) { + pageShow.setAttribute("delete", "false"); + } + pageShow.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(pageShow)); + + // Add field:display elements for each field + for (final FieldMetadata field : fields) { + // Ignoring java.util.Map field types (see ROO-194) + if (field.getFieldType().equals(new JavaType(Map.class.getName()))) { + continue; + } + final String fieldName = uncapitalize(field.getFieldName() + .getSymbolName()); + final Element fieldDisplay = new XmlElementBuilder("field:display", + document) + .addAttribute( + "id", + XmlUtils.convertId("s:" + + formBackingType + .getFullyQualifiedTypeName() + "." + + field.getFieldName().getSymbolName())) + .addAttribute("object", + "${" + entityName.toLowerCase() + "}") + .addAttribute("field", fieldName).build(); + if (field.getFieldType().equals(DATE)) { + if (fieldName.equals(CREATED)) { + continue; + } + fieldDisplay.setAttribute("date", "true"); + fieldDisplay.setAttribute("dateTimePattern", "${" + entityName + + "_" + fieldName.toLowerCase() + "_date_format}"); + } + else if (field.getFieldType().equals(CALENDAR)) { + fieldDisplay.setAttribute("calendar", "true"); + fieldDisplay.setAttribute("dateTimePattern", "${" + entityName + + "_" + fieldName.toLowerCase() + "_date_format}"); + } + else if (field.getFieldType().isCommonCollectionType() + && field.getCustomData().get( + CustomDataKeys.ONE_TO_MANY_FIELD) != null) { + continue; + } + fieldDisplay.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(fieldDisplay)); + + pageShow.appendChild(fieldDisplay); + } + div.appendChild(pageShow); + + return document; + } + + public Document getUpdateDocument() { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + + // Add document namespaces + final Element div = (Element) document + .appendChild(new XmlElementBuilder("div", document) + .addAttribute("xmlns:form", + "urn:jsptagdir:/WEB-INF/tags/form") + .addAttribute("xmlns:field", + "urn:jsptagdir:/WEB-INF/tags/form/fields") + .addAttribute("xmlns:jsp", + "http://java.sun.com/JSP/Page") + .addAttribute("version", "2.0") + .addChild( + new XmlElementBuilder("jsp:directive.page", + document).addAttribute("contentType", + "text/html;charset=UTF-8").build()) + .addChild( + new XmlElementBuilder("jsp:output", document) + .addAttribute("omit-xml-declaration", + "yes").build()).build()); + + // Add form update element + final Element formUpdate = new XmlElementBuilder("form:update", + document) + .addAttribute( + "id", + XmlUtils.convertId("fu:" + + formBackingType.getFullyQualifiedTypeName())) + .addAttribute("modelAttribute", entityName).build(); + + if (!controllerPath.equalsIgnoreCase(formBackingType + .getSimpleTypeName())) { + formUpdate.setAttribute("path", controllerPath); + } + if (!"id".equals(formBackingTypePersistenceMetadata + .getIdentifierField().getFieldName().getSymbolName())) { + formUpdate.setAttribute("idField", + formBackingTypePersistenceMetadata.getIdentifierField() + .getFieldName().getSymbolName()); + } + final MethodMetadata versionAccessorMethod = formBackingTypePersistenceMetadata + .getVersionAccessorMethod(); + if (versionAccessorMethod == null) { + formUpdate.setAttribute("versionField", "none"); + } + else { + final String methodName = versionAccessorMethod.getMethodName() + .getSymbolName(); + formUpdate.setAttribute("versionField", + methodName.substring("get".length())); + } + + // Filter out embedded ID fields as they represent the composite PK + // which is not to be updated. + final List fieldCopy = new ArrayList( + fields); + for (final FieldMetadata embeddedField : formBackingTypePersistenceMetadata + .getRooIdentifierFields()) { + for (int i = 0; i < fieldCopy.size(); i++) { + // Make sure form fields are not presented twice. + if (fieldCopy.get(i).getFieldName() + .equals(embeddedField.getFieldName())) { + fieldCopy.remove(i); + } + } + } + + createFieldsForCreateAndUpdate(fieldCopy, document, formUpdate, false); + formUpdate.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(formUpdate)); + div.appendChild(formUpdate); + + return document; + } + + private boolean isEmailField(final FieldMetadata field) { + return STRING.equals(field.getFieldType()) + && uncapitalize(field.getFieldName().getSymbolName()).contains( + "email"); + } + + private String uncapitalize(final String term) { + // [ROO-1790] this is needed to adhere to the JavaBean naming + // conventions (see JavaBean spec section 8.8) + return Introspector.decapitalize(StringUtils.capitalize(term)); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/AbstractLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/AbstractLanguage.java new file mode 100644 index 000000000..f511030bf --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/AbstractLanguage.java @@ -0,0 +1,61 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n; + +import java.util.Locale; + +/** + * Convenience class for I18n implementations. Offers equals and hashCode method + * implementations based on Locale (only!). Offers also toString(). + * + * @author Stefan Schmidt + * @since 1.1 + */ +public abstract class AbstractLanguage implements I18n { + + /** + * equals compares locale only! + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof I18n)) { + return false; + } + + final Locale thisLocale = getLocale(); + final Locale other = ((I18n) obj).getLocale(); + if (thisLocale == null) { + if (other != null) { + return false; + } + } + else if (!thisLocale.equals(other)) { + return false; + } + return true; + } + + /** + * hashCode uses locale only! + */ + @Override + public int hashCode() { + final Locale locale = getLocale(); + final int prime = 31; + int result = 1; + result = prime * result + (locale == null ? 0 : locale.hashCode()); + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Locale: ").append(getLocale()); + sb.append("Language label: ").append(getLanguage()); + return sb.toString(); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18n.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18n.java new file mode 100644 index 000000000..a5dbe7712 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18n.java @@ -0,0 +1,52 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n; + +import java.io.InputStream; +import java.util.Locale; + +/** + * This interface is needs to be implemented by translation providers for the + * Roo MVC JSP scaffolded UI. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface I18n { + + /** + * The input stream for the flag graphic (must be a png image 16 x 11 + * pixels, 72 DPI). Preferred flag icon set is the Fam Fam Fam set at + * http://www.famfamfam.com/lab/icons/flags/ + * + * @return the flag image stream + */ + InputStream getFlagGraphic(); + + /** + * The language label to be presented in the Web UI (ie: "English") + * + * @return the language + */ + String getLanguage(); + + /** + * The locale can be initialized statically or by using the constructor if + * the langauge is not statically supported or if a country specific + * language translation is provided (ie en_AU): static: Locale.ENGLISH + * constructor (no country): new Locale("en"); // Lowercase two-letter + * ISO-639 code. constructor (country specific): new Locale("en", "AU"); // + * Language lowercase two-letter ISO-639 code, country uppercase two-letter + * ISO-3166 code. + * + * @return the locale + */ + Locale getLocale(); + + /** + * The input stream for the translated message bundle. It will be saved in + * the addon according to the locale provided (ie messages_en.properties, or + * messages_en_AU.properties) + * + * @return the message bundle input stream + */ + InputStream getMessageBundle(); +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nComponent.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nComponent.java new file mode 100644 index 000000000..fbd2fbba8 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nComponent.java @@ -0,0 +1,61 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; + +/** + * Listener for OSGi service events for registering and unregistering I18n + * addons. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +@Reference(name = "language", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = I18n.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class I18nComponent implements I18nSupport { + + private final Set i18nSet = new HashSet(); + private final Object mutex = new Object(); + + protected void bindLanguage(final I18n i18n) { + synchronized (mutex) { + i18nSet.add(i18n); + } + } + + public I18n getLanguage(final Locale locale) { + synchronized (mutex) { + for (final I18n lang : Collections.unmodifiableSet(i18nSet)) { + if (lang.getLocale().toString() + .equalsIgnoreCase(locale.toString())) { + return lang; + } + } + } + return null; + } + + public Set getSupportedLanguages() { + Set set = null; + synchronized (mutex) { + set = Collections.unmodifiableSet(i18nSet); + } + return set; + } + + protected void unbindLanguage(final I18n i18n) { + synchronized (mutex) { + i18nSet.remove(i18n); + } + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nConverter.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nConverter.java new file mode 100644 index 000000000..85ab32f45 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nConverter.java @@ -0,0 +1,58 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n; + +import java.util.List; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link I18n}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class I18nConverter implements Converter { + + @Reference private I18nSupport i18nSupport; + + public I18n convertFromText(final String value, + final Class requiredType, final String optionContext) { + if (value.length() == 2) { + return i18nSupport.getLanguage(new Locale(value, "", "")); + // Disabled due to ROO-1584 + // } else if (value.length() == 5) { + // String[] split = value.split("_"); + // return i18nSupport.getLanguage(new Locale(split[0], + // split[1].toUpperCase(), "")); + } + return null; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + for (final I18n i18n : i18nSupport.getSupportedLanguages()) { + final Locale locale = i18n.getLocale(); + final StringBuilder localeString = new StringBuilder( + locale.getLanguage()); + if (locale.getCountry() == null || locale.getCountry().length() > 0) { + localeString.append("_").append( + locale.getCountry().toUpperCase()); + } + completions.add(new Completion(localeString.toString())); + } + return true; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return I18n.class.isAssignableFrom(requiredType); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nSupport.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nSupport.java new file mode 100644 index 000000000..99e208ff5 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/I18nSupport.java @@ -0,0 +1,17 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n; + +import java.util.Locale; +import java.util.Set; + +/** + * Service interface to allow addons to find all present I18n addons. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface I18nSupport { + + I18n getLanguage(Locale locale); + + Set getSupportedLanguages(); +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/DuchLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/DuchLanguage.java new file mode 100644 index 000000000..e236ea2b3 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/DuchLanguage.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n.languages; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * Dutch language support. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class DuchLanguage extends AbstractLanguage { + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "nl.png"); + } + + public String getLanguage() { + return "Dutch"; + } + + public Locale getLocale() { + return new Locale("nl"); + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "messages_nl.properties"); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/EnglishLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/EnglishLanguage.java new file mode 100644 index 000000000..0feabbc43 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/EnglishLanguage.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n.languages; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * English language support. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class EnglishLanguage extends AbstractLanguage { + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "gb.png"); + } + + public String getLanguage() { + return "English"; + } + + public Locale getLocale() { + return Locale.ENGLISH; + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "messages.properties"); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/GermanLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/GermanLanguage.java new file mode 100644 index 000000000..9dd5c27e7 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/GermanLanguage.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n.languages; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * German language support. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class GermanLanguage extends AbstractLanguage { + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "de.png"); + } + + public String getLanguage() { + return "Deubuilderh"; + } + + public Locale getLocale() { + return Locale.GERMAN; + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "messages_de.properties"); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/ItalianLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/ItalianLanguage.java new file mode 100644 index 000000000..993de81a2 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/ItalianLanguage.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n.languages; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * Italian language support. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class ItalianLanguage extends AbstractLanguage { + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "it.png"); + } + + public String getLanguage() { + return "Italiano"; + } + + public Locale getLocale() { + return Locale.ITALIAN; + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "messages_it.properties"); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/SpanishLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/SpanishLanguage.java new file mode 100644 index 000000000..481822779 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/SpanishLanguage.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n.languages; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * Spanish language support. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SpanishLanguage extends AbstractLanguage { + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "es.png"); + } + + public String getLanguage() { + return "Espanol"; + } + + public Locale getLocale() { + return new Locale("es"); + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "messages_es.properties"); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/SwedishLanguage.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/SwedishLanguage.java new file mode 100644 index 000000000..debdd6a8d --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/SwedishLanguage.java @@ -0,0 +1,36 @@ +package org.springframework.roo.addon.web.mvc.jsp.i18n.languages; + +import java.io.InputStream; +import java.util.Locale; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.jsp.i18n.AbstractLanguage; +import org.springframework.roo.support.util.FileUtils; + +/** + * Svedish language support. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class SwedishLanguage extends AbstractLanguage { + + public InputStream getFlagGraphic() { + return FileUtils.getInputStream(getClass(), "sv.png"); + } + + public String getLanguage() { + return "Svenska"; + } + + public Locale getLocale() { + return new Locale("sv"); + } + + public InputStream getMessageBundle() { + return FileUtils.getInputStream(getClass(), "messages_sv.properties"); + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/menu/MenuOperations.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/menu/MenuOperations.java new file mode 100644 index 000000000..2d7842a3d --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/menu/MenuOperations.java @@ -0,0 +1,94 @@ +package org.springframework.roo.addon.web.mvc.jsp.menu; + +import java.util.List; + +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.project.LogicalPath; + +/** + * Interface to {@link MenuOperations}. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.1 + */ +public interface MenuOperations { + + String DEFAULT_MENU_ITEM_PREFIX = "i_"; + + String FINDER_MENU_ITEM_PREFIX = "fi_"; + + /** + * Allows for the addition of menu categories and menu items. If a category + * or menu item with the given identifier exists, it will not be + * overwritten or replaced. + *

    + * Addons should determine their own category and menu item identifiers that + * do not clash with other addons. + *

    + * This method will not write i18n message codes. This means the + * caller will manage the properties himself, allowing for better + * efficiency. + *

    + * The recommended category identifier naming convention is + * menu_category_the-name_label where intention represents a further + * identifier to differentiate between different categories provided by the + * same addon. Similarly, the recommended menu item identifier naming + * convention is menu_item_the-name_the-category_label. + * + * @param menuCategoryName + * @param menuItemId + * @param globalMessageCode + * @param link + * @param idPrefix + * @param logicalPath + */ + void addMenuItem(JavaSymbolName menuCategoryName, + JavaSymbolName menuItemId, String globalMessageCode, String link, + String idPrefix, LogicalPath logicalPath); + + /** + * Allows for the addition of menu categories and menu items. If a category + * or menu item with the given identifier exists, it will not be + * overwritten or replaced. + *

    + * Addons should determine their own category and menu item identifiers that + * do not clash with other addons. + * + * @param menuCategoryName the identifier for the menu category (required) + * @param menuItemId the menu item identifier (required) + * @param menuItemLabel + * @param globalMessageCode message code for the menu item (required) + * @param link the menu item link (required) + * @param idPrefix the prefix to be used for this menu item (optional, + * MenuOperations.DEFAULT_MENU_ITEM_PREFIX is default) + * @param logicalPath + */ + void addMenuItem(JavaSymbolName menuCategoryName, + JavaSymbolName menuItemId, String menuItemLabel, + String globalMessageCode, String link, String idPrefix, + LogicalPath logicalPath); + + /** + * Attempts to locate a unused finder menu items and remove them. + * + * @param menuCategoryName the identifier for the menu category (required) + * @param allowedFinderMenuIds Finder menu ids currently installed + * @param logicalPath + */ + void cleanUpFinderMenuItems(JavaSymbolName menuCategoryName, + List allowedFinderMenuIds, LogicalPath logicalPath); + + /** + * Attempts to locate a menu item and remove it. + * + * @param menuCategoryName the identifier for the menu category (required) + * @param menuItemName the menu item identifier (required) + * @param idPrefix the prefix to be used for this menu item (optional, + * MenuOperations.DEFAULT_MENU_ITEM_PREFIX is default) + * @param logicalPath + */ + void cleanUpMenuItem(JavaSymbolName menuCategoryName, + JavaSymbolName menuItemName, String idPrefix, + LogicalPath logicalPath); +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/menu/MenuOperationsImpl.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/menu/MenuOperationsImpl.java new file mode 100644 index 000000000..d1ebacc2b --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/menu/MenuOperationsImpl.java @@ -0,0 +1,413 @@ +package org.springframework.roo.addon.web.mvc.jsp.menu; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.propfiles.PropFileOperations; +import org.springframework.roo.addon.web.mvc.jsp.roundtrip.XmlRoundTripFileManager; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Generates the jsp menu and allows for management of menu items. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class MenuOperationsImpl implements MenuOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(MenuOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private FileManager fileManager; + private ProjectOperations projectOperations; + private PropFileOperations propFileOperations; + private XmlRoundTripFileManager xmlRoundTripFileManager; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + public void addMenuItem(final JavaSymbolName menuCategoryName, + final JavaSymbolName menuItemId, final String globalMessageCode, + final String link, final String idPrefix, + final LogicalPath logicalPath) { + addMenuItem(menuCategoryName, menuItemId, "", globalMessageCode, link, + idPrefix, false, logicalPath); + } + + private void addMenuItem(final JavaSymbolName menuCategoryName, + final JavaSymbolName menuItemId, final String menuItemLabel, + final String globalMessageCode, final String link, String idPrefix, + final boolean writeProps, final LogicalPath logicalPath) { + Validate.notNull(menuCategoryName, "Menu category name required"); + Validate.notNull(menuItemId, "Menu item name required"); + Validate.notBlank(link, "Link required"); + + final Map properties = new LinkedHashMap(); + + if (StringUtils.isBlank(idPrefix)) { + idPrefix = DEFAULT_MENU_ITEM_PREFIX; + } + + final Document document = getMenuDocument(logicalPath); + + // Make the root element of the menu the one with the menu identifier + // allowing for different decorations of menu + Element rootElement = XmlUtils.findFirstElement("//*[@id='_menu']", + document.getFirstChild()); + if (rootElement == null) { + final Element rootMenu = new XmlElementBuilder("menu:menu", + document).addAttribute("id", "_menu").build(); + rootMenu.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(rootMenu)); + rootElement = (Element) document.getDocumentElement().appendChild( + rootMenu); + } + + // Check for existence of menu category by looking for the identifier + // provided + final String lcMenuCategoryName = menuCategoryName.getSymbolName() + .toLowerCase(); + + Element category = XmlUtils.findFirstElement("//*[@id='c_" + + lcMenuCategoryName + "']", rootElement); + // If not exists, create new one + if (category == null) { + category = (Element) rootElement.appendChild(new XmlElementBuilder( + "menu:category", document).addAttribute("id", + "c_" + lcMenuCategoryName).build()); + category.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(category)); + properties.put("menu_category_" + lcMenuCategoryName + "_label", + menuCategoryName.getReadableSymbolName()); + } + + // Check for existence of menu item by looking for the identifier + // provided + Element menuItem = XmlUtils.findFirstElement("//*[@id='" + idPrefix + + lcMenuCategoryName + "_" + + menuItemId.getSymbolName().toLowerCase() + "']", rootElement); + if (menuItem == null) { + menuItem = new XmlElementBuilder("menu:item", document) + .addAttribute( + "id", + idPrefix + lcMenuCategoryName + "_" + + menuItemId.getSymbolName().toLowerCase()) + .addAttribute("messageCode", globalMessageCode) + .addAttribute("url", link).build(); + menuItem.setAttribute("z", + XmlRoundTripUtils.calculateUniqueKeyFor(menuItem)); + category.appendChild(menuItem); + } + if (writeProps) { + properties.put("menu_item_" + lcMenuCategoryName + "_" + + menuItemId.getSymbolName().toLowerCase() + "_label", + menuItemLabel); + getPropFileOperations().addProperties(getProjectOperations() + .getPathResolver().getFocusedPath(Path.SRC_MAIN_WEBAPP), + "WEB-INF/i18n/application.properties", properties, true, + false); + } + getXmlRoundTripFileManager().writeToDiskIfNecessary( + getMenuFileName(logicalPath), document); + } + + public void addMenuItem(final JavaSymbolName menuCategoryName, + final JavaSymbolName menuItemId, final String menuItemLabel, + final String globalMessageCode, final String link, + final String idPrefix, final LogicalPath logicalPath) { + addMenuItem(menuCategoryName, menuItemId, menuItemLabel, + globalMessageCode, link, idPrefix, true, logicalPath); + } + + public void cleanUpFinderMenuItems(final JavaSymbolName menuCategoryName, + final List allowedFinderMenuIds, + final LogicalPath logicalPath) { + Validate.notNull(menuCategoryName, "Menu category identifier required"); + Validate.notNull(allowedFinderMenuIds, + "List of allowed menu items required"); + + final Document document = getMenuDocument(logicalPath); + + // Find any menu items under this category which have an id that starts + // with the menuItemIdPrefix + final List elements = XmlUtils.findElements( + "//category[@id='c_" + + menuCategoryName.getSymbolName().toLowerCase() + + "']//item[starts-with(@id, '" + + FINDER_MENU_ITEM_PREFIX + "')]", + document.getDocumentElement()); + if (elements.isEmpty()) { + return; + } + for (final Element element : elements) { + if (!allowedFinderMenuIds.contains(element.getAttribute("id")) + && isNotUserManaged(element)) { + element.getParentNode().removeChild(element); + } + } + getXmlRoundTripFileManager().writeToDiskIfNecessary( + getMenuFileName(logicalPath), document); + } + + /** + * Attempts to locate a menu item and remove it. + * + * @param menuCategoryName the identifier for the menu category (required) + * @param menuItemName the menu item identifier (required) + * @param idPrefix the prefix to be used for this menu item (optional, + * MenuOperations.DEFAULT_MENU_ITEM_PREFIX is default) + */ + public void cleanUpMenuItem(final JavaSymbolName menuCategoryName, + final JavaSymbolName menuItemName, String idPrefix, + final LogicalPath logicalPath) { + Validate.notNull(menuCategoryName, "Menu category identifier required"); + Validate.notNull(menuItemName, "Menu item id required"); + + if (StringUtils.isBlank(idPrefix)) { + idPrefix = DEFAULT_MENU_ITEM_PREFIX; + } + + final Document document = getMenuDocument(logicalPath); + + // Find menu item under this category if exists + final Element element = XmlUtils.findFirstElement("//category[@id='c_" + + menuCategoryName.getSymbolName().toLowerCase() + + "']//item[@id='" + idPrefix + + menuCategoryName.getSymbolName().toLowerCase() + "_" + + menuItemName.getSymbolName().toLowerCase() + "']", + document.getDocumentElement()); + if (element == null) { + return; + } + if (isNotUserManaged(element)) { + element.getParentNode().removeChild(element); + } + + getXmlRoundTripFileManager().writeToDiskIfNecessary( + getMenuFileName(logicalPath), document); + } + + private Document getMenuDocument(final LogicalPath logicalPath) { + try { + return XmlUtils.readXml(getMenuFileInputStream(logicalPath)); + } + catch (final Exception e) { + throw new IllegalArgumentException("Unable to parse menu.jspx" + + (StringUtils.isBlank(e.getMessage()) ? "" : " (" + + e.getMessage() + ")"), e); + } + } + + private InputStream getMenuFileInputStream(final LogicalPath logicalPath) { + final String menuFileName = getMenuFileName(logicalPath); + if (!getFileManager().exists(menuFileName)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), "menu.jspx"); + outputStream = getFileManager().createFile(menuFileName) + .getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException( + "Encountered an error during copying of menu.jspx for MVC Menu addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + final PathResolver pathResolver = getProjectOperations().getPathResolver(); + + final String menuPath = pathResolver.getIdentifier(logicalPath, + "WEB-INF/tags/menu/menu.tagx"); + if (!getFileManager().exists(menuPath)) { + InputStream inputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), "menu.tagx"); + getFileManager().createOrUpdateTextFileIfRequired(menuPath, + IOUtils.toString(inputStream), false); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of menu.tagx for MVC Menu addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + final String itemPath = pathResolver.getIdentifier(logicalPath, + "WEB-INF/tags/menu/item.tagx"); + if (!getFileManager().exists(itemPath)) { + InputStream inputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), "item.tagx"); + getFileManager().createOrUpdateTextFileIfRequired(menuPath, + IOUtils.toString(inputStream), false); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of item.tagx for MVC Menu addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + final String categoryPath = pathResolver.getIdentifier(logicalPath, + "WEB-INF/tags/menu/category.tagx"); + if (!getFileManager().exists(categoryPath)) { + InputStream inputStream = null; + try { + inputStream = FileUtils.getInputStream(getClass(), + "category.tagx"); + getFileManager().createOrUpdateTextFileIfRequired(menuPath, + IOUtils.toString(inputStream), false); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of category.tagx for MVC Menu addon.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + return getFileManager().getInputStream(menuFileName); + } + + private String getMenuFileName(final LogicalPath logicalPath) { + return getProjectOperations().getPathResolver().getIdentifier(logicalPath, + "WEB-INF/views/menu.jspx"); + } + + private boolean isNotUserManaged(final Element element) { + return "?".equals(element.getAttribute("z")) + || XmlRoundTripUtils.calculateUniqueKeyFor(element).equals( + element.getAttribute("z")); + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on MenuOperationsImpl."); + return null; + } + }else{ + return fileManager; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on MenuOperationsImpl."); + return null; + } + }else{ + return projectOperations; + } + } + + public PropFileOperations getPropFileOperations(){ + if(propFileOperations == null){ + // Get all Services implement PropFileOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PropFileOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (PropFileOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PropFileOperations on MenuOperationsImpl."); + return null; + } + }else{ + return propFileOperations; + } + } + + public XmlRoundTripFileManager getXmlRoundTripFileManager(){ + if(xmlRoundTripFileManager == null){ + // Get all Services implement XmlRoundTripFileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(XmlRoundTripFileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (XmlRoundTripFileManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load XmlRoundTripFileManager on MenuOperationsImpl."); + return null; + } + }else{ + return xmlRoundTripFileManager; + } + } +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/roundtrip/DefaultXmlRoundTripFileManager.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/roundtrip/DefaultXmlRoundTripFileManager.java new file mode 100644 index 000000000..835c502a0 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/roundtrip/DefaultXmlRoundTripFileManager.java @@ -0,0 +1,75 @@ +package org.springframework.roo.addon.web.mvc.jsp.roundtrip; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlRoundTripUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; + +/** + * Default implementation of {@link XmlRoundTripFileManager}. + * + * @author James Tyrrell + * @since 1.2.0 + */ +@Component +@Service +public class DefaultXmlRoundTripFileManager implements XmlRoundTripFileManager { + + @Reference private FileManager fileManager; + private final Map fileContentsMap = new HashMap(); + + public void writeToDiskIfNecessary(final String filename, + final Document proposed) { + Validate.notNull(filename, "The file name is required"); + Validate.notNull(proposed, "The proposed document is required"); + if (fileManager.exists(filename)) { + final String proposedContents = XmlUtils.nodeToString(proposed); + try { + final String contents = FileUtils.readFileToString(new File( + filename)) + proposedContents; + final String contentsSha = DigestUtils.shaHex(contents); + final String lastContents = fileContentsMap.get(filename); + if (lastContents != null && contentsSha.equals(lastContents)) { + return; + } + fileContentsMap.put(filename, contentsSha); + } + catch (final IOException ignored) { + } + try { + final Document original = XmlUtils.readXml(fileManager + .getInputStream(filename)); + if (XmlRoundTripUtils.compareDocuments(original, proposed)) { + DomUtils.removeTextNodes(original); + final String updateContents = XmlUtils + .nodeToString(original); + fileManager.createOrUpdateTextFileIfRequired(filename, + updateContents, false); + } + } + catch (final Exception e) { + throw new IllegalStateException("Failed to write " + filename + + " : " + e.getMessage()); + } + } + else { + final String contents = XmlUtils.nodeToString(proposed); + final String contentsSha = DigestUtils.shaHex(contents + contents); + fileContentsMap.put(filename, contentsSha); + fileManager.createOrUpdateTextFileIfRequired(filename, contents, + false); + } + } +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/roundtrip/XmlRoundTripFileManager.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/roundtrip/XmlRoundTripFileManager.java new file mode 100644 index 000000000..7f97a3b29 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/roundtrip/XmlRoundTripFileManager.java @@ -0,0 +1,27 @@ +package org.springframework.roo.addon.web.mvc.jsp.roundtrip; + +import org.w3c.dom.Document; + +/** + * Used to write XML documents that are round tripped, i.e. contain Z attributed + * elements, to disk. The class shouldn't be used for general XML documents. + * + * @author James Tyrrell + * @since 1.2.0 + */ +public interface XmlRoundTripFileManager { + + /** + * Updates or creates an XML file at the passed in location based on the + * passed in proposed contains. If the file specified doesn't exist the + * proposed document is interpreted as a String and written to disk. Should + * the document exist it is parsed, compared with the proposed and if + * required updated accordingly. The file output is cached using the + * proposed and the original as a key, this improves performance + * significantly. + * + * @param filename the path of the file to written or updated (required) + * @param proposed the proposed contents of the file + */ + void writeToDiskIfNecessary(String filename, Document proposed); +} diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/tiles/TilesOperations.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/tiles/TilesOperations.java new file mode 100644 index 000000000..0b04929d3 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/tiles/TilesOperations.java @@ -0,0 +1,48 @@ +package org.springframework.roo.addon.web.mvc.jsp.tiles; + +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.project.LogicalPath; + +/** + * Methods for manipulating Apache Tiles view configuration files. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public interface TilesOperations { + + String DEFAULT_TEMPLATE = "default"; + + String PUBLIC_TEMPLATE = "public"; + + /** + * Adds a new view definition to the views.xml Tiles + * configuration in the given folder + * + * @param folderName the name of the folder under + * /WEB-INF/views (specified via the path attribute + * in {@link WebScaffoldMetadata}; can be blank to update the + * main views file, or if not, any leading slash is ignored + * @param tilesViewName The simple name of the view (i.e. 'list', 'show', + * 'update', etc) or, if views are nested in sub-folders the name + * should be 'owner/list', 'owner/show', etc.; any leading slash + * is ignored + * @param tilesTemplateName The template name (i.e. 'admin', 'public') + * @param viewLocation The location of the view in the Web application (i.e. + * "/WEB-INF/views/owner/list.jspx") + */ + void addViewDefinition(String folderName, LogicalPath path, + String tilesViewName, String tilesTemplateName, String viewLocation); + + /** + * Removes a view definition from the views.xml Tiles + * configuration in the given folder + * + * @param name the simple name of the view to remove (i.e. 'list', 'show', + * 'update', etc) + * @param folderName the name of the folder under + * /WEB-INF/views; can be blank to update the main + * views file, or if not, any leading slash is ignored + */ + void removeViewDefinition(String name, String folderName, LogicalPath path); +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/tiles/TilesOperationsImpl.java b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/tiles/TilesOperationsImpl.java new file mode 100644 index 000000000..a58d57ed0 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/java/org/springframework/roo/addon/web/mvc/jsp/tiles/TilesOperationsImpl.java @@ -0,0 +1,230 @@ +package org.springframework.roo.addon.web.mvc.jsp.tiles; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Provides operations to manage tiles view definitions. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class TilesOperationsImpl implements TilesOperations { + + private static class TilesDtdResolver implements EntityResolver { + public InputSource resolveEntity(final String publicId, + final String systemId) { + if (systemId + .equals("http://tiles.apache.org/dtds/tiles-config_2_1.dtd")) { + return new InputSource(FileUtils.getInputStream( + TilesOperationsImpl.class, "tiles-config_2_1.dtd")); + } + // Use the default behaviour + return null; + } + } + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + + public void addViewDefinition(final String folderName, + final LogicalPath path, final String tilesViewName, + final String tilesTemplateName, final String viewLocation) { + Validate.notBlank(tilesViewName, "View name required"); + Validate.notBlank(tilesTemplateName, "Template name required"); + Validate.notBlank(viewLocation, "View location required"); + + final String viewsDefinitionFile = getTilesConfigFile(folderName, path); + + final String unprefixedViewName = StringUtils.removeStart( + tilesViewName, "/"); + final Element root = getViewsElement(viewsDefinitionFile); + final Element existingDefinition = XmlUtils.findFirstElement( + "/tiles-definitions/definition[@name = '" + unprefixedViewName + + "']", root); + if (existingDefinition != null) { + // A definition with this name does already exist - nothing to do + return; + } + + final Element newDefinition = root.getOwnerDocument().createElement( + "definition"); + newDefinition.setAttribute("name", unprefixedViewName); + newDefinition.setAttribute("extends", tilesTemplateName); + + final Element putAttribute = root.getOwnerDocument().createElement( + "put-attribute"); + putAttribute.setAttribute("name", "body"); + putAttribute.setAttribute("value", viewLocation); + + newDefinition.appendChild(putAttribute); + root.appendChild(newDefinition); + + writeToDiskIfNecessary(viewsDefinitionFile, root); + } + + /** + * Returns the canonical path of the "views.xml" Tiles configuration file in + * the given folder. + * + * @param folderName can be blank for the main views file; if not, any + * leading slash is ignored + * @param path + * @return a non-null path + */ + private String getTilesConfigFile(final String folderName, + final LogicalPath path) { + final String subPath; + if (StringUtils.isNotBlank(folderName) && !"/".equals(folderName)) { + subPath = "/" + folderName; + } + else { + subPath = ""; + } + return pathResolver.getIdentifier(path, "WEB-INF/views" + subPath + + "/views.xml"); + } + + /** + * Returns the root element of the given Tiles configuration file + * + * @param viewsDefinitionFile the canonical path of the file to load + * @return the root of a new XML document if that file does not exist + */ + private Element getViewsElement(final String viewsDefinitionFile) { + final Document tilesView; + if (fileManager.exists(viewsDefinitionFile)) { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + builder.setEntityResolver(new TilesDtdResolver()); + try { + tilesView = builder.parse(fileManager + .getInputStream(viewsDefinitionFile)); + } + catch (final SAXException se) { + throw new IllegalStateException("Unable to parse the tiles " + + viewsDefinitionFile + " file", se); + } + catch (final IOException ioe) { + throw new IllegalStateException("Unable to read the tiles " + + viewsDefinitionFile + " file (reason: " + + ioe.getMessage() + ")", ioe); + } + } + else { + tilesView = XmlUtils.getDocumentBuilder().newDocument(); + tilesView.appendChild(tilesView.createElement("tiles-definitions")); + } + return tilesView.getDocumentElement(); + } + + public void removeViewDefinition(final String name, + final String folderName, final LogicalPath path) { + Validate.notBlank(name, "View name required"); + + final String viewsDefinitionFile = getTilesConfigFile(folderName, path); + + final Element root = getViewsElement(viewsDefinitionFile); + + // Find menu item under this category if exists + final Element element = XmlUtils.findFirstElement( + "/tiles-definitions/definition[@name = '" + name + "']", root); + if (element != null) { + element.getParentNode().removeChild(element); + writeToDiskIfNecessary(viewsDefinitionFile, root); + } + } + + /** + * @param viewsDefinitionFile the canonical path of the file to update + * @param body the element whose parent document is to be written + * @return + */ + private boolean writeToDiskIfNecessary(final String tilesDefinitionFile, + final Element body) { + // Build a string representation of the Tiles config file + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + final Transformer transformer = XmlUtils.createIndentingTransformer(); + transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, + "http://tiles.apache.org/dtds/tiles-config_2_1.dtd"); + transformer + .setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, + "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"); + XmlUtils.writeXml(transformer, byteArrayOutputStream, + body.getOwnerDocument()); + final String viewContent = byteArrayOutputStream.toString(); + + // If mutableFile becomes non-null, it means we need to use it to write + // out the contents of jspContent to the file + MutableFile mutableFile = null; + if (fileManager.exists(tilesDefinitionFile)) { + // First verify if the file has even changed + final File file = new File(tilesDefinitionFile); + String existing = null; + try { + existing = org.apache.commons.io.FileUtils + .readFileToString(file); + } + catch (final IOException ignored) { + } + + if (!viewContent.equals(existing)) { + mutableFile = fileManager.updateFile(tilesDefinitionFile); + } + } + else { + mutableFile = fileManager.createFile(tilesDefinitionFile); + Validate.notNull(mutableFile, + "Could not create tiles view definition '%s'", + tilesDefinitionFile); + } + + if (mutableFile != null) { + OutputStream outputStream = null; + try { + // We need to write the file out (it's a new file, or the + // existing file has different contents) + outputStream = mutableFile.getOutputStream(); + IOUtils.write(viewContent, outputStream); + + // Return and indicate we wrote out the file + return true; + } + catch (final IOException ioe) { + throw new IllegalStateException("Could not output '" + + mutableFile.getCanonicalPath() + "'", ioe); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + + // A file existed, but it contained the same content, so we return false + return false; + } +} diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/dataAccessFailure.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/dataAccessFailure.jspx new file mode 100644 index 000000000..15d9b23ee --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/dataAccessFailure.jspx @@ -0,0 +1,30 @@ +

    + + + + +

    ${fn:escapeXml(title)}

    +

    + +

    + +

    +

    + +

    + + + + + + + + +
    +
    +
    +

    +
    +
    +
    + diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/de.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/de.png new file mode 100644 index 000000000..ac4a97736 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/de.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/es.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/es.png new file mode 100755 index 000000000..c2de2d711 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/es.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/fr.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/fr.png new file mode 100644 index 000000000..aea81ce54 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/fr.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/gb.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/gb.png new file mode 100644 index 000000000..ff701e19f Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/gb.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/it.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/it.png new file mode 100644 index 000000000..89692f74f Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/it.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages.properties new file mode 100644 index 000000000..be4a76138 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages.properties @@ -0,0 +1,99 @@ +#menu +global_menu_new=Create new {0} +global_menu_list=List all {0} +global_menu_find=Find by {0} +global_language_switch=Switch language to {0} +global_language=Language +global_sponsored=Sponsored by SpringSource +global_theme=Theme +global_theme_alt=alt +global_theme_standard=standard +global_generic={0} + +#welcome page +welcome_titlepane=Welcome to {0} +welcome_h3=Welcome to {0} +welcome_text=Spring Roo provides interactive, lightweight and user customizable tooling that enables rapid delivery of high performance enterprise Java applications. + +#entity labels +entity_list_all=List all {0} +entity_show=Show {0} +entity_create=Create new {0} +entity_update=Update {0} +entity_delete=Delete {0} +entity_delete_confirm=Are you sure want to delete this item? +entity_find=Find {0} +entity_not_found=No {0} found. +entity_not_found_single=No {0} found with this id. +entity_dependency_required=The following dependencies need to be created first: +entity_reference_not_managed=This relationship is managed from the {0} side. + +#button labels +button_home=Home +button_save=Save +button_update=Update +button_find=Find +button_cancel=Cancel +button_proceed=Proceed +button_submit=Submit +button_reset=Reset +button_end=End +button_showmessage=Show Message +button_showstacktrace=Show Stack Trace +button_showcookie=Show Cookie + +#field labels +field_simple_validation=Enter {0} {1} +field_invalid_email=Please enter a valid email +field_invalid_number=Number with \\'-\\' or \\'.\\' allowed +field_invalid_integer=Integer numbers only +field_invalid=Please enter valid {0} +field_required=required + +#list labels +list_first=First Page +list_next=Next Page +list_previous=Previous Page +list_last=Last Page +list_page=Page {0} of {1} +list_size=List results per page: + +#selenium +selenium_menu_test_suite=Test Suite + +#exception +exception_message=Exception Message +exception_stacktrace=Exception Stack Trace +exception_cookie=Cookies +exception_details=Details + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Data access failure +error_dataaccessfailure_problemdescription=Sorry, a problem occurred while accessing the database. + +#resourceNotFound_jspx +error_resourcenotfound_title=Requested Resource Not Found +error_resourcenotfound_problemdescription=Sorry, we did not find the resource you were looking for. + +#uncaughtException_jspx +error_uncaughtexception_title=Internal Error +error_uncaughtexception_problemdescription=Sorry, we encountered an internal error. + +#webflow +webflow_menu_enter=Enter {0} flow +webflow_state1_title=Spring Web Flow - View State One +webflow_state1_message=This is a simple example to get started with Spring Web Flow. The buttons below lead you to another view state (Proceed) or to an end state. +webflow_state2_title=Spring Web Flow - View State Two +webflow_state2_message=This is a simple example to get started with Spring Web Flow. The buttons below lead you to another view state (Proceed) or to an end state. +webflow_endstate_title=Spring Web Flow - End State +webflow_endstate_message=You have now reached the end of this flow. + +#security +security_login_title=Spring Security Login +security_login_message=You have tried to access a protected area of this application. By default you can login as "admin", with a password of "admin". +security_login_form_name=Name +security_login_form_name_message=Enter your name +security_login_form_password=Password +security_login_form_password_message=Enter your password +security_login_unsuccessful=Your login attempt was not successful, try again. Reason: +security_logout=Logout \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_de.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_de.properties new file mode 100644 index 000000000..a3698993b --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_de.properties @@ -0,0 +1,100 @@ +#menu +global_menu_new={0} erstellen +global_menu_list=Alle {0} finden +global_menu_find=Suchen: {0} +global_language_switch=Sprache nach {0} ndern +global_language=Sprache +global_sponsored=Gesponsert von SpringSource +global_theme=Thema +global_theme_alt=alt +global_theme_standard=standard +global_generic={0} + +#welcome page +welcome_titlepane=Willkommen zu {0} +welcome_h3=Willkommen zu {0} +welcome_text=Spring Roo bietet ein interaktives, leichtgewichtiges und vom Anwender anpassbares Tool, welches die schnelle Erstellung von performanten Enterprise Java Applikationen ermglicht. + +#entity labels +entity_list_all=Liste alle {0} +entity_show=Zeige {0} +entity_create={0} erstellen +entity_update={0} aktualisieren +entity_delete={0} lschen +entity_delete_confirm=Sind Sie sicher dass dieses Element entfernt werden soll? +entity_find=Finde {0} +entity_not_found={0} konnte nicht gefunden werden. +entity_not_found_single={0} konnte nicht gefunden werden mit dieser ID. +entity_dependency_required=Die folgenden Abhngigkeiten mssen erst geschaffen werden: +entity_reference_not_managed=Diese Beziehung ist von der {0} Seite gemanaged. + +#button labels +button_home=Home +button_save=Speichern +button_update=Aktualisieren +button_find=Finde +button_cancel=Abbrechen +button_proceed=Fortfahren +button_submit=Absenden +button_reset=Zurcksetzen +button_end=Ende +button_showmessage=Meldung Anzeigen +button_showstacktrace=Stack Trace Anzeigen +button_showcookie=Cookie Anzeigen + +#field labels +field_simple_validation=Bitte {0} eingeben {1} +field_invalid_email=E-Mail Adresse muss korrekt sein +field_invalid_number=Nummern mit \\'-\\' oder \\'.\\' erlaubt +field_invalid_integer=Nur Integer-Zahlen erlaubt +field_required=Pflichtfeld + +#list labels +list_first=Erste Seite +list_next=Nchste Seite +list_previous=Vorhergehende Seite +list_last=Letzte Seite +list_page=Seite {0} von {1} +list_size=Ergebnisse pro Seite: + +#webflow +webflow_menu_enter= {0} flow starten + +#selenium +selenium_menu_test_suite=Testflle + +#exception +exception_message=Exception Meldung +exception_stacktrace=Exception Stack Trace +exception_details=Details + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Datenzugriff fehlgeschlagen +error_dataaccessfailure_problemdescription=Ein Problem ist aufgetreten whrend des Datenzugriffs. + +#resourceNotFound_jspx +error_resourcenotfound_title=Angeforderte Ressource nicht gefunden +error_resourcenotfound_problemdescription=Die angeforderte Ressource konnte leider nicht gefunden werden. + +#uncaughtException_jspx +error_uncaughtexception_title=Interner Fehler +error_uncaughtexception_problemdescription=Leider ist ein interner Fehler aufgetreten. + +#webflow +webflow_menu_enter=Flow {0} starten +webflow_state1_title=Spring Web Flow - Seite (View State) Eins +webflow_state1_message=Dies ist ein einfaches Beispiel um Spring Web Flow zu demonstrieren.Die Buttons unten fhren Sie zum nchsten Schritt (Fortfahren) oder zum Ende (Ende). +webflow_state2_title=Spring Web Flow - Seite (View State) Zwei +webflow_state2_message=Dies ist ein einfaches Beispiel um Spring Web Flow zu demonstrieren.Der Button unten fhrt Sie zum Ende (Ende) des Flows. +webflow_endstate_title=Spring Web Flow - Letzte Seite (End State) +webflow_endstate_message=Sie haben das Ende des Flows erreicht. + +#security +security_login_title=Spring Security Login +security_login_message=Sie haben versucht eine gesicherte Seite aufzurufen.Standardmig knnen Sie als "admin", mit "admin" als Passwort einloggen. +security_login_form_name=Name +security_login_form_name_message=Geben Sie Ihren Namen ein +security_login_form_password=Passwort +security_login_form_password_message=Geben Sie Ihr Passwort ein +security_login_unsuccessful=Ihre Anmeldung war nicht erfolgreich, bitte versuchen Sie es noch einmal. Grund: +security_logout=Ausloggen \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_es.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_es.properties new file mode 100644 index 000000000..27bba1129 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_es.properties @@ -0,0 +1,99 @@ +#menu +global_menu_new=Crear nuevo {0} +global_menu_list=Listar {0} +global_menu_find=Buscar por {0} +global_language_switch=Cambiar idioma a {0} +global_language=Idioma +global_sponsored=Patrocinado por SpringSource +global_theme=Tema +global_theme_alt=alt +global_theme_standard=estndar +global_generic={0} + +#welcome page +welcome_titlepane=Bienvenido a {0} +welcome_h3=Bienvenido a {0} +welcome_text=Spring Roo proporciona herramientas interactivas, ligeras y adaptables al usuario que permiten entregar rpidamente aplicaciones empresariales Java de alto rendimiento. + +#entity labels +entity_list_all=Listar {0} +entity_show=Mostrar {0} +entity_create=Crear nuevo {0} +entity_update=Actualizar {0} +entity_delete=Borrar {0} +entity_delete_confirm=Estas seguro que quieres eliminar este elemento +entity_find=Buscar {0} +entity_not_found=No se han encontrado {0}. +entity_not_found_single=No se han encontrado {0} con este id. +entity_dependency_required=Las siguientes dependencias es necesario crear primero: +entity_reference_not_managed=Esta relacin se administra desde el {0} lado. + +#button labels +button_home=Inicio +button_save=Guardar +button_update=Actualizar +button_find=Buscar +button_cancel=Cancelar +button_proceed=Proceder +button_submit=Enviar +button_reset=Reiniciar +button_end=Fin +button_showmessage=Mostrar Mensaje +button_showstacktrace=Mostrar Rastro de Pila +button_showcookie=Mostrar Cookie + +#field labels +field_simple_validation=Introducir {0} {1} +field_invalid_email=Por favor, introduzca un email vlido +field_invalid_number=Se permiten nmeros con \\'-\\' o \\'.\\' +field_invalid_integer=Slo nmeros enteros +field_invalid=Por favor, introduzca un {0} vlido +field_required=obligatorio + +#list labels +list_first=Primera Pgina +list_next=Pgina Siguiente +list_previous=Pgina Anterior +list_last=ltima pgina +list_page=Pgina {0} de {1} +list_size=Resultados por pgina: + +#selenium +selenium_menu_test_suite=Batera de pruebas + +#exception +exception_message=Mensaje de la Excepcin +exception_stacktrace=Rastro la Pila de la Excepcin +exception_cookie=Cookies +exception_details=Detalles + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Error de acceso a datos +error_dataaccessfailure_problemdescription=Lo sentimos, ha ocurrido un problema al acceder a la base de datos. + +#resourceNotFound_jspx +error_resourcenotfound_title=Recurso Solicitado No Encontrado +error_resourcenotfound_problemdescription=Lo sentimos, no hemos encontrado el recurso que buscaba. + +#uncaughtException_jspx +error_uncaughtexception_title=Error Interno +error_uncaughtexception_problemdescription=Lo sentimos, ha ocurrido un error interno. + +#webflow +webflow_menu_enter=Empezar flow {0} +webflow_state1_title=Spring Web Flow - Estado Vista Uno +webflow_state1_message=Este es un ejemplo simple para empezar con Spring Web Flow. Los botones de abajo le llevan a otro estado vista (Proceder) o a al estado final. +webflow_state2_title=Spring Web Flow - Estado Vista Dos +webflow_state2_message=Este es un ejemplo simple para empezar con Spring Web Flow. Los botones de abajo le llevan a otro estado vista (Proceder) o a al estado final. +webflow_endstate_title=Spring Web Flow - Estado final +webflow_endstate_message=Ha llegado al final de este flujo. + +#security +security_login_title=Spring Security Login +security_login_message=Ha intentado acceder a una rea protegida de esta aplicacin. Por defecto, puede iniciar la sesin con "admin" y contrasea "admin". +security_login_form_name=Nombre +security_login_form_name_message=Introduzca su nombre +security_login_form_password=Contrasea +security_login_form_password_message=Introduzca su contrasea +security_login_unsuccessful=Su intento de inicio de sesin no ha tenido xito, intntelo de nuevo. Razn: +security_logout=Cerrar sesin \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_fr.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_fr.properties new file mode 100644 index 000000000..a3af4091e --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_fr.properties @@ -0,0 +1,99 @@ +#menu +global_menu_new=Créer un(e) {0} +global_menu_list={0}: Tout lister +global_menu_find=Lister par {0} +global_language_switch=Changer la langue pour {0} +global_language=Langue +global_sponsored=Sponsorisé par SpringSource +global_theme=Thème +global_theme_alt=alt +global_theme_standard=standard +global_generic={0} + +#welcome page +welcome_titlepane=Bienvenue sur {0} +welcome_h3=Bienvenue sur {0} +welcome_text=Spring Roo est un outil interactif, léger et customisable permettant la mise en place rapide d'applications Java Enterprise avec de grandes performances. + +#entity labels +entity_list_all=Tout lister {0} +entity_show=SAfficher {0} +entity_create=Créer un(e) {0} +entity_update=Mettre à jour {0} +entity_delete=Supprimer{0} +entity_delete_confirm=Êtes-vous sûr de vouloir supprimer cette entrée ? +entity_find=Chercher {0} +entity_not_found=Aucun(e) {0} trouvé(e). +entity_not_found_single=Pas de {0} trouvé(e) avec cet id. +entity_dependency_required=Les dépendances suivantes doivent être créés au préalable: +entity_reference_not_managed=Cette relation est gérée par {0}. + +#button labels +button_home=Accueil +button_save=Sauvegarder +button_update=Mettre à jour +button_find=Chercher +button_cancel=Annuler +button_proceed=Lancer +button_submit=Soumettre +button_reset=Remise à zéro +button_end=Fin +button_showmessage=Afficher Message +button_showstacktrace=Afficher le Stack Trace +button_showcookie=Afficher le Cookie + +#field labels +field_simple_validation=Entrer {0} {1} +field_invalid_email=Merci d'entrer une adresse email valide +field_invalid_number=Les nombres contenant \\'-\\' ou \\'.\\' sont autorisés +field_invalid_integer=Entiers uniquement +field_invalid=Merci d'entrer un(e) {0} valide. +field_required=requis + +#list labels +list_first=Première Page +list_next=Page Suivante +list_previous=Page Précédente +list_last=Dernière Page +list_page=Page {0} sur {1} +list_size=Afficher les résultats par page: + +#selenium +selenium_menu_test_suite=Test Suite + +#exception +exception_message=Exception Message +exception_stacktrace=Exception Stack Trace +exception_cookie=Cookies +exception_details=Détails + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Erreur d'accès aux données +error_dataaccessfailure_problemdescription=Désolé, un problème est apparu lors de l'accès à la base de données. + +#resourceNotFound_jspx +error_resourcenotfound_title=La ressource demandée n'a pas été trouvée. +error_resourcenotfound_problemdescription=Désolé, nous n'avons pas trouvé la ressource recherchée. + +#uncaughtException_jspx +error_uncaughtexception_title=Erreur Interne +error_uncaughtexception_problemdescription=Désolé, une erreur interne est survenue. + +#webflow +webflow_menu_enter=Entrer dans le flux {0} +webflow_state1_title=Flux Spring Web - Voir l'état 1 +webflow_state1_message=Ceci est un example pour débuter avec les flux Spring Web. Les boutons ci-dessus vous amène à un autre état (Lancer) ou à la fin du flux. +webflow_state2_title=Flux Spring Web - Voir l'état 2 +webflow_state2_message=Ceci est un example pour débuter avec les flux Spring Web. Les boutons ci-dessus vous amène à un autre état (Lancer) ou à la fin du flux. +webflow_endstate_title=Flux Spring Web - Etat final +webflow_endstate_message=Vous avez atteint la fin de ce flux. + +#security +security_login_title=Connection Sécurisée Spring +security_login_message=Vous essayez d'accéder à une zone protégée de l'application. Par défaut, vous pouvez utiliser le login "admin" et le mot de passe "admin". +security_login_form_name=Nom +security_login_form_name_message=Entrez votre nom +security_login_form_password=Mot de passe +security_login_form_password_message=Entrer votre mot de passe +security_login_unsuccessful=Vous n'avez pas réussi à vous connecter pour la raison suivante: +security_logout=Déconnexion \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_it.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_it.properties new file mode 100644 index 000000000..e9623c98c --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_it.properties @@ -0,0 +1,99 @@ +#menu +global_menu_new=Crea nuovo {0} +global_menu_list=Elenca tutti {0} +global_menu_find=Trova per {0} +global_language_switch=Cambia linguaggio in {0} +global_language=Lingua +global_sponsored=Sponsorizzato da SpringSource +global_theme=Tema +global_theme_alt=alt +global_theme_standard=standard +global_generic={0} + +#welcome page +welcome_titlepane=Benvenuto in {0} +welcome_h3=Benvenuto in {0} +welcome_text=Spring Roo fornisce un tool interattivo, leggero e personalizzabile dall'utente, che abilita lo sviluppo rapido di applicazioni Java enterprise ad alte prestazioni. + +#entity labels +entity_list_all=Elenca tutti {0} +entity_show=Mostra {0} +entity_create=Crea nuovo {0} +entity_update=Aggiorna {0} +entity_delete=Elimina {0} +entity_delete_confirm=Sei sicuro di voler cancellare questa voce? +entity_find={0} +entity_not_found=Nessun {0} trovato_ +entity_not_found_single=Nessun {0} trovato con questo id. +entity_dependency_required=Le seguenti dipendenze devono essere create prima: +entity_reference_not_managed=Questo rapporto gestito da {0} lato. + +#button labels +button_home=Home +button_save=Salva +button_update=Aggiorna +button_find=Trova +button_cancel=Cancella +button_proceed=Procedi +button_submit=Invia +button_reset=Azzera +button_end=Fine +button_showmessage=Mostra Messaggio +button_showstacktrace=Mostra Stack Trace +button_showcookie=Mostra Cookie + +#field labels +field_simple_validation=Inserisci {0} {1} +field_invalid_email=Si prega di inserire un indirizzo email valido +field_invalid_number=Numero con \\'-\\' o \\'.\\' ammesso +field_invalid_integer=Solo numeri interi +field_invalid=Si prega di inserire dei validi {0} +field_required=richiesto + +#list labels +list_first=Prima pagina +list_next=Prossima Pagina +list_previous=Pagina Precedente +list_last=Ultima Pagina +list_page=Pagina {0} di {1} +list_size=Elenco risultati per pagina: + +#selenium +selenium_menu_test_suite=Test Suite + +#exception +exception_message=Messaggio Eccezione +exception_stacktrace=Stack Trace Eccezione +exception_cookie=Cookies +exception_details=Dettagli + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Fallito l'accesso ai dati +error_dataaccessfailure_problemdescription=Spiacente, si verificato un problema durante l'accesso al database. + +#resourceNotFound_jspx +error_resourcenotfound_title=Risorsa richiesta non trovata +error_resourcenotfound_problemdescription=Spiacente, non troviamo la risorsa che stai cercando. + +#uncaughtException_jspx +error_uncaughtexception_title=Errore Interno +error_uncaughtexception_problemdescription=Spiacente, abbiamo incontrato un errore interno. + +#webflow +webflow_menu_enter=Inserire {0} flow +webflow_state1_title=Spring Web Flow - Stato View Uno +webflow_state1_message=Questo un semplice esempio con Spring Web Flow. I pulsanti sotto conducono ad un altro view state (Procedi) o ad uno stato finale. +webflow_state2_title=Spring Web Flow - Stato View Due +webflow_state2_message=Questo un semplice esempio per iniziare con Spring Web Flow. I pulsanti sotto conducono ad un altro stato view (Procedi) o ad uno stato finale. +webflow_endstate_title=Spring Web Flow - Stato finale +webflow_endstate_message=Hai raggiunto la fine di questo flusso. + +#security +security_login_title=Spring Security Login +security_login_message=Hai provato ad accedere ad un area protetta di questa applicazione. Di default puoi fare il login come "admin", con password "admin". +security_login_form_name=Nome +security_login_form_name_message=Inserisci il tuo nome +security_login_form_password=Password +security_login_form_password_message=Inserisci la password +security_login_unsuccessful=Il tentativo di login non ha avuto successo, prova ancora. Motivo: +security_logout=Logout diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_nl.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_nl.properties new file mode 100644 index 000000000..3d10769b9 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_nl.properties @@ -0,0 +1,100 @@ +#menu +global_menu_new={0} maken +global_menu_list=Toon alle {0} +global_menu_find=Vind op {0} +global_language_switch=Wissel taal naar: {0} +global_language=Taal +global_sponsored=Gesponsord door SpringSource +global_theme=Thema +global_theme_alt=alt +global_theme_standard=standaard +global_generic={0} + +#welcome page +welcome_titlepane=Welkom bij {0} +welcome_h3=Welkom bij {0} +welcome_text=Spring Roo levert interactieve, lichtgewicht en door de gebruiker aanpasbare tools waarmee snel hoogwaardige Enterprise Java applicaties gemaakt kunnen worden. + +#entity labels +entity_list_all=Toon alle {0} +entity_show=Toon {0} +entity_create={0} maken +entity_update=Bewerk {0} +entity_delete=Verwijder {0} +entity_delete_confirm=Weet je zeker dat dit item wilt verwijderen? +entity_find=Zoek {0} +entity_not_found=Geen {0} gevonden_ +entity_not_found_single=Geen {0} gevonden met dit id. +entity_dependency_required=De volgende afhankelijkheden eerst moeten worden gecreerd: +entity_reference_not_managed=Deze relatie wordt beheerd vanuit de {0} kant. + +#button labels +button_home=Home +button_save=Sla op +button_update=Bewerk +button_find=Zoek +button_cancel=Annuleer +button_proceed=Ga door +button_submit=Verstuur +button_reset=Herstel +button_end=Einde +button_showmessage=Toon bericht +button_showstacktrace=Toon stack trace +button_showcookie=Toon cookie + +#field labels +field_simple_validation=Voer {0} {1} in +field_invalid_email=Voer een geldig email adres in +field_invalid_number=Getal met \\'-\\' or \\',\\' toegestaan +field_invalid_integer=Alleen gehele getallen +field_invalid=Voer een geldige {0} in +field_required=verplicht + +#list labels +list_first=Eerste pagina +list_next=Volgende pagina +list_previous=Vorige pagina +list_last=Laatste pagina +list_page=Pagina {0} van {1} +list_size=Resultaten per pagina: + +#selenium +selenium_menu_test_suite=Test Suite + +#exception +#intentionally left the two exception entries below in English as Dutch translations of technical terms are confusing to Dutch developers +exception_message=Exception Message +exception_stacktrace=Exception Stack Trace +exception_cookie=Cookies +exception_details=Details + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Data toegang gefaald +error_dataaccessfailure_problemdescription=Sorry, er heeft zich een probleem voorgedaan bij het benaderen van de database. + +#resourceNotFound_jspx +error_resourcenotfound_title=Gevraagde bron is niet gevonden +error_resourcenotfound_problemdescription=Sorry, de gevraagde bron is niet gevonden + +#uncaughtException_jspx +error_uncaughtexception_title=Interne fout +error_uncaughtexception_problemdescription=Sorry, het systeem heeft een intern probleem aangetroffen + +#webflow +webflow_menu_enter=Ga flow {0} in +webflow_state1_title=Spring Web Flow - Bekijk eerste status +webflow_state1_message=Dit is een simpel voorbeeld van hoe met Spring Web Flow te beginnen. De knoppen hieronder leiden je naar een andere view state (Proceed), of naar een eind state. +webflow_state2_title=Spring Web Flow - Bekijk tweede status +webflow_state2_message=Dit is een simpel voorbeeld van hoe met Spring Web Flow te beginnen. De knoppen hieronder leiden je naar een andere view state (Proceed), of naar een eind state. +webflow_endstate_title=Spring Web Flow - status einde +webflow_endstate_message=U heeft het einde van deze flow bereikt. + +#security +security_login_title=Spring Security Login +security_login_message=U hebt geprobeerd een beveiligd gedeelte van de applicatie te benaderen. De standaard login is "admin", met wachtwoord "admin". +security_login_form_name=Naam +security_login_form_name_message=Voer uw naam in +security_login_form_password=Wachtwoord +security_login_form_password_message=Voer uw wachtwoord in +security_login_unsuccessful=Uw login poging was niet succesvoal, probeer het nogmaals. Reden: +security_logout=Afmelden \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_sv.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_sv.properties new file mode 100644 index 000000000..9ef717bcd --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/messages_sv.properties @@ -0,0 +1,100 @@ +#menu +global_menu_new=Skapa ny {0} +global_menu_list=Visa alla {0} +global_menu_find=Sk p {0} +global_language_switch=Byt sprk till {0} +global_language=Sprk +global_sponsored=Sponsrad av SpringSource +global_theme=Tema +global_theme_alt=alt +global_theme_standard=standard +global_generic={0} + +#welcome page +welcome_titlepane=Vlkommen till {0} +welcome_h3=Vlkommen till {0} +welcome_text=Spring Roo ger interaktiva, lttviktiga verktyg som kan anpassas av anvndaren och mjliggr snabb leverans av hgpresterande enterprise Java applikationer. + +#entity labels +entity_list_all=Visa alla {0} +entity_show=Visa {0} +entity_create=Skapa ny {0} +entity_update=Uppdatera {0} +entity_delete=Ta bort {0} +entity_delete_confirm=Vill du verkligen ta bort denna post? +entity_find=Sk p {0} +entity_not_found=Ingen {0} hittades. +entity_not_found_single=Ingen {0} hittades med detta id. +entity_dependency_required=Fljande beroenden mste skapas frst: +entity_reference_not_managed=Denna relation styrs frn den {0} sidan. + +#button labels +button_home=Hem +button_save=Spara +button_update=Uppdatera +button_find=Hitta +button_cancel=Avbryt +button_proceed=Fortstt +button_submit=Skicka +button_reset=Brja om +button_end=Sista +button_showmessage=Visa meddelande +button_showstacktrace=Visa Stack Trace +button_showcookie=Visa Cookie + +#field labels +field_simple_validation=Ange {0} {1} +field_invalid_email=Var vnlig anvnd en giltig emailadress +field_invalid_number=Siffra med \\'-\\' eller \\'.\\' tillten +field_invalid_integer=Endast heltal +field_invalid=Var vnlig anvd en giltig {0} +field_required=obligatoriskt + +#list labels +list_first=Frsta sidan +list_next=Nsta sida +list_previous=Frra sidan +list_last=Sista sidan +list_page=Sida {0} av {1} +list_size=Visa antal resultat per sida: + +#selenium +selenium_menu_test_suite=Test Svit + +#exception +exception_message=Undantagsmeddelande +exception_stacktrace=Undantag Stack Trace +exception_cookie=Cookies +exception_details=Detaljer + +#dataAccessFailure_jspx +error_dataaccessfailure_title=Fel vid datalsning +error_dataaccessfailure_problemdescription=Beklagar, ett problem uppstod vid lsning av databasen. + +#resourceNotFound_jspx +error_resourcenotfound_title=Efterfrgad tillgng hittades inte +error_resourcenotfound_problemdescription=Beklagar, vi kunde inte hitta tillgngen du efterfrgade. + +#uncaughtException_jspx +error_uncaughtexception_title=Internt fel +error_uncaughtexception_problemdescription=Beklagar, ett internt fel intrffade. + +#webflow +webflow_menu_enter=Brja {0} flow +webflow_state1_title=Spring Web Flow - See tillstnd ett +webflow_state1_message=Det hr r ett enkelt exempel p hur du kommer igng med Spring Web Flow. Knapparna nedan leder dig till ett annat tillstnd (Fortstt) eller till sluttillstndet. +webflow_state2_title=Spring Web Flow - See tillstnd tv +webflow_state2_message=Det hr r ett enkelt exempel p hur du kommer igng med Spring Web Flow. Knapparna nedan leder dig till ett annat tillstnd (Fortstt) eller till sluttillstndet. +webflow_endstate_title=Spring Web Flow - Sluttillstndet +webflow_endstate_message=Du har nu kommit till slutet p detta flde + +#security +security_login_title=Spring Security Login +security_login_message=Du har frskt komma in p en skyddad del av den hr applikationen. Som standard kan du logga in som "admin", med lsenord "admin". + +security_login_form_name=Namn +security_login_form_name_message=Ange ditt namn +security_login_form_password=Lsenord +security_login_form_password_message=Ange ditt lsenord +security_login_unsuccessful=Ditt frsk att logga in misslyckades, frsk igen_ Anledning: +security_logout=Logga ut \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/nl.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/nl.png new file mode 100644 index 000000000..fe44791e3 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/nl.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/sv.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/sv.png new file mode 100755 index 000000000..1994653da Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/i18n/languages/sv.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/add.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/add.png new file mode 100644 index 000000000..d5bfa0719 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/add.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/banner-graphic.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/banner-graphic.png new file mode 100644 index 000000000..7d2b78c6e Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/banner-graphic.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/create.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/create.png new file mode 100644 index 000000000..d5bfa0719 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/create.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/delete.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/delete.png new file mode 100644 index 000000000..3141467c6 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/delete.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/favicon.ico b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/favicon.ico new file mode 100644 index 000000000..c2f2b6ccd Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/favicon.ico differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/list.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/list.png new file mode 100644 index 000000000..acc30b853 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/list.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_first.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_first.png new file mode 100644 index 000000000..b03eaf8b5 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_first.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_last.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_last.png new file mode 100644 index 000000000..8ec894784 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_last.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_next.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_next.png new file mode 100644 index 000000000..e252606d3 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_next.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_previous.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_previous.png new file mode 100644 index 000000000..18f9cc109 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/resultset_previous.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/show.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/show.png new file mode 100644 index 000000000..2f193889f Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/show.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/springsource-logo.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/springsource-logo.png new file mode 100644 index 000000000..e170f8abf Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/springsource-logo.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/update.png b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/update.png new file mode 100644 index 000000000..046811ed7 Binary files /dev/null and b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/images/update.png differ diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/index-template.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/index-template.jspx new file mode 100644 index 000000000..07000646a --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/index-template.jspx @@ -0,0 +1,11 @@ +
    + + + + + +

    + +

    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/index.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/index.jspx new file mode 100644 index 000000000..12b501465 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/index.jspx @@ -0,0 +1,14 @@ +
    + + + + + +

    + +

    +

    + +

    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/resourceNotFound.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/resourceNotFound.jspx new file mode 100644 index 000000000..b3d27c763 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/resourceNotFound.jspx @@ -0,0 +1,29 @@ +
    + + + + +

    ${fn:escapeXml(title)}

    +

    + +

    + +

    +

    + +

    + + + + + + + + +
    +
    +
    +

    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/alt.css b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/alt.css new file mode 100644 index 000000000..dc99e4edf --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/alt.css @@ -0,0 +1,369 @@ +/* main elements */ + +body,div,td { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #666; +} + +body { + background-color: #fff; + text-align: center; +} + +#header { + margin-bottom: 1em; +} + +#wrapper { + width:800px; + min-width: 800px; + max-width: 800px; + margin-right: auto; + margin-left: auto; + + /* fix max-width incompatibility in IE6 */ + width:expression(document.body.clientWidth > 800? "800px": "auto" ); + + overflow:hidden; + display:block; +} + +/* header and footer elements */ + +#main { + overflow:hidden; + display:box; +} + +#menu { + background: #eee; + position:relative; + float:right; + left:0px; + width:220px; + margin-left:15px; +} + +#menu ul{ + list-style: none; + margin: 0; + padding: 0; +} + +#menu ul li{ + padding: 0px; +} + + +#menu a, #menu h2 { + display: block; + margin: 0; + padding: 2px 6px; + color:#FFFFFF; +} + +#menu h2 { + color: #fff; + background: #648C1D; + text-transform: uppercase; + font-weight:bold; + font-size: 12px; +} + +#menu a { + color: #666666; + background: #efefef; + text-decoration: none; + padding: 2px 12px; +} + +#menu a:hover { + color: #648C1D; + background: #fff; +} + +#footer { + background:#fff; + border:none; + margin-top:15px; + border-top:1px solid #999999; +} + +#footer .new { + float:left; +} + +#footer a:link {color: #7db223;} + +.quicklinks { + clear:both; + padding-bottom: 15px +} +.quicklinks span { + float: right; +} + +label { + width:100px; + float:left; + margin-left: 5px; + margin-top: 0px; +} + +input { + height:20px; +} + +input, textarea, select { + border:1px solid #B3B3B3; +} + +input.image { + border: none; + height: auto; + vertical-align: middle; +} + +submit { + height:25px; +} + +div { + text-align: left; +} + +div .box { + display:block; + margin-left:105px; +} + +/* menu elements*/ + +a.menu, a.menu:link, a.menu:visited {display:block; width:150px; height:25px;} + +/* text styles */ + +h1,h2,h3 { + font-family: Helvetica, sans-serif; + color: #7db223; +} + +h1 { + font-size: 20px; + line-height: 26px; +} + +h2 { + font-size: 18px; + line-height: 20px; +} + +h3 { + font-size: 15px; + line-height: 21px; + color:#555; +} + +h4 { + font-size: 14px; + line-height: 20px; +} + +.errors { + color: red; + font-weight: bold; + display: block; + margin-left: 105px; +} + +a { + text-decoration: underline; + font-size: 12px; +} + +a img { + border: 0 none; + vertical-align: middle; +} + +tr:nth-child(odd) { + background-color: #FFFFFF; +} + +tr:nth-child(even) { + background-color: #EFEFEF; +} + +a:link { + color: #7db223; +} + +a:hover { + color: #456314; +} + +a:active { + color: #7db223; +} + +a:visited { + color: #7db223; +} + +li { + padding-top: 5px; + text-align: left; +} + +ul li { + margin:0 0 0.25em 0; + padding:0; +} +/* table elements */ + +table { + background: #EEEEEE; + margin: 2px 0 0 0; + border: 1px solid #BBBBBB; + border-collapse: collapse; + width: 100% +} + +table table { + margin: -5px 0; + border: 0px solid #e0e7d3; + width: 100%; +} + +table td,table th { + padding: 2px; + border: 1px solid #CCCCCC; +} + +table td form { + text-align:center; + vertical-align: middle; + margin: 0px; +} + +table th { + font-size: 0.9em; + text-align: left; + font-weight: bold; + color: #FFF; + background: #999; +} + +table thead { + font-weight: bold; + font-style: italic; + background-color: #BBBBBB; +} + +table a:link {color: #303030;} + +.utilbox {width: 18px;} + +caption { + caption-side: top; + width: auto; + text-align: left; + font-size: 12px; + color: #848f73; + padding-bottom: 4px; +} + +fieldset { + background: #e0e7d3; + padding: 8px; + padding-bottom: 22px; + border: none; + width: 560px; +} + +fieldset label { + width: 70px; + float: left; + margin-top: 1.7em; + margin-left: 20px; +} + +fieldset textfield { + margin: 3px; + height: 20px; + background: #e0e7d3; +} + +fieldset textarea { + margin: 3px; + height: 165px; + background: #e0e7d3; +} + +fieldset input { + margin: 3px; + height: 20px; + background: #e0e7d3; +} + +fieldset table { + width: 100%; +} + +fieldset th { + padding-left: 25px; +} + +.table-buttons { + background-color:#fff; + border:none; +} + +.table-buttons td { + border:none; +} + +.submit input { + border: 1px solid #BBBBBB; + color:#777777; + padding:2px 7px; + font-size:11px; + text-transform:uppercase; + font-weight:bold; + height:24px; +} + +.updated { + background:#ecf1e5; + font-size:11px; + margin-left:2px; + border:4px solid #ecf1e5; +} + +.updated td { + padding:2px 8px; + font-size:11px; + color:#888888; +} + +.dijitArrowButton { + height: 20px; +} + +.dijitTextArea{ + min-height:5.5em !important; + max-height:22em !important; + overflow-y: auto !important; + max-width: 175px; +} + +.RichTextEditable{ + min-height:18em !important; + max-height:18em !important; +} + +.flag { + height: 11px; + width: 16px; +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/alt.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/alt.properties new file mode 100644 index 000000000..48df87ff7 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/alt.properties @@ -0,0 +1 @@ +styleSheet=resources/styles/alt.css diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/standard.css b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/standard.css new file mode 100644 index 000000000..9f79bb08c --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/standard.css @@ -0,0 +1,373 @@ +/* main elements */ + +body,div,td { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #666; +} + +body { + background-color: #fff; + text-align: center; +} + +#header { + margin-bottom: 1em; +} + +#wrapper { + width:800px; + min-width: 800px; + max-width: 800px; + margin-right: auto; + margin-left: auto; + + /* fix max-width incompatibility in IE6 */ + width:expression(document.body.clientWidth > 800? "800px": "auto" ); + + overflow:hidden; + display:block; +} + +/* header and footer elements */ + +#main { + overflow:hidden; + display:box; +} + +#menu { + background: #eee; + position:relative; + float:left; + left:0px; + width:220px; + margin-right:15px; +} + +#menu ul{ + list-style: none; + margin: 0; + padding: 0; +} + +#menu ul li{ + padding: 0px; +} + + +#menu a, #menu h2 { + display: block; + margin: 0; + padding: 2px 6px; + color:#FFFFFF; +} + +#menu h2 { + color: #fff; + background: #648C1D; + text-transform: uppercase; + font-weight:bold; + font-size: 12px; +} + +#menu a { + color: #666666; + background: #efefef; + text-decoration: none; + padding: 2px 12px; +} + +#menu a:hover { + color: #648C1D; + background: #fff; +} + +#footer { + background:#fff; + border:none; + margin-top:15px; + border-top:1px solid #999999; +} + +#footer .new { + float:left; +} + +#footer a:link {color: #7db223;} + +.quicklinks { + clear:both; + padding-bottom: 15px +} +.quicklinks span { + float: right; +} + +table.navigation { + border: 0px; +} + +label { + width:100px; + float:left; + margin-left: 5px; + margin-top: 0px; +} + +input { + height:20px; +} + +input, textarea, select { + border:1px solid #B3B3B3; +} + +input.image { + border: none; + height: auto; + vertical-align: middle; +} + +submit { + height:25px; +} + +div { + text-align: left; +} + +div .box { + display:block; + margin-left:105px; +} + +/* menu elements*/ + +a.menu, a.menu:link, a.menu:visited {display:block; width:150px; height:25px;} + +/* text styles */ + +h1,h2,h3 { + font-family: Helvetica, sans-serif; + color: #7db223; +} + +h1 { + font-size: 20px; + line-height: 26px; +} + +h2 { + font-size: 18px; + line-height: 20px; +} + +h3 { + font-size: 15px; + line-height: 21px; + color:#555; +} + +h4 { + font-size: 14px; + line-height: 20px; +} + +.errors { + color: red; + font-weight: bold; + display: block; + margin-left: 105px; +} + +a { + text-decoration: underline; + font-size: 12px; +} + +a img { + border: 0 none; + vertical-align: middle; +} + +tr:nth-child(odd) { + background-color: #FFFFFF; +} + +tr:nth-child(even) { + background-color: #EFEFEF; +} + +a:link { + color: #7db223; +} + +a:hover { + color: #456314; +} + +a:active { + color: #7db223; +} + +a:visited { + color: #7db223; +} + +li { + padding-top: 5px; + text-align: left; +} + +ul li { + margin:0 0 0.25em 0; + padding:0; +} +/* table elements */ + +table { + background: #EEEEEE; + margin: 2px 0 0 0; + border: 1px solid #BBBBBB; + border-collapse: collapse; + width: 100% +} + +table table { + margin: -5px 0; + border: 0px solid #e0e7d3; + width: 100%; +} + +table td,table th { + padding: 2px; + border: 1px solid #CCCCCC; +} + +table td form { + text-align:center; + vertical-align: middle; + margin: 0px; +} + +table th { + font-size: 0.9em; + text-align: left; + font-weight: bold; + color: #FFF; + background: #999; +} + +table thead { + font-weight: bold; + font-style: italic; + background-color: #BBBBBB; +} + +table a:link {color: #303030;} + +.utilbox {width: 18px;} + +caption { + caption-side: top; + width: auto; + text-align: left; + font-size: 12px; + color: #848f73; + padding-bottom: 4px; +} + +fieldset { + background: #e0e7d3; + padding: 8px; + padding-bottom: 22px; + border: none; + width: 560px; +} + +fieldset label { + width: 70px; + float: left; + margin-top: 1.7em; + margin-left: 20px; +} + +fieldset textfield { + margin: 3px; + height: 20px; + background: #e0e7d3; +} + +fieldset textarea { + margin: 3px; + height: 165px; + background: #e0e7d3; +} + +fieldset input { + margin: 3px; + height: 20px; + background: #e0e7d3; +} + +fieldset table { + width: 100%; +} + +fieldset th { + padding-left: 25px; +} + +.table-buttons { + background-color:#fff; + border:none; +} + +.table-buttons td { + border:none; +} + +.submit input { + border: 1px solid #BBBBBB; + color:#777777; + padding:2px 7px; + font-size:11px; + text-transform:uppercase; + font-weight:bold; + height:24px; +} + +.updated { + background:#ecf1e5; + font-size:11px; + margin-left:2px; + border:4px solid #ecf1e5; +} + +.updated td { + padding:2px 8px; + font-size:11px; + color:#888888; +} + +.dijitArrowButton { + height: 20px; +} + +.dijitTextArea{ + min-height:5.5em !important; + max-height:22em !important; + overflow-y: auto !important; + max-width: 175px; +} + +.RichTextEditable{ + min-height:18em !important; + max-height:18em !important; +} + +.flag { + height: 11px; + width: 16px; +} \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/standard.properties b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/standard.properties new file mode 100644 index 000000000..d8dc0164d --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/styles/standard.properties @@ -0,0 +1 @@ +styleSheet=resources/styles/standard.css diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/create.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/create.tagx new file mode 100644 index 000000000..f8d78e791 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/create.tagx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/dependency.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/dependency.tagx new file mode 100644 index 000000000..4888a1283 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/dependency.tagx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + +

    + +

    +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/checkbox.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/checkbox.tagx new file mode 100644 index 000000000..1804424d2 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/checkbox.tagx @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + ${field} + + + +
    + + + + + + + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/column.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/column.tagx new file mode 100644 index 000000000..b94c4f072 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/column.tagx @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/datetime.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/datetime.tagx new file mode 100644 index 000000000..934661283 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/datetime.tagx @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${field} + + + + + + + +
    + + + + + + + +
    + +
    +
    + + + + + + + + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/display.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/display.tagx new file mode 100644 index 000000000..a6e8f43c4 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/display.tagx @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/editor.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/editor.tagx new file mode 100644 index 000000000..c4c619285 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/editor.tagx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${field} + + + +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/input.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/input.tagx new file mode 100644 index 000000000..c1a52a12e --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/input.tagx @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${field} + + +
    + + + + + + + + + + + + + + +
    + +
    +
    + + + + + + + + + + + ${field_validation} + + + ${field_invalid} + + + ${field_required} + + + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/reference.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/reference.tagx new file mode 100644 index 000000000..a99eb1ca3 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/reference.tagx @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + +
    + + + + + + + + ${fn:escapeXml(add_message)} + + + + + ( + + ) + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/select.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/select.tagx new file mode 100644 index 000000000..e454985b2 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/select.tagx @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${field} + + + + ${itemLabel} + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + + + + + + +
    + + + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/simple.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/simple.tagx new file mode 100644 index 000000000..f8a86f328 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/simple.tagx @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/table.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/table.tagx new file mode 100644 index 000000000..6b17e4812 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/table.tagx @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + ${fn:escapeXml(show_label)} + + + + + + + + + ${fn:escapeXml(update_label)} + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/textarea.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/textarea.tagx new file mode 100644 index 000000000..1b5ba2384 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/fields/textarea.tagx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${field} + + + +
    + + +
    + + +
    +
    + +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/find.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/find.tagx new file mode 100644 index 000000000..3df8b3606 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/find.tagx @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/list.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/list.tagx new file mode 100644 index 000000000..b81d25669 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/list.tagx @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/show.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/show.tagx new file mode 100644 index 000000000..47cc06aa5 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/show.tagx @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/update.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/update.tagx new file mode 100644 index 000000000..01217c518 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/form/update.tagx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/category.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/category.tagx new file mode 100644 index 000000000..f78f81f39 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/category.tagx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
  • +

    + +

    +
      + +
    +
  • + +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/item.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/item.tagx new file mode 100644 index 000000000..b1768a11a --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/item.tagx @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + +
  • + + + + +
  • + +
    +
    diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/menu.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/menu.tagx new file mode 100644 index 000000000..f16269aa7 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/menu/menu.tagx @@ -0,0 +1,13 @@ + + + + + + + + +
      + +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/language.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/language.tagx new file mode 100644 index 000000000..c894b75d6 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/language.tagx @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ${fn:escapeXml(lang_label)} + + + + diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/load-scripts.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/load-scripts.tagx new file mode 100644 index 000000000..3aa5ace4b --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/load-scripts.tagx @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + ${pageContext.response.locale} + + + + + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/pagination.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/pagination.tagx new file mode 100644 index 000000000..268bc6551 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/pagination.tagx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${i} + + + + + + + + + + + + + + + ${fn:escapeXml(first_label)} + + + + + + + + + + + ${fn:escapeXml(previous_label)} + + + + + + + + + + + + + + ${fn:escapeXml(next_label)} + + + + + + + + + + + ${fn:escapeXml(last_label)} + + + + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/panel.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/panel.tagx new file mode 100644 index 000000000..a1d58257a --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/panel.tagx @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + ${id} + + + + ${openPane} + + + + ${title} + + + +
    + + +
    +
    +
    diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/placeholder.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/placeholder.tagx new file mode 100644 index 000000000..0ff9617d9 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/placeholder.tagx @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/theme.tagx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/theme.tagx new file mode 100644 index 000000000..6fe7723fe --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tags/util/theme.tagx @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + ${fn:escapeXml(theme_standard)} + + + + + + + + + + + + ${fn:escapeXml(theme_alt)} + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/configuration.xml b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/configuration.xml new file mode 100644 index 000000000..925f4fd49 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/configuration.xml @@ -0,0 +1,12 @@ + + + + + + org.apache.tiles + tiles-jsp + 2.2.2 + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/default.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/default.jspx new file mode 100644 index 000000000..b045bc0f3 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/default.jspx @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + <spring:message code="welcome_h3" arguments="${app_name}" /> + + + +
    + + +
    + + +
    +
    + + diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/footer.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/footer.jspx new file mode 100644 index 000000000..289b267c1 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/footer.jspx @@ -0,0 +1,35 @@ + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/header.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/header.jspx new file mode 100644 index 000000000..dbcf95042 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/header.jspx @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/layouts.xml b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/layouts.xml new file mode 100644 index 000000000..3c9d6d4bb --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/layouts.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/menu.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/menu.jspx new file mode 100644 index 000000000..e41ae0634 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/menu.jspx @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/tiles-config_2_1.dtd b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/tiles-config_2_1.dtd new file mode 100644 index 000000000..9ef5147a4 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/tiles-config_2_1.dtd @@ -0,0 +1,364 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/tiles-mvc-config-template.xml b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/tiles-mvc-config-template.xml new file mode 100644 index 000000000..5a9f7cff0 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/tiles-mvc-config-template.xml @@ -0,0 +1,16 @@ + + + + + + + + + + /WEB-INF/layouts/layouts.xml + + /WEB-INF/views/**/views.xml + + + + \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/views.xml b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/views.xml new file mode 100644 index 000000000..d12473f91 --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/tiles/views.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/uncaughtException.jspx b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/uncaughtException.jspx new file mode 100644 index 000000000..9180b0dac --- /dev/null +++ b/addon-web-mvc-jsp/src/main/resources/org/springframework/roo/addon/web/mvc/jsp/uncaughtException.jspx @@ -0,0 +1,29 @@ +
    + + + + +

    ${fn:escapeXml(title)}

    +

    + +

    + +

    +

    + +

    + + + + + + + + +
    +
    +
    +

    +
    +
    +
    \ No newline at end of file diff --git a/addon-web-mvc-jsp/src/test/java/org/springframework/roo/addon/web/mvc/jsp/JspOperationsImplTest.java b/addon-web-mvc-jsp/src/test/java/org/springframework/roo/addon/web/mvc/jsp/JspOperationsImplTest.java new file mode 100644 index 000000000..8817b6b53 --- /dev/null +++ b/addon-web-mvc-jsp/src/test/java/org/springframework/roo/addon/web/mvc/jsp/JspOperationsImplTest.java @@ -0,0 +1,71 @@ +package org.springframework.roo.addon.web.mvc.jsp; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.Test; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link JspOperationsImpl} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JspOperationsImplTest { + + /** + * Asserts that the given preferred mapping provided by the user gives rise + * to the expected folder name and mapping to use in the annotation + * + * @param preferredMapping + * @param expectedFolder + * @param expectedMapping + */ + private void assertFolderAndMapping(final String preferredMapping, + final String expectedFolder, final String expectedMapping) { + // Set up + final JavaType mockController = mock(JavaType.class); + when(mockController.getSimpleTypeName()).thenReturn("FooController"); + + // Invoke + final ImmutablePair pair = JspOperationsImpl + .getFolderAndMapping(preferredMapping, mockController); + + // Check + assertEquals(expectedFolder, pair.getKey()); + assertEquals(expectedMapping, pair.getValue()); + } + + @Test + public void testGetFolderAndMappingForBlankPreferredMapping() { + assertFolderAndMapping("", "foo", "/foo/**"); + } + + @Test + public void testGetFolderAndMappingForPreferredMappingWithLeadingSlash() { + assertFolderAndMapping("/foo", "foo", "/foo/**"); + } + + @Test + public void testGetFolderAndMappingForPreferredMappingWithMixedCase() { + assertFolderAndMapping("fooBar", "fooBar", "/fooBar/**"); + } + + @Test + public void testGetFolderAndMappingForPreferredMappingWithTrailingSlash() { + assertFolderAndMapping("foo/", "foo", "/foo/**"); + } + + @Test + public void testGetFolderAndMappingForPreferredMappingWithTrailingWildcard() { + assertFolderAndMapping("foo/**", "foo", "/foo/**"); + } + + @Test + public void testGetFolderAndMappingForUnadornedPreferredMapping() { + assertFolderAndMapping("foo", "foo", "/foo/**"); + } +} diff --git a/addon-web-selenium/pom.xml b/addon-web-selenium/pom.xml new file mode 100644 index 000000000..258eba7f5 --- /dev/null +++ b/addon-web-selenium/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.addon.web.selenium + bundle + Spring Roo - Addon - Web Selenium Test Generator + Configuration and integration of Selenium web tests in the target project. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.addon.configurable + + + org.springframework.roo + org.springframework.roo.addon.finder + + + org.springframework.roo + org.springframework.roo.addon.plural + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumCommands.java b/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumCommands.java new file mode 100644 index 000000000..aee118ee8 --- /dev/null +++ b/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumCommands.java @@ -0,0 +1,37 @@ +package org.springframework.roo.addon.web.selenium; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Commands for the 'selenium' add-on to be used by the ROO shell. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class SeleniumCommands implements CommandMarker { + + @Reference private SeleniumOperations seleniumOperations; + + @CliCommand(value = "selenium test", help = "Creates a new Selenium test for a particular controller") + public void generateTest( + @CliOption(key = "controller", mandatory = true, help = "Controller to create a Selenium test for") final JavaType controller, + @CliOption(key = "name", mandatory = false, help = "Name of the test") final String name, + @CliOption(key = "serverUrl", mandatory = false, unspecifiedDefaultValue = "http://localhost:8080/", specifiedDefaultValue = "http://localhost:8080/", help = "URL of the server where the web application is available, including protocol, port and hostname") final String url) { + + seleniumOperations.generateTest(controller, name, url); + } + + @CliAvailabilityIndicator({ "selenium test" }) + public boolean isJdkFieldManagementAvailable() { + return seleniumOperations.isSeleniumInstallationPossible(); + } +} \ No newline at end of file diff --git a/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumOperations.java b/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumOperations.java new file mode 100644 index 000000000..65f92997b --- /dev/null +++ b/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumOperations.java @@ -0,0 +1,23 @@ +package org.springframework.roo.addon.web.selenium; + +import org.springframework.roo.model.JavaType; + +/** + * Provides Selenium operations. + * + * @author Ben Alex + * @since 1.0 + */ +public interface SeleniumOperations { + + /** + * Creates a new Selenium testcase + * + * @param controller the JavaType of the controller under test (required) + * @param name the name of the test case (optional) + * @param serverURL the URL of the Selenium server (optional) + */ + void generateTest(JavaType controller, String name, String serverURL); + + boolean isSeleniumInstallationPossible(); +} \ No newline at end of file diff --git a/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumOperationsImpl.java b/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumOperationsImpl.java new file mode 100644 index 000000000..3226f44bb --- /dev/null +++ b/addon-web-selenium/src/main/java/org/springframework/roo/addon/web/selenium/SeleniumOperationsImpl.java @@ -0,0 +1,462 @@ +package org.springframework.roo.addon.web.selenium; + +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; +import static org.springframework.roo.model.Jsr303JavaType.FUTURE; +import static org.springframework.roo.model.Jsr303JavaType.MIN; +import static org.springframework.roo.model.Jsr303JavaType.PAST; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.addon.web.mvc.controller.details.WebMetadataService; +import org.springframework.roo.addon.web.mvc.controller.scaffold.WebScaffoldMetadata; +import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.operations.DateTime; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.FeatureNames; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Implementation of {@link SeleniumOperations}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class SeleniumOperationsImpl implements SeleniumOperations { + + private static final Logger LOGGER = HandlerUtils + .getLogger(SeleniumOperationsImpl.class); + + @Reference private FileManager fileManager; + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private MenuOperations menuOperations; + @Reference private MetadataService metadataService; + @Reference private PathResolver pathResolver; + @Reference private PersistenceMemberLocator persistenceMemberLocator; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private WebMetadataService webMetadataService; + + private Node clickAndWaitCommand(final Document document, + final String linkTarget) { + final Node tr = document.createElement("tr"); + + final Node td1 = tr.appendChild(document.createElement("td")); + td1.setTextContent("clickAndWait"); + + final Node td2 = tr.appendChild(document.createElement("td")); + td2.setTextContent(linkTarget); + + final Node td3 = tr.appendChild(document.createElement("td")); + td3.setTextContent(" "); + + return tr; + } + + private String convertToInitializer(final FieldMetadata field) { + String initializer = " "; + short index = 1; + final AnnotationMetadata min = MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), MIN); + if (min != null) { + final AnnotationAttributeValue value = min + .getAttribute(new JavaSymbolName("value")); + if (value != null) { + index = new Short(value.getValue().toString()); + } + } + final JavaType fieldType = field.getFieldType(); + if (field.getFieldName().getSymbolName().contains("email") + || field.getFieldName().getSymbolName().contains("Email")) { + initializer = "some@email.com"; + } + else if (fieldType.equals(JavaType.STRING)) { + initializer = "some" + + field.getFieldName() + .getSymbolNameCapitalisedFirstLetter() + index; + } + else if (fieldType.equals(new JavaType(Date.class.getName())) + || fieldType.equals(new JavaType(Calendar.class.getName()))) { + final Calendar cal = Calendar.getInstance(); + AnnotationMetadata dateTimeFormat = null; + String style = null; + if ((dateTimeFormat = MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), DATE_TIME_FORMAT)) != null) { + final AnnotationAttributeValue value = dateTimeFormat + .getAttribute(new JavaSymbolName("style")); + if (value != null) { + style = value.getValue().toString(); + } + } + if (MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), + PAST) != null) { + cal.add(Calendar.YEAR, -1); + cal.add(Calendar.MONTH, -1); + cal.add(Calendar.DAY_OF_MONTH, -1); + } + else if (MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), FUTURE) != null) { + cal.add(Calendar.YEAR, 1); + cal.add(Calendar.MONTH, 1); + cal.add(Calendar.DAY_OF_MONTH, 1); + } + if (style != null) { + if (style.startsWith("-")) { + initializer = ((SimpleDateFormat) DateFormat + .getTimeInstance( + DateTime.parseDateFormat(style.charAt(1)), + Locale.getDefault())).format(cal.getTime()); + } + else if (style.endsWith("-")) { + initializer = ((SimpleDateFormat) DateFormat + .getDateInstance( + DateTime.parseDateFormat(style.charAt(0)), + Locale.getDefault())).format(cal.getTime()); + } + else { + initializer = ((SimpleDateFormat) DateFormat + .getDateTimeInstance( + DateTime.parseDateFormat(style.charAt(0)), + DateTime.parseDateFormat(style.charAt(1)), + Locale.getDefault())).format(cal.getTime()); + } + } + else { + initializer = ((SimpleDateFormat) DateFormat.getDateInstance( + DateFormat.SHORT, Locale.getDefault())).format(cal + .getTime()); + } + + } + else if (fieldType.equals(JavaType.BOOLEAN_OBJECT) + || fieldType.equals(JavaType.BOOLEAN_PRIMITIVE)) { + initializer = Boolean.valueOf(false).toString(); + } + else if (fieldType.equals(JavaType.INT_OBJECT) + || fieldType.equals(JavaType.INT_PRIMITIVE)) { + initializer = Integer.valueOf(index).toString(); + } + else if (fieldType.equals(JavaType.DOUBLE_OBJECT) + || fieldType.equals(JavaType.DOUBLE_PRIMITIVE)) { + initializer = Double.toString(index); + } + else if (fieldType.equals(JavaType.FLOAT_OBJECT) + || fieldType.equals(JavaType.FLOAT_PRIMITIVE)) { + initializer = Float.toString(index); + } + else if (fieldType.equals(LONG_OBJECT) + || fieldType.equals(JavaType.LONG_PRIMITIVE)) { + initializer = Long.valueOf(index).toString(); + } + else if (fieldType.equals(JavaType.SHORT_OBJECT) + || fieldType.equals(JavaType.SHORT_PRIMITIVE)) { + initializer = Short.valueOf(index).toString(); + } + else if (fieldType.equals(BIG_DECIMAL)) { + initializer = new BigDecimal(index).toString(); + } + return initializer; + } + + /** + * Creates a new Selenium testcase + * + * @param controller the JavaType of the controller under test (required) + * @param name the name of the test case (optional) + */ + public void generateTest(final JavaType controller, String name, + String serverURL) { + Validate.notNull(controller, "Controller type required"); + + final ClassOrInterfaceTypeDetails controllerTypeDetails = typeLocationService + .getTypeDetails(controller); + Validate.notNull( + controllerTypeDetails, + "Class or interface type details for type '%s' could not be resolved", + controller); + + final LogicalPath path = PhysicalTypeIdentifier + .getPath(controllerTypeDetails.getDeclaredByMetadataId()); + final String webScaffoldMetadataIdentifier = WebScaffoldMetadata + .createIdentifier(controller, path); + final WebScaffoldMetadata webScaffoldMetadata = (WebScaffoldMetadata) metadataService + .get(webScaffoldMetadataIdentifier); + Validate.notNull( + webScaffoldMetadata, + "Web controller '%s' does not appear to be an automatic, scaffolded controller", + controller.getFullyQualifiedTypeName()); + + // We abort the creation of a selenium test if the controller does not + // allow the creation of new instances for the form backing object + if (!webScaffoldMetadata.getAnnotationValues().isCreate()) { + LOGGER.warning("The controller you specified does not allow the creation of new instances of the form backing object. No Selenium tests created."); + return; + } + + if (!serverURL.endsWith("/")) { + serverURL = serverURL + "/"; + } + + final JavaType formBackingType = webScaffoldMetadata + .getAnnotationValues().getFormBackingObject(); + final String relativeTestFilePath = "selenium/test-" + + formBackingType.getSimpleTypeName().toLowerCase() + ".xhtml"; + final String seleniumPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, relativeTestFilePath); + + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), "selenium-template.xhtml"); + Validate.notNull(templateInputStream, + "Could not acquire selenium.xhtml template"); + final Document document = XmlUtils.readXml(templateInputStream); + + final Element root = (Element) document.getLastChild(); + if (root == null || !"html".equals(root.getNodeName())) { + throw new IllegalArgumentException( + "Could not parse selenium test case template file!"); + } + + name = name != null ? name : "Selenium test for " + + controller.getSimpleTypeName(); + XmlUtils.findRequiredElement("/html/head/title", root).setTextContent( + name); + + XmlUtils.findRequiredElement("/html/body/table/thead/tr/td", root) + .setTextContent(name); + + final Element tbody = XmlUtils.findRequiredElement( + "/html/body/table/tbody", root); + tbody.appendChild(openCommand( + document, + serverURL + + projectOperations.getProjectName(projectOperations + .getFocusedModuleName()) + "/" + + webScaffoldMetadata.getAnnotationValues().getPath() + + "?form")); + + final ClassOrInterfaceTypeDetails formBackingTypeDetails = typeLocationService + .getTypeDetails(formBackingType); + Validate.notNull( + formBackingType, + "Class or interface type details for type '%s' could not be resolved", + formBackingType); + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(getClass().getName(), formBackingTypeDetails); + + // Add composite PK identifier fields if needed + for (final FieldMetadata field : persistenceMemberLocator + .getEmbeddedIdentifierFields(formBackingType)) { + final JavaType fieldType = field.getFieldType(); + if (!fieldType.isCommonCollectionType() + && !isSpecialType(fieldType)) { + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + field); + final String fieldName = field.getFieldName().getSymbolName(); + fieldBuilder.setFieldName(new JavaSymbolName(fieldName + "." + + fieldName)); + tbody.appendChild(typeCommand(document, fieldBuilder.build())); + } + } + + // Add all other fields + final List fields = webMetadataService + .getScaffoldEligibleFieldMetadata(formBackingType, + memberDetails, null); + for (final FieldMetadata field : fields) { + final JavaType fieldType = field.getFieldType(); + if (!fieldType.isCommonCollectionType() + && !isSpecialType(fieldType)) { + tbody.appendChild(typeCommand(document, field)); + } + } + + tbody.appendChild(clickAndWaitCommand(document, + "//input[@id = 'proceed']")); + + // Add verifications for all other fields + for (final FieldMetadata field : fields) { + final JavaType fieldType = field.getFieldType(); + if (!fieldType.isCommonCollectionType() + && !isSpecialType(fieldType)) { + tbody.appendChild(verifyTextCommand(document, formBackingType, + field)); + } + } + + fileManager.createOrUpdateTextFileIfRequired(seleniumPath, + XmlUtils.nodeToString(document), false); + + manageTestSuite(relativeTestFilePath, name, serverURL); + + // Install selenium-maven-plugin + installMavenPlugin(); + } + + private void installMavenPlugin() { + // Stop if the plugin is already installed + for (final Plugin plugin : projectOperations.getFocusedModule() + .getBuildPlugins()) { + if (plugin.getArtifactId().equals("selenium-maven-plugin")) { + return; + } + } + + final Element configuration = XmlUtils.getConfiguration(getClass()); + final Element plugin = XmlUtils.findFirstElement( + "/configuration/selenium/plugin", configuration); + + // Now install the plugin itself + if (plugin != null) { + projectOperations.addBuildPlugin( + projectOperations.getFocusedModuleName(), + new Plugin(plugin)); + } + } + + public boolean isSeleniumInstallationPossible() { + return projectOperations.isFocusedProjectAvailable() + && projectOperations.isFeatureInstalled(FeatureNames.MVC); + } + + private boolean isSpecialType(final JavaType javaType) { + return typeLocationService.isInProject(javaType); + } + + private void manageTestSuite(final String testPath, final String name, + final String serverURL) { + final String relativeTestFilePath = "selenium/test-suite.xhtml"; + final String seleniumPath = pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_WEBAPP, relativeTestFilePath); + + final InputStream inputStream; + if (fileManager.exists(seleniumPath)) { + inputStream = fileManager.getInputStream(seleniumPath); + } + else { + inputStream = FileUtils.getInputStream(getClass(), + "selenium-test-suite-template.xhtml"); + Validate.notNull(inputStream, + "Could not acquire selenium test suite template"); + } + + final Document suite = XmlUtils.readXml(inputStream); + final Element root = (Element) suite.getLastChild(); + + XmlUtils.findRequiredElement("/html/head/title", root).setTextContent( + "Test suite for " + + projectOperations.getProjectName(projectOperations + .getFocusedModuleName()) + "project"); + + final Element tr = suite.createElement("tr"); + final Element td = suite.createElement("td"); + tr.appendChild(td); + final Element a = suite.createElement("a"); + a.setAttribute( + "href", + serverURL + + projectOperations.getProjectName(projectOperations + .getFocusedModuleName()) + "/resources/" + + testPath); + a.setTextContent(name); + td.appendChild(a); + + XmlUtils.findRequiredElement("/html/body/table", root).appendChild(tr); + + fileManager.createOrUpdateTextFileIfRequired(seleniumPath, + XmlUtils.nodeToString(suite), false); + + menuOperations.addMenuItem(new JavaSymbolName("SeleniumTests"), + new JavaSymbolName("Test"), "Test", "selenium_menu_test_suite", + "/resources/" + relativeTestFilePath, "si_", + pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP)); + } + + private Node openCommand(final Document document, final String linkTarget) { + final Node tr = document.createElement("tr"); + + final Node td1 = tr.appendChild(document.createElement("td")); + td1.setTextContent("open"); + + final Node td2 = tr.appendChild(document.createElement("td")); + td2.setTextContent(linkTarget + (linkTarget.contains("?") ? "&" : "?") + + "lang=" + Locale.getDefault()); + + final Node td3 = tr.appendChild(document.createElement("td")); + td3.setTextContent(" "); + + return tr; + } + + private Node typeCommand(final Document document, final FieldMetadata field) { + final Node tr = document.createElement("tr"); + + final Node td1 = tr.appendChild(document.createElement("td")); + td1.setTextContent("type"); + + final Node td2 = tr.appendChild(document.createElement("td")); + td2.setTextContent("_" + field.getFieldName().getSymbolName() + "_id"); + + final Node td3 = tr.appendChild(document.createElement("td")); + td3.setTextContent(convertToInitializer(field)); + + return tr; + } + + private Node verifyTextCommand(final Document document, + final JavaType formBackingType, final FieldMetadata field) { + final Node tr = document.createElement("tr"); + + final Node td1 = tr.appendChild(document.createElement("td")); + td1.setTextContent("verifyText"); + + final Node td2 = tr.appendChild(document.createElement("td")); + td2.setTextContent(XmlUtils.convertId("_s_" + + formBackingType.getFullyQualifiedTypeName() + "_" + + field.getFieldName().getSymbolName() + "_" + + field.getFieldName().getSymbolName() + "_id")); + + final Node td3 = tr.appendChild(document.createElement("td")); + td3.setTextContent(convertToInitializer(field)); + + return tr; + } +} diff --git a/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/configuration.xml b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/configuration.xml new file mode 100644 index 000000000..7050e596d --- /dev/null +++ b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/configuration.xml @@ -0,0 +1,23 @@ + + + + + org.codehaus.mojo + selenium-maven-plugin + 2.3 + + *firefox + src/main/webapp/selenium/test-suite.xhtml + ${project.build.directory}/selenium.html + http://localhost:4444/ + + + + org.seleniumhq.selenium + selenium-server + 2.25.0 + + + + + \ No newline at end of file diff --git a/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/images/selenium.png b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/images/selenium.png new file mode 100644 index 000000000..57b03ce7a Binary files /dev/null and b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/images/selenium.png differ diff --git a/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/selenium-template.xhtml b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/selenium-template.xhtml new file mode 100644 index 000000000..9483cd363 --- /dev/null +++ b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/selenium-template.xhtml @@ -0,0 +1,19 @@ + + + + + +TO_BE_CHANGED_BY_ADDON + + + + + + + + + + +
    Selenium Test
    + + diff --git a/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/selenium-test-suite-template.xhtml b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/selenium-test-suite-template.xhtml new file mode 100644 index 000000000..8997fd946 --- /dev/null +++ b/addon-web-selenium/src/main/resources/org/springframework/roo/addon/web/selenium/selenium-test-suite-template.xhtml @@ -0,0 +1,13 @@ + + + +TO_BE_CHANGED_BY_ADDON + + + + + + +
    Suite Of Tests
    + + \ No newline at end of file diff --git a/annotations/pom.xml b/annotations/pom.xml new file mode 100644 index 000000000..32f87a0f4 --- /dev/null +++ b/annotations/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.annotations + bundle + Spring Roo - Annotations (ASLv2 Licensed) + + + + maven-antrun-plugin + 1.7 + + + generate-sources + generate-sources + + + + + + + + + + + + + + + + run + + + + + + ant-contrib + ant-contrib + 20020829 + + + org.apache.ant + ant-apache-regexp + 1.7.1 + + + + + + \ No newline at end of file diff --git a/annotations/src/main/java/readme.txt b/annotations/src/main/java/readme.txt new file mode 100644 index 000000000..3ce72b993 --- /dev/null +++ b/annotations/src/main/java/readme.txt @@ -0,0 +1,8 @@ +Please do not create any source files in the "annotation" module. + +The "annotation" module is automatically built during compilation. It +contains various files copied from other modules. The src/main/java/org +directory is excluded from SVN and explicitly deleted on each run. + +If you need to edit files currently appearing under src/main/java/org, +please do so in the original source module for that file. diff --git a/bootstrap/legal-bootstrap.txt b/bootstrap/legal-bootstrap.txt new file mode 100644 index 000000000..b25e216c8 --- /dev/null +++ b/bootstrap/legal-bootstrap.txt @@ -0,0 +1,34 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: Apache Felix +Software Web Site: http://felix.apache.org +Effective License: Apache License, Version 2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.html + +Statement as required by the license: + +"This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2007). +Licensed under the Apache License 2.0." + +Apache Felix is a runtime dependency of this module. Apache Felix +provides an OSGi container to host the Roo system. The following +source files were derived from those included in Apache Felix: + +org.springframework.roo.bootstrap.Main +org.springframework.roo.bootstrap.AutoProcessor + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml new file mode 100644 index 000000000..70b70d627 --- /dev/null +++ b/bootstrap/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.bootstrap + + jar + Spring Roo - Bootstrap + + + + org.apache.felix + org.apache.felix.framework + + + + org.apache.felix + org.apache.felix.scr + + + org.apache.felix + org.apache.felix.scr.annotations + + + org.apache.felix + org.apache.felix.ipojo + + + diff --git a/bootstrap/readme.txt b/bootstrap/readme.txt new file mode 100644 index 000000000..268e4da44 --- /dev/null +++ b/bootstrap/readme.txt @@ -0,0 +1,30 @@ +====================================================================== +WELCOME TO SPRING ROO! +====================================================================== + +Thank you for taking the time to download and install Spring Roo. + +You're now only a few minutes away from enterprise applications that +are considerably faster to build and maintain, faster to execute, +require less memory, and use the latest Java technologies in a best +practice, architecturally-optimal manner. We know that sounds too good +to be true, but you'll be able to see this for yourself very shortly! + +We're prepared comprehensive documentation to guide you on your Roo +journey. This includes installation instructions, and how to complete +the "ten minute test" of building an application from scratch and +starting it up. You can find this documentation (in several formats) +in your main Roo installation directory: + + $ROO_HOME/docs/html/index.html + $ROO_HOME/docs/pdf/spring-roo-docs.pdf + +You can also find documentation online at: + + http://static.springsource.org/spring-roo/reference/index.html + +If you use Twitter, you're encouraged to follow @SpringRoo. Also +please use @SpringRoo in your tweets so everyone can easily see them. + +Once again, welcome to the Spring Roo community! We hope that you find +Roo as much fun to work with as we have had in building it. diff --git a/bootstrap/roo-dev b/bootstrap/roo-dev new file mode 100755 index 000000000..cec42e7de --- /dev/null +++ b/bootstrap/roo-dev @@ -0,0 +1,90 @@ +#!/bin/sh + +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done +ROO_HOME=`dirname "$PRG"` + +# Absolute path +ROO_HOME=`cd "$ROO_HOME/.." ; pwd` +# echo "Resolved ROO_HOME: $ROO_HOME" + +# Remove osgi cache +rm -rf "$ROO_HOME"/bootstrap/target/osgi + +mkdir -p "$ROO_HOME"/bootstrap/target/osgi/bin +mkdir -p "$ROO_HOME"/bootstrap/target/osgi/bundle +mkdir -p "$ROO_HOME"/bootstrap/target/osgi/conf + +cp "$ROO_HOME"/bootstrap/src/main/bin/* "$ROO_HOME"/bootstrap/target/osgi/bin +chmod +x "$ROO_HOME"/bootstrap/target/osgi/bin/*.sh + +cp "$ROO_HOME"/bootstrap/src/main/conf/* "$ROO_HOME"/bootstrap/target/osgi/conf + +# Most Roo bundles are not special and belong in "bundle" +cp "$ROO_HOME"/target/all/*.jar "$ROO_HOME"/bootstrap/target/osgi/bundle + +# Move the startup-related JAR from the "bundle" directory to the "bin" directory +mv "$ROO_HOME"/bootstrap/target/osgi/bundle/org.springframework.roo.bootstrap-*.jar "$ROO_HOME"/bootstrap/target/osgi/bin +mv "$ROO_HOME"/bootstrap/target/osgi/bundle/org.apache.felix.framework-*.jar "$ROO_HOME"/bootstrap/target/osgi/bin + +# Build a classpath containing our two magical startup JARs (we look for " /" as per ROO-905) +ROO_CP=`echo "$ROO_HOME"/bootstrap/target/osgi/bin/*.jar | sed 's/ \//:\//g'` +# echo ROO_CP: $ROO_CP + +# Store file locations in variables to facilitate Cygwin conversion if needed + +ROO_OSGI_FRAMEWORK_STORAGE="$ROO_HOME/bootstrap/target/osgi/cache" +# echo "ROO_OSGI_FRAMEWORK_STORAGE: $ROO_OSGI_FRAMEWORK_STORAGE" + +ROO_AUTO_DEPLOY_DIRECTORY="$ROO_HOME/bootstrap/target/osgi/bundle" +# echo "ROO_AUTO_DEPLOY_DIRECTORY: $ROO_AUTO_DEPLOY_DIRECTORY" + +ROO_CONFIG_FILE_PROPERTIES="$ROO_HOME/bootstrap/target/osgi/conf/config.properties" +# echo "ROO_CONFIG_FILE_PROPERTIES: $ROO_CONFIG_FILE_PROPERTIES" + +cygwin=false; +case "`uname`" in + CYGWIN*) + cygwin=true + ;; +esac + +if [ "$cygwin" = "true" ]; then + export ROO_HOME=`cygpath -wp "$ROO_HOME"` + export ROO_CP=`cygpath -wp "$ROO_CP"` + export ROO_OSGI_FRAMEWORK_STORAGE=`cygpath -wp "$ROO_OSGI_FRAMEWORK_STORAGE"` + export ROO_AUTO_DEPLOY_DIRECTORY=`cygpath -wp "$ROO_AUTO_DEPLOY_DIRECTORY"` + export ROO_CONFIG_FILE_PROPERTIES=`cygpath -wp "$ROO_CONFIG_FILE_PROPERTIES"` + # echo "Modified ROO_HOME: $ROO_HOME" + # echo "Modified ROO_CP: $ROO_CP" + # echo "Modified ROO_OSGI_FRAMEWORK_STORAGE: $ROO_OSGI_FRAMEWORK_STORAGE" + # echo "Modified ROO_AUTO_DEPLOY_DIRECTORY: $ROO_AUTO_DEPLOY_DIRECTORY" + # echo "Modified ROO_CONFIG_FILE_PROPERTIES: $ROO_CONFIG_FILE_PROPERTIES" +fi + +# make sure to disable the flash message feature for the default OSX terminal, we recommend to use a ANSI compliant terminal such as iTerm if flash message support is desired +APPLE_TERMINAL=false; +if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + APPLE_TERMINAL=true +fi + +# Hop, hop, hop... +#DEBUG= +DEBUG="-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n" +PAUSE= +#PAUSE="-Droo.pause=true" +METADATA_TRACE= +#METADATA_TRACE="-Droo.metadata.trace=true" +ANSI="-Droo.console.ansi=true" +java $PAUSE $DEBUG $METADATA_TRACE $ANSI -Dis.apple.terminal=$APPLE_TERMINAL $ROO_OPTS -Dorg.osgi.framework.bootdelegation=org.netbeans.lib.profiler,org.netbeans.lib.profiler.\* -Droo.args="$*" -DdevelopmentMode=true -Dorg.osgi.framework.storage="$ROO_OSGI_FRAMEWORK_STORAGE" -Dfelix.auto.deploy.dir="$ROO_AUTO_DEPLOY_DIRECTORY" -Dfelix.config.properties="file:$ROO_CONFIG_FILE_PROPERTIES" -cp "$ROO_CP" org.springframework.roo.bootstrap.Main +EXITED=$? +echo Roo exited with code $EXITED diff --git a/bootstrap/roo-dev.bat b/bootstrap/roo-dev.bat new file mode 100644 index 000000000..4f56713dc --- /dev/null +++ b/bootstrap/roo-dev.bat @@ -0,0 +1,26 @@ +@echo off +setlocal enabledelayedexpansion + +for %%? in ("%~dp0..") do set ROO_HOME=%%~f? +rem echo Resolved ROO_HOME: "%ROO_HOME%" + +if not exist "%ROO_HOME%\bootstrap\target\osgi\bin" mkdir "%ROO_HOME%\bootstrap\target\osgi\bin" +if not exist "%ROO_HOME%\bootstrap\target\osgi\bundle" mkdir "%ROO_HOME%\bootstrap\target\osgi\bundle" +if not exist "%ROO_HOME%\bootstrap\target\osgi\conf" mkdir "%ROO_HOME%\bootstrap\target\osgi\conf" + +copy "%ROO_HOME%\bootstrap\src\main\bin\*.*" "%ROO_HOME%\bootstrap\target\osgi\bin" > NUL +copy "%ROO_HOME%\bootstrap\src\main\conf\*.*" "%ROO_HOME%\bootstrap\target\osgi\conf" > NUL + +rem Most Roo bundles are not special and belong in "bundle" +copy "%ROO_HOME%\target\all\*.jar" "%ROO_HOME%\bootstrap\target\osgi\bundle" > NUL + +rem Move the startup-related JAR from the "bundle" directory to the "bin" directory +move "%ROO_HOME%\bootstrap\target\osgi\bundle\org.springframework.roo.bootstrap-*.jar" "%ROO_HOME%\bootstrap\target\osgi\bin" > NUL 2>&1 +move "%ROO_HOME%\bootstrap\target\osgi\bundle\org.apache.felix.framework-*.jar" "%ROO_HOME%\bootstrap\target\osgi\bin" > NUL 2>&1 + +rem Build a classpath containing our two magical startup JARs +for %%a in ("%ROO_HOME%\bootstrap\target\osgi\bin\*.jar") do set ROO_CP=!ROO_CP!%%a; + +rem Hop, hop, hop... +java -Djline.nobell=true %ROO_OPTS% -Droo.args="%*" -DdevelopmentMode=true -Dorg.osgi.framework.storage="%ROO_HOME%\bootstrap\target\osgi\cache" -Dfelix.auto.deploy.dir="%ROO_HOME%\bootstrap\target\osgi\bundle" -Dfelix.config.properties="file:%ROO_HOME%\bootstrap\target\osgi\conf\config.properties" -Droo.console.ansi=true -cp "%ROO_CP%" org.springframework.roo.bootstrap.Main +echo Roo exited with code %errorlevel% diff --git a/bootstrap/src/main/bin/roo.bat b/bootstrap/src/main/bin/roo.bat new file mode 100644 index 000000000..c79e8ac83 --- /dev/null +++ b/bootstrap/src/main/bin/roo.bat @@ -0,0 +1,14 @@ +@echo off +setlocal enabledelayedexpansion + +for %%? in ("%~dp0..") do set ROO_HOME=%%~f? +rem echo Resolved ROO_HOME: "%ROO_HOME%" + +rem Build a classpath containing our two magical startup JARs +for %%a in ("%ROO_HOME%\bin\*.jar") do set ROO_CP=!ROO_CP!%%a; + +rem Hop, hop, hop... +java -Dflash.message.disabled=false -Djline.nobell=true %ROO_OPTS% -Droo.args="%*" -DdevelopmentMode=false -Dorg.osgi.framework.storage="%ROO_HOME%\cache" -Dfelix.auto.deploy.dir="%ROO_HOME%\bundle" -Dfelix.config.properties="file:%ROO_HOME%\conf\config.properties" -Droo.console.ansi=true -cp "%ROO_CP%" org.springframework.roo.bootstrap.Main +rem echo Roo exited with code %errorlevel% + +:end diff --git a/bootstrap/src/main/bin/roo.sh b/bootstrap/src/main/bin/roo.sh new file mode 100644 index 000000000..684284102 --- /dev/null +++ b/bootstrap/src/main/bin/roo.sh @@ -0,0 +1,74 @@ +#!/bin/sh + +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done +ROO_HOME=`dirname "$PRG"` + +# Absolute path +ROO_HOME=`cd "$ROO_HOME/.." ; pwd` + +# echo Resolved ROO_HOME: $ROO_HOME +# echo "JAVA_HOME $JAVA_HOME" + +cygwin=false; +case "`uname`" in + CYGWIN*) + cygwin=true + ;; +esac + +# Build a classpath containing our two magical startup JARs (we look for " /" as per ROO-905) +ROO_CP=`echo "$ROO_HOME"/bin/*.jar | sed 's/ \//:\//g'` +# echo ROO_CP: $ROO_CP + +# Store file locations in variables to facilitate Cygwin conversion if needed + +ROO_OSGI_FRAMEWORK_STORAGE="$ROO_HOME/cache" +# echo "ROO_OSGI_FRAMEWORK_STORAGE: $ROO_OSGI_FRAMEWORK_STORAGE" + +ROO_AUTO_DEPLOY_DIRECTORY="$ROO_HOME/bundle" +# echo "ROO_AUTO_DEPLOY_DIRECTORY: $ROO_AUTO_DEPLOY_DIRECTORY" + +ROO_CONFIG_FILE_PROPERTIES="$ROO_HOME/conf/config.properties" +# echo "ROO_CONFIG_FILE_PROPERTIES: $ROO_CONFIG_FILE_PROPERTIES" + +cygwin=false; +case "`uname`" in + CYGWIN*) + cygwin=true + ;; +esac + +if [ "$cygwin" = "true" ]; then + export ROO_HOME=`cygpath -wp "$ROO_HOME"` + export ROO_CP=`cygpath -wp "$ROO_CP"` + export ROO_OSGI_FRAMEWORK_STORAGE=`cygpath -wp "$ROO_OSGI_FRAMEWORK_STORAGE"` + export ROO_AUTO_DEPLOY_DIRECTORY=`cygpath -wp "$ROO_AUTO_DEPLOY_DIRECTORY"` + export ROO_CONFIG_FILE_PROPERTIES=`cygpath -wp "$ROO_CONFIG_FILE_PROPERTIES"` + # echo "Modified ROO_HOME: $ROO_HOME" + # echo "Modified ROO_CP: $ROO_CP" + # echo "Modified ROO_OSGI_FRAMEWORK_STORAGE: $ROO_OSGI_FRAMEWORK_STORAGE" + # echo "Modified ROO_AUTO_DEPLOY_DIRECTORY: $ROO_AUTO_DEPLOY_DIRECTORY" + # echo "Modified ROO_CONFIG_FILE_PROPERTIES: $ROO_CONFIG_FILE_PROPERTIES" +fi + +# make sure to disable the flash message feature for the default OSX terminal, we recommend to use a ANSI compliant terminal such as iTerm if flash message support is desired +APPLE_TERMINAL=false; +if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + APPLE_TERMINAL=true +fi + +ANSI="-Droo.console.ansi=true" +# Hop, hop, hop... +java -Dis.apple.terminal=$APPLE_TERMINAL $ROO_OPTS $ANSI -Droo.args="$*" -DdevelopmentMode=false -Dorg.osgi.framework.storage="$ROO_OSGI_FRAMEWORK_STORAGE" -Dfelix.auto.deploy.dir="$ROO_AUTO_DEPLOY_DIRECTORY" -Dfelix.config.properties="file:$ROO_CONFIG_FILE_PROPERTIES" -cp "$ROO_CP" org.springframework.roo.bootstrap.Main +EXITED=$? +# echo Roo exited with code $EXITED diff --git a/bootstrap/src/main/conf/config.properties b/bootstrap/src/main/conf/config.properties new file mode 100644 index 000000000..c2840ff7f --- /dev/null +++ b/bootstrap/src/main/conf/config.properties @@ -0,0 +1,14 @@ +felix.log.level=1 +felix.auto.deploy.action=install,start + +# The keyserver Roo contacts for verifying PGP signed resources such as add-on bundles +pgp.keyserver.url=http://keyserver.ubuntu.com/pks/lookup?op=get&search= + +# The URL Roo contacts to retrieve the list of add-ons registered through RooBot +roobot.url=http://spring-roo-repository.springsource.org/roobot/roobot.xml.zip + +# Indicate if the RooBot add-on index should be downloaded eagerly on shell start +roobot.index.dowload=true + +# The URL of the iconset used by Roo to auto-install flag icons during MVC i18n add-on creation +creator.i18n.iconset.url=http://www.famfamfam.com/lab/icons/flags/famfamfam_flag_icons.zip diff --git a/bootstrap/src/main/java/org/springframework/roo/bootstrap/AutoProcessor.java b/bootstrap/src/main/java/org/springframework/roo/bootstrap/AutoProcessor.java new file mode 100644 index 000000000..1862d1135 --- /dev/null +++ b/bootstrap/src/main/java/org/springframework/roo/bootstrap/AutoProcessor.java @@ -0,0 +1,390 @@ +package org.springframework.roo.bootstrap; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.service.startlevel.StartLevel; + +@SuppressWarnings({ "unchecked", "rawtypes" }) // **** CHANGE FROM ORIGINAL FELIX VERSION **** +public class AutoProcessor +{ + /** + * The property name used for the bundle directory. + **/ + public static final String AUTO_DEPLOY_DIR_PROPERY = "felix.auto.deploy.dir"; + /** + * The default name used for the bundle directory. + **/ + public static final String AUTO_DEPLOY_DIR_VALUE = "bundle"; + /** + * The property name used to specify auto-deploy actions. + **/ + public static final String AUTO_DEPLOY_ACTION_PROPERY = "felix.auto.deploy.action"; + /** + * The property name used to specify auto-deploy start level. + **/ + public static final String AUTO_DEPLOY_STARTLEVEL_PROPERY = "felix.auto.deploy.startlevel"; + /** + * The name used for the auto-deploy install action. + **/ + public static final String AUTO_DEPLOY_INSTALL_VALUE = "install"; + /** + * The name used for the auto-deploy start action. + **/ + public static final String AUTO_DEPLOY_START_VALUE = "start"; + /** + * The name used for the auto-deploy update action. + **/ + public static final String AUTO_DEPLOY_UPDATE_VALUE = "update"; + /** + * The name used for the auto-deploy uninstall action. + **/ + public static final String AUTO_DEPLOY_UNINSTALL_VALUE = "uninstall"; + /** + * The property name prefix for the launcher's auto-install property. + **/ + public static final String AUTO_INSTALL_PROP = "felix.auto.install"; + /** + * The property name prefix for the launcher's auto-start property. + **/ + public static final String AUTO_START_PROP = "felix.auto.start"; + + /** + * Used to instigate auto-deploy directory process and auto-install/auto-start + * configuration property processing during. + * @param configMap Map of configuration properties. + * @param context The system bundle context. + **/ + public static void process(Map configMap, BundleContext context) + { + configMap = (configMap == null) ? new HashMap() : configMap; + processAutoDeploy(configMap, context); + processAutoProperties(configMap, context); + } + + /** + *

    + * Processes bundles in the auto-deploy directory, performing the + * specified deploy actions. + *

    + */ + private static void processAutoDeploy(Map configMap, BundleContext context) + { + // Determine if auto deploy actions to perform. + String action = (String) configMap.get(AUTO_DEPLOY_ACTION_PROPERY); + action = (action == null) ? "" : action; + List actionList = new ArrayList(); + StringTokenizer st = new StringTokenizer(action, ","); + while (st.hasMoreTokens()) + { + String s = st.nextToken().trim().toLowerCase(); + if (s.equals(AUTO_DEPLOY_INSTALL_VALUE) + || s.equals(AUTO_DEPLOY_START_VALUE) + || s.equals(AUTO_DEPLOY_UPDATE_VALUE) + || s.equals(AUTO_DEPLOY_UNINSTALL_VALUE)) + { + actionList.add(s); + } + } + + // Perform auto-deploy actions. + if (actionList.size() > 0) + { + // Retrieve the Start Level service, since it will be needed + // to set the start level of the installed bundles. + StartLevel sl = (StartLevel) context.getService( + context.getServiceReference(org.osgi.service.startlevel.StartLevel.class.getName())); + + // Get start level for auto-deploy bundles. + int startLevel = sl.getInitialBundleStartLevel(); + if (configMap.get(AUTO_DEPLOY_STARTLEVEL_PROPERY) != null) + { + try + { + startLevel = Integer.parseInt( + configMap.get(AUTO_DEPLOY_STARTLEVEL_PROPERY).toString()); + } + catch (NumberFormatException ex) + { + // Ignore and keep default level. + } + } + + // Get list of already installed bundles as a map. + Map installedBundleMap = new HashMap(); + Bundle[] bundles = context.getBundles(); + for (int i = 0; i < bundles.length; i++) + { + installedBundleMap.put(bundles[i].getLocation(), bundles[i]); + } + + // Get the auto deploy directory. + String autoDir = (String) configMap.get(AUTO_DEPLOY_DIR_PROPERY); + autoDir = (autoDir == null) ? AUTO_DEPLOY_DIR_VALUE : autoDir; + // Look in the specified bundle directory to create a list + // of all JAR files to install. + File[] files = new File(autoDir).listFiles(); + List jarList = new ArrayList(); + if (files != null) + { + Arrays.sort(files); + for (int i = 0; i < files.length; i++) + { + if (files[i].getName().endsWith(".jar")) + { + jarList.add(files[i]); + } + } + } + + // Install bundle JAR files and remember the bundle objects. + final List startBundleList = new ArrayList(); + for (int i = 0; i < jarList.size(); i++) + { + // Look up the bundle by location, removing it from + // the map of installed bundles so the remaining bundles + // indicate which bundles may need to be uninstalled. + Bundle b = (Bundle) installedBundleMap.remove( + ((File) jarList.get(i)).toURI().toString()); + + try + { + // If the bundle is not already installed, then install it + // if the 'install' action is present. + if ((b == null) && actionList.contains(AUTO_DEPLOY_INSTALL_VALUE)) + { + b = context.installBundle( + ((File) jarList.get(i)).toURI().toString()); + } + // If the bundle is already installed, then update it + // if the 'update' action is present. + else if ((b != null) && actionList.contains(AUTO_DEPLOY_UPDATE_VALUE)) + { + b.update(); + } + + // If we have found and/or successfully installed a bundle, + // then add it to the list of bundles to potentially start + // and also set its start level accordingly. + if ((b != null) && !isFragment(b)) + { + startBundleList.add(b); + sl.setBundleStartLevel(b, startLevel); + } + } + catch (BundleException ex) + { + System.err.println("Auto-deploy install: " + + ex + ((ex.getCause() != null) ? " - " + ex.getCause() : "")); + } + } + + // Uninstall all bundles not in the auto-deploy directory if + // the 'uninstall' action is present. + if (actionList.contains(AUTO_DEPLOY_UNINSTALL_VALUE)) + { + for (Iterator it = installedBundleMap.entrySet().iterator(); it.hasNext(); ) + { + Map.Entry entry = (Map.Entry) it.next(); + Bundle b = (Bundle) entry.getValue(); + if (b.getBundleId() != 0) + { + try + { + b.uninstall(); + } + catch (BundleException ex) + { + System.err.println("Auto-deploy uninstall: " + + ex + ((ex.getCause() != null) ? " - " + ex.getCause() : "")); + } + } + } + } + + // Start all installed and/or updated bundles if the 'start' + // action is present. + if (actionList.contains(AUTO_DEPLOY_START_VALUE)) + { + for (int i = 0; i < startBundleList.size(); i++) + { + try + { + ((Bundle) startBundleList.get(i)).start(); + } + catch (BundleException ex) + { + System.err.println("Auto-deploy start: " + + ex + ((ex.getCause() != null) ? " - " + ex.getCause() : "")); + } + } + } + } + } + + /** + *

    + * Processes the auto-install and auto-start properties from the + * specified configuration properties. + *

    + */ + private static void processAutoProperties(Map configMap, BundleContext context) + { + // Retrieve the Start Level service, since it will be needed + // to set the start level of the installed bundles. + StartLevel sl = (StartLevel) context.getService( + context.getServiceReference(org.osgi.service.startlevel.StartLevel.class.getName())); + + // Retrieve all auto-install and auto-start properties and install + // their associated bundles. The auto-install property specifies a + // space-delimited list of bundle URLs to be automatically installed + // into each new profile, while the auto-start property specifies + // bundles to be installed and started. The start level to which the + // bundles are assigned is specified by appending a ".n" to the + // property name, where "n" is the desired start level for the list + // of bundles. If no start level is specified, the default start + // level is assumed. + for (Iterator i = configMap.keySet().iterator(); i.hasNext(); ) + { + String key = ((String) i.next()).toLowerCase(); + + // Ignore all keys that are not an auto property. + if (!key.startsWith(AUTO_INSTALL_PROP) && !key.startsWith(AUTO_START_PROP)) + { + continue; + } + + // If the auto property does not have a start level, + // then assume it is the default bundle start level, otherwise + // parse the specified start level. + int startLevel = sl.getInitialBundleStartLevel(); + if (!key.equals(AUTO_INSTALL_PROP) && !key.equals(AUTO_START_PROP)) + { + try + { + startLevel = Integer.parseInt(key.substring(key.lastIndexOf('.') + 1)); + } + catch (NumberFormatException ex) + { + System.err.println("Invalid property: " + key); + } + } + + // Parse and install the bundles associated with the key. + StringTokenizer st = new StringTokenizer((String) configMap.get(key), "\" ", true); + for (String location = nextLocation(st); location != null; location = nextLocation(st)) + { + try + { + Bundle b = context.installBundle(location, null); + sl.setBundleStartLevel(b, startLevel); + } + catch (Exception ex) + { + System.err.println("Auto-properties install: " + location + " (" + + ex + ((ex.getCause() != null) ? " - " + ex.getCause() : "") + ")"); +if (ex.getCause() != null) + ex.printStackTrace(); + } + } + } + + // Now loop through the auto-start bundles and start them. + for (Iterator i = configMap.keySet().iterator(); i.hasNext(); ) + { + String key = ((String) i.next()).toLowerCase(); + if (key.startsWith(AUTO_START_PROP)) + { + StringTokenizer st = new StringTokenizer((String) configMap.get(key), "\" ", true); + for (String location = nextLocation(st); location != null; location = nextLocation(st)) + { + // Installing twice just returns the same bundle. + try + { + Bundle b = context.installBundle(location, null); + if (b != null) + { + b.start(); + } + } + catch (Exception ex) + { + System.err.println("Auto-properties start: " + location + " (" + + ex + ((ex.getCause() != null) ? " - " + ex.getCause() : "") + ")"); + } + } + } + } + } + + private static String nextLocation(StringTokenizer st) + { + String retVal = null; + + if (st.countTokens() > 0) + { + String tokenList = "\" "; + StringBuffer tokBuf = new StringBuffer(10); + String tok = null; + boolean inQuote = false; + boolean tokStarted = false; + boolean exit = false; + while ((st.hasMoreTokens()) && (!exit)) + { + tok = st.nextToken(tokenList); + if (tok.equals("\"")) + { + inQuote = ! inQuote; + if (inQuote) + { + tokenList = "\""; + } + else + { + tokenList = "\" "; + } + + } + else if (tok.equals(" ")) + { + if (tokStarted) + { + retVal = tokBuf.toString(); + tokStarted=false; + tokBuf = new StringBuffer(10); + exit = true; + } + } + else + { + tokStarted = true; + tokBuf.append(tok.trim()); + } + } + + // Handle case where end of token stream and + // still got data + if ((!exit) && (tokStarted)) + { + retVal = tokBuf.toString(); + } + } + + return retVal; + } + + private static boolean isFragment(Bundle bundle) + { + return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null; + } +} \ No newline at end of file diff --git a/bootstrap/src/main/java/org/springframework/roo/bootstrap/Main.java b/bootstrap/src/main/java/org/springframework/roo/bootstrap/Main.java new file mode 100644 index 000000000..fdca66d7f --- /dev/null +++ b/bootstrap/src/main/java/org/springframework/roo/bootstrap/Main.java @@ -0,0 +1,583 @@ +package org.springframework.roo.bootstrap; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Map; +import java.util.HashMap; +import java.util.Properties; + +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; + +/** + * Loads Roo via Felix. + * + *

    + * This class is based on Apache Felix's org.apache.felix.main.Main class. + * + *

    + * For maximum compatibility with Felix, this class has minimal changes from the + * original. All changes are noted with a + * "**** CHANGE FROM ORIGINAL FELIX VERSION ****" comment. + * + * @author Ben Alex + * @since 1.1.0 + */ +@SuppressWarnings("all") // **** CHANGE FROM ORIGINAL FELIX VERSION **** +public class Main +{ + /** + * Switch for specifying bundle directory. + **/ + public static final String BUNDLE_DIR_SWITCH = "-b"; + + /** + * The property name used to specify whether the launcher should + * install a shutdown hook. + **/ + public static final String SHUTDOWN_HOOK_PROP = "felix.shutdown.hook"; + /** + * The property name used to specify an URL to the system + * property file. + **/ + public static final String SYSTEM_PROPERTIES_PROP = "felix.system.properties"; + /** + * The default name used for the system properties file. + **/ + public static final String SYSTEM_PROPERTIES_FILE_VALUE = "system.properties"; + /** + * The property name used to specify an URL to the configuration + * property file to be used for the created the framework instance. + **/ + public static final String CONFIG_PROPERTIES_PROP = "felix.config.properties"; + /** + * The default name used for the configuration properties file. + **/ + public static final String CONFIG_PROPERTIES_FILE_VALUE = "config.properties"; + /** + * Name of the configuration directory. + */ + public static final String CONFIG_DIRECTORY = "conf"; + + private static Framework m_fwk = null; + + /** + *

    + * This method performs the main task of constructing an framework instance + * and starting its execution. The following functions are performed + * when invoked: + *

    + *
      + *
    1. Examine and verify command-line arguments. The launcher + * accepts a "-b" command line switch to set the bundle auto-deploy + * directory and a single argument to set the bundle cache directory. + *
    2. + *
    3. Read the system properties file. This is a file + * containing properties to be pushed into System.setProperty() + * before starting the framework. This mechanism is mainly shorthand + * for people starting the framework from the command line to avoid having + * to specify a bunch of -D system property definitions. + * The only properties defined in this file that will impact the framework's + * behavior are the those concerning setting HTTP proxies, such as + * http.proxyHost, http.proxyPort, and + * http.proxyAuth. Generally speaking, the framework does + * not use system properties at all. + *
    4. + *
    5. Read the framework's configuration property file. This is + * a file containing properties used to configure the framework + * instance and to pass configuration information into + * bundles installed into the framework instance. The configuration + * property file is called config.properties by default + * and is located in the conf/ directory of the Felix + * installation directory, which is the parent directory of the + * directory containing the felix.jar file. It is possible + * to use a different location for the property file by specifying + * the desired URL using the felix.config.properties + * system property; this should be set using the -D syntax + * when executing the JVM. If the config.properties file + * cannot be found, then default values are used for all configuration + * properties. Refer to the + * Felix + * constructor documentation for more information on framework + * configuration properties. + *
    6. + *
    7. Copy configuration properties specified as system properties + * into the set of configuration properties. Even though the + * Felix framework does not consult system properties for configuration + * information, sometimes it is convenient to specify them on the command + * line when launching Felix. To make this possible, the Felix launcher + * copies any configuration properties specified as system properties + * into the set of configuration properties passed into Felix. + *
    8. + *
    9. Add shutdown hook. To make sure the framework shutdowns + * cleanly, the launcher installs a shutdown hook; this can be disabled + * with the felix.shutdown.hook configuration property. + *
    10. + *
    11. Create and initialize a framework instance. The OSGi standard + * FrameworkFactory is retrieved from META-INF/services + * and used to create a framework instance with the configuration properties. + *
    12. + *
    13. Auto-deploy bundles. All bundles in the auto-deploy + * directory are deployed into the framework instance. + *
    14. + *
    15. Start the framework. The framework is started and + * the launcher thread waits for the framework to shutdown. + *
    16. + *
    + *

    + * It should be noted that simply starting an instance of the framework is not + * enough to create an interactive session with it. It is necessary to install + * and start bundles that provide a some means to interact with the framework; + * this is generally done by bundles in the auto-deploy directory or specifying + * an "auto-start" property in the configuration property file. If no bundles + * providing a means to interact with the framework are installed or if the + * configuration property file cannot be found, the framework will appear to + * be hung or deadlocked. This is not the case, it is executing correctly, + * there is just no way to interact with it. + *

    + *

    + * The launcher provides two ways to deploy bundles into a framework at + * startup, which have associated configuration properties: + *

    + *
      + *
    • Bundle auto-deploy - Automatically deploys all bundles from a + * specified directory, controlled by the following configuration + * properties: + *
        + *
      • felix.auto.deploy.dir - Specifies the auto-deploy directory + * from which bundles are automatically deploy at framework startup. + * The default is the bundle/ directory of the current directory. + *
      • + *
      • felix.auto.deploy.action - Specifies the auto-deploy actions + * to be found on bundle JAR files found in the auto-deploy directory. + * The possible actions are install, update, + * start, and uninstall. If no actions are specified, + * then the auto-deploy directory is not processed. There is no default + * value for this property. + *
      • + *
      + *
    • + *
    • Bundle auto-properties - Configuration properties which specify URLs + * to bundles to install/start: + *
        + *
      • felix.auto.install.N - Space-delimited list of bundle + * URLs to automatically install when the framework is started, + * where N is the start level into which the bundle will be + * installed (e.g., felix.auto.install.2). + *
      • + *
      • felix.auto.start.N - Space-delimited list of bundle URLs + * to automatically install and start when the framework is started, + * where N is the start level into which the bundle will be + * installed (e.g., felix.auto.start.2). + *
      • + *
      + *
    • + *
    + *

    + * These properties should be specified in the config.properties + * so that they can be processed by the launcher during the framework + * startup process. + *

    + * @param args Accepts arguments to set the auto-deploy directory and/or + * the bundle cache directory. + * @throws Exception If an error occurs. + **/ + public static void main(String[] args) throws Exception + { + // Look for bundle directory and/or cache directory. + // We support at most one argument, which is the bundle + // cache directory. + String bundleDir = null; + String cacheDir = null; + boolean expectBundleDir = false; + for (int i = 0; i < args.length; i++) + { + if (args[i].equals(BUNDLE_DIR_SWITCH)) + { + expectBundleDir = true; + } + else if (expectBundleDir) + { + bundleDir = args[i]; + expectBundleDir = false; + } + else + { + cacheDir = args[i]; + } + } + + if ((args.length > 3) || (expectBundleDir && bundleDir == null)) + { + System.out.println("Usage: [-b ] []"); + System.exit(0); + } + + // Load system properties. + Main.loadSystemProperties(); + + // Read configuration properties. + Map configProps = Main.loadConfigProperties(); + // If no configuration properties were found, then create + // an empty properties object. + if (configProps == null) + { + System.err.println("No " + CONFIG_PROPERTIES_FILE_VALUE + " found."); + configProps = new HashMap(); + } + + // Copy framework properties from the system properties. + Main.copySystemProperties(configProps); + + // If there is a passed in bundle auto-deploy directory, then + // that overwrites anything in the config file. + if (bundleDir != null) + { + configProps.put(AutoProcessor.AUTO_DEPLOY_DIR_PROPERY, bundleDir); + } + + // If there is a passed in bundle cache directory, then + // that overwrites anything in the config file. + if (cacheDir != null) + { + configProps.put(Constants.FRAMEWORK_STORAGE, cacheDir); + } + + // If enabled, register a shutdown hook to make sure the framework is + // cleanly shutdown when the VM exits. + String enableHook = configProps.get(SHUTDOWN_HOOK_PROP); + if ((enableHook == null) || !enableHook.equalsIgnoreCase("false")) + { + Runtime.getRuntime().addShutdownHook(new Thread("Spring Roo Felix Shutdown Hook") { // **** CHANGE FROM ORIGINAL FELIX VERSION **** + public void run() + { + try + { + if (m_fwk != null) + { + m_fwk.stop(); + m_fwk.waitForStop(0); + } + } + catch (Exception ex) + { + System.err.println("Error stopping framework: " + ex); + } + } + }); + } + + try + { + double startedNanoseconds = System.nanoTime(); // **** CHANGE FROM ORIGINAL FELIX VERSION **** + // Create an instance of the framework. + FrameworkFactory factory = getFrameworkFactory(); + m_fwk = factory.newFramework(configProps); + // Initialize the framework, but don't start it yet. + m_fwk.init(); + // Use the system bundle context to process the auto-deploy + // and auto-install/auto-start properties. + AutoProcessor.process(configProps, m_fwk.getBundleContext()); + FrameworkEvent event; + do + { + // Start the framework. + m_fwk.start(); + // Wait for framework to stop to exit the VM. + event = m_fwk.waitForStop(0); + } + // If the framework was updated, then restart it. + while (event.getType() == FrameworkEvent.STOPPED_UPDATE); + // **** CHANGE FROM ORIGINAL FELIX VERSION **** + if (System.getProperty("developmentMode") != null && System.getProperty("developmentMode").equals(Boolean.TRUE.toString())) { + System.out.println("Total execution time " + Math.round(((System.nanoTime() - startedNanoseconds) / 1000000000D) * Math.pow(10, 3)) / Math.pow(10, 3) + " seconds"); + } + // Otherwise, exit. + System.exit(System.getProperty("roo.exit") == null ? 99 : new Integer(System.getProperty("roo.exit"))); + // **** END OF CHANGE FROM ORIGINAL FELIX VERSION **** + } + catch (Exception ex) + { + System.err.println("Could not create framework: " + ex); + ex.printStackTrace(); + System.exit(-1); // **** CHANGE FROM ORIGINAL FELIX VERSION **** + } + } + + /** + * Simple method to parse META-INF/services file for framework factory. + * Currently, it assumes the first non-commented line is the class name + * of the framework factory implementation. + * @return The created FrameworkFactory instance. + * @throws Exception if any errors occur. + **/ + private static FrameworkFactory getFrameworkFactory() throws Exception + { + URL url = Main.class.getClassLoader().getResource( + "META-INF/services/org.osgi.framework.launch.FrameworkFactory"); + if (url != null) + { + BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream())); + try + { + for (String s = br.readLine(); s != null; s = br.readLine()) + { + s = s.trim(); + // Try to load first non-empty, non-commented line. + if ((s.length() > 0) && (s.charAt(0) != '#')) + { + return (FrameworkFactory) Class.forName(s).newInstance(); + } + } + } + finally + { + if (br != null) br.close(); + } + } + + throw new Exception("Could not find framework factory."); + } + + /** + *

    + * Loads the properties in the system property file associated with the + * framework installation into System.setProperty(). These properties + * are not directly used by the framework in anyway. By default, the system + * property file is located in the conf/ directory of the Felix + * installation directory and is called "system.properties". The + * installation directory of Felix is assumed to be the parent directory of + * the felix.jar file as found on the system class path property. + * The precise file from which to load system properties can be set by + * initializing the "felix.system.properties" system property to an + * arbitrary URL. + *

    + **/ + public static void loadSystemProperties() + { + // The system properties file is either specified by a system + // property or it is in the same directory as the Felix JAR file. + // Try to load it from one of these places. + + // See if the property URL was specified as a property. + URL propURL = null; + String custom = System.getProperty(SYSTEM_PROPERTIES_PROP); + if (custom != null) + { + try + { + propURL = new URL(custom); + } + catch (MalformedURLException ex) + { + System.err.print("Main: " + ex); + return; + } + } + else + { + // Determine where the configuration directory is by figuring + // out where felix.jar is located on the system class path. + File confDir = null; + String classpath = System.getProperty("java.class.path"); + int index = classpath.toLowerCase().indexOf("felix.jar"); + int start = classpath.lastIndexOf(File.pathSeparator, index) + 1; + if (index >= start) + { + // Get the path of the felix.jar file. + String jarLocation = classpath.substring(start, index); + // Calculate the conf directory based on the parent + // directory of the felix.jar directory. + confDir = new File( + new File(new File(jarLocation).getAbsolutePath()).getParent(), + CONFIG_DIRECTORY); + } + else + { + // Can't figure it out so use the current directory as default. + confDir = new File(System.getProperty("user.dir"), CONFIG_DIRECTORY); + } + + try + { + propURL = new File(confDir, SYSTEM_PROPERTIES_FILE_VALUE).toURL(); + } + catch (MalformedURLException ex) + { + System.err.print("Main: " + ex); + return; + } + } + + // Read the properties file. + Properties props = new Properties(); + InputStream is = null; + try + { + is = propURL.openConnection().getInputStream(); + props.load(is); + is.close(); + } + catch (FileNotFoundException ex) + { + // Ignore file not found. + } + catch (Exception ex) + { + System.err.println( + "Main: Error loading system properties from " + propURL); + System.err.println("Main: " + ex); + try + { + if (is != null) is.close(); + } + catch (IOException ex2) + { + // Nothing we can do. + } + return; + } + + // Perform variable substitution on specified properties. + for (Enumeration e = props.propertyNames(); e.hasMoreElements(); ) + { + String name = (String) e.nextElement(); + System.setProperty(name, + Util.substVars(props.getProperty(name), name, null, null)); + } + } + + /** + *

    + * Loads the configuration properties in the configuration property file + * associated with the framework installation; these properties + * are accessible to the framework and to bundles and are intended + * for configuration purposes. By default, the configuration property + * file is located in the conf/ directory of the Felix + * installation directory and is called "config.properties". + * The installation directory of Felix is assumed to be the parent + * directory of the felix.jar file as found on the system class + * path property. The precise file from which to load configuration + * properties can be set by initializing the "felix.config.properties" + * system property to an arbitrary URL. + *

    + * @return A Properties instance or null if there was an error. + **/ + public static Map loadConfigProperties() + { + // The config properties file is either specified by a system + // property or it is in the conf/ directory of the Felix + // installation directory. Try to load it from one of these + // places. + + // See if the property URL was specified as a property. + URL propURL = null; + String custom = System.getProperty(CONFIG_PROPERTIES_PROP); + if (custom != null) + { + try + { + propURL = new URL(custom); + } + catch (MalformedURLException ex) + { + System.err.print("Main: " + ex); + return null; + } + } + else + { + // Determine where the configuration directory is by figuring + // out where felix.jar is located on the system class path. + File confDir = null; + String classpath = System.getProperty("java.class.path"); + int index = classpath.toLowerCase().indexOf("felix.jar"); + int start = classpath.lastIndexOf(File.pathSeparator, index) + 1; + if (index >= start) + { + // Get the path of the felix.jar file. + String jarLocation = classpath.substring(start, index); + // Calculate the conf directory based on the parent + // directory of the felix.jar directory. + confDir = new File( + new File(new File(jarLocation).getAbsolutePath()).getParent(), + CONFIG_DIRECTORY); + } + else + { + // Can't figure it out so use the current directory as default. + confDir = new File(System.getProperty("user.dir"), CONFIG_DIRECTORY); + } + + try + { + propURL = new File(confDir, CONFIG_PROPERTIES_FILE_VALUE).toURL(); + } + catch (MalformedURLException ex) + { + System.err.print("Main: " + ex); + return null; + } + } + + // Read the properties file. + Properties props = new Properties(); + InputStream is = null; + try + { + // Try to load config.properties. + is = propURL.openConnection().getInputStream(); + props.load(is); + is.close(); + } + catch (Exception ex) + { + // Try to close input stream if we have one. + try + { + if (is != null) is.close(); + } + catch (IOException ex2) + { + // Nothing we can do. + } + + return null; + } + + // Perform variable substitution for system properties and + // convert to dictionary. + Map map = new HashMap(); + for (Enumeration e = props.propertyNames(); e.hasMoreElements(); ) + { + String name = (String) e.nextElement(); + map.put(name, + Util.substVars(props.getProperty(name), name, null, props)); + } + + return map; + } + + public static void copySystemProperties(Map configProps) + { + for (Enumeration e = System.getProperties().propertyNames(); + e.hasMoreElements(); ) + { + String key = (String) e.nextElement(); + if (key.startsWith("felix.") || key.startsWith("org.osgi.framework.")) + { + configProps.put(key, System.getProperty(key)); + } + } + } +} diff --git a/classpath-antlrjavaparser/legal-classpath-antlrjavaparser.txt b/classpath-antlrjavaparser/legal-classpath-antlrjavaparser.txt new file mode 100644 index 000000000..d8d311608 --- /dev/null +++ b/classpath-antlrjavaparser/legal-classpath-antlrjavaparser.txt @@ -0,0 +1,19 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: Antlr Java Parser +Software Web Site: https://github.com/antlrjavaparser/antlr-java-parser/ +Effective License: GNU Lesser General Public License +License Info Page: http://www.gnu.org/licenses/lgpl-3.0.txt + +Antlr Java Parser is a runtime dependency of this module. It provides an +abstract syntax tree (AST) model for parsing Java source code. + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/classpath-antlrjavaparser/pom.xml b/classpath-antlrjavaparser/pom.xml new file mode 100644 index 000000000..84bbc50a0 --- /dev/null +++ b/classpath-antlrjavaparser/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.classpath.antlrjavaparser + bundle + Spring Roo - Classpath (Antlr Java Parser) + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + + com.github.antlrjavaparser + antlr-java-parser + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.antlr4-runtime + + + diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/CompilationUnitServices.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/CompilationUnitServices.java new file mode 100644 index 000000000..ef0a2b3d2 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/CompilationUnitServices.java @@ -0,0 +1,38 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import java.util.List; + +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.api.ImportDeclaration; +import com.github.antlrjavaparser.api.body.TypeDeclaration; + +/** + * An interface that enables Java Parser types to query relevant information + * about a compilation unit. + * + * @author Ben Alex + * @author James Tyrrell + * @since 1.0 + */ +public interface CompilationUnitServices { + + JavaPackage getCompilationUnitPackage(); + + /** + * @return the enclosing type (never null) + */ + JavaType getEnclosingTypeName(); + + List getImports(); + + /** + * @return the names of each inner type and the enclosing type (never null + * but may be empty) + */ + List getInnerTypes(); + + PhysicalTypeCategory getPhysicalTypeCategory(); +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeParsingService.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeParsingService.java new file mode 100644 index 000000000..decc2d581 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeParsingService.java @@ -0,0 +1,541 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import static org.springframework.roo.model.JavaType.OBJECT; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeParsingService; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserAnnotationMetadataBuilder; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserCommentMetadataBuilder; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserConstructorMetadataBuilder; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserFieldMetadataBuilder; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserMethodMetadataBuilder; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ImportMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.ASTHelper; +import com.github.antlrjavaparser.JavaParser; +import com.github.antlrjavaparser.ParseException; +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.ImportDeclaration; +import com.github.antlrjavaparser.api.PackageDeclaration; +import com.github.antlrjavaparser.api.TypeParameter; +import com.github.antlrjavaparser.api.body.BodyDeclaration; +import com.github.antlrjavaparser.api.body.ClassOrInterfaceDeclaration; +import com.github.antlrjavaparser.api.body.EnumConstantDeclaration; +import com.github.antlrjavaparser.api.body.EnumDeclaration; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.NameExpr; +import com.github.antlrjavaparser.api.expr.QualifiedNameExpr; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; + +@Component +@Service +public class JavaParserTypeParsingService implements TypeParsingService { + + @Reference MetadataService metadataService; + @Reference TypeLocationService typeLocationService; + + private void addEnumConstant(final List constants, + final JavaSymbolName name) { + // Determine location to insert + for (final EnumConstantDeclaration constant : constants) { + if (constant.getName().equals(name.getSymbolName())) { + throw new IllegalArgumentException("Enum constant '" + + name.getSymbolName() + "' already exists"); + } + } + final EnumConstantDeclaration newEntry = new EnumConstantDeclaration( + name.getSymbolName()); + constants.add(constants.size(), newEntry); + } + + @Override + public final String getCompilationUnitContents( + final ClassOrInterfaceTypeDetails cid) { + Validate.notNull(cid, "Class or interface type details are required"); + // Create a compilation unit to store the type to be created + final CompilationUnit compilationUnit = new CompilationUnit(); + + // NB: this import list is replaced at the end of this method by a + // sorted version + compilationUnit.setImports(new ArrayList()); + + if (!cid.getName().isDefaultPackage()) { + compilationUnit.setPackage(new PackageDeclaration(ASTHelper + .createNameExpr(cid.getName().getPackage() + .getFullyQualifiedPackageName()))); + } + + // Add the class of interface declaration to the compilation unit + final List types = new ArrayList(); + compilationUnit.setTypes(types); + + updateOutput(compilationUnit, null, cid, null); + + return compilationUnit.toString(); + } + + @Override + public ClassOrInterfaceTypeDetails getTypeAtLocation( + final String fileIdentifier, final String declaredByMetadataId, + final JavaType typeName) { + Validate.notBlank(fileIdentifier, "Compilation unit path required"); + Validate.notBlank(declaredByMetadataId, + "Declaring metadata ID required"); + Validate.notNull(typeName, "Java type to locate required"); + final File file = new File(fileIdentifier); + String typeContents = ""; + try { + typeContents = FileUtils.readFileToString(file); + } + catch (final IOException ignored) { + } + if (StringUtils.isBlank(typeContents)) { + return null; + } + return getTypeFromString(typeContents, declaredByMetadataId, typeName); + } + + @Override + public ClassOrInterfaceTypeDetails getTypeFromString( + final String fileContents, final String declaredByMetadataId, + final JavaType typeName) { + if (StringUtils.isBlank(fileContents)) { + return null; + } + + Validate.notBlank(declaredByMetadataId, + "Declaring metadata ID required"); + Validate.notNull(typeName, "Java type to locate required"); + try { + final CompilationUnit compilationUnit = JavaParser + .parse(new ByteArrayInputStream(fileContents.getBytes())); + final TypeDeclaration typeDeclaration = JavaParserUtils + .locateTypeDeclaration(compilationUnit, typeName); + if (typeDeclaration == null) { + return null; + } + return JavaParserClassOrInterfaceTypeDetailsBuilder.getInstance( + compilationUnit, null, typeDeclaration, + declaredByMetadataId, typeName, metadataService, + typeLocationService).build(); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + catch (final ParseException e) { + throw new IllegalStateException("Failed to parse " + typeName + + " : " + e.getMessage()); + } + } + + /** + * Appends the presented class to the end of the presented body + * declarations. The body declarations appear within the presented + * compilation unit. This is used to progressively build inner types. + * + * @param compilationUnit the work-in-progress compilation unit (required) + * @param enclosingCompilationUnitServices + * @param cid the new class to add (required) + * @param parent the class body declarations a subclass should be added to + * (may be null, which denotes a top-level type within the + * compilation unit) + */ + private void updateOutput(final CompilationUnit compilationUnit, + CompilationUnitServices enclosingCompilationUnitServices, + final ClassOrInterfaceTypeDetails cid, + final List parent) { + // Append the new imports this class declares + Validate.notNull( + compilationUnit.getImports(), + "Compilation unit imports should be non-null when producing type '%s'", + cid.getName()); + for (final ImportMetadata importType : cid.getRegisteredImports()) { + ImportDeclaration importDeclaration; + + if (!importType.isAsterisk()) { + NameExpr typeToImportExpr; + if (importType.getImportType().getEnclosingType() == null) { + typeToImportExpr = new QualifiedNameExpr(new NameExpr( + importType.getImportType().getPackage() + .getFullyQualifiedPackageName()), + importType.getImportType().getSimpleTypeName()); + } + else { + typeToImportExpr = new QualifiedNameExpr(new NameExpr( + importType.getImportType().getEnclosingType() + .getFullyQualifiedTypeName()), importType + .getImportType().getSimpleTypeName()); + } + + importDeclaration = new ImportDeclaration(typeToImportExpr, + importType.isStatic(), false); + } + else { + importDeclaration = new ImportDeclaration(new NameExpr( + importType.getImportPackage() + .getFullyQualifiedPackageName()), + importType.isStatic(), importType.isAsterisk()); + } + + JavaParserCommentMetadataBuilder.updateCommentsToJavaParser( + importDeclaration, importType.getCommentStructure()); + + compilationUnit.getImports().add(importDeclaration); + } + + // Create a class or interface declaration to represent this actual type + final int javaParserModifier = JavaParserUtils + .getJavaParserModifier(cid.getModifier()); + TypeDeclaration typeDeclaration; + ClassOrInterfaceDeclaration classOrInterfaceDeclaration; + + // Implements handling + final List implementsList = new ArrayList(); + for (final JavaType current : cid.getImplementsTypes()) { + implementsList.add(JavaParserUtils.getResolvedName(cid.getName(), + current, compilationUnit)); + } + + if (cid.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE + || cid.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS) { + final boolean isInterface = cid.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE; + + if (parent == null) { + // Top level type + typeDeclaration = new ClassOrInterfaceDeclaration( + javaParserModifier, isInterface, cid + .getName() + .getNameIncludingTypeParameters() + .replace( + cid.getName().getPackage() + .getFullyQualifiedPackageName() + + ".", "")); + classOrInterfaceDeclaration = (ClassOrInterfaceDeclaration) typeDeclaration; + } + else { + // Inner type + typeDeclaration = new ClassOrInterfaceDeclaration( + javaParserModifier, isInterface, cid.getName() + .getSimpleTypeName()); + classOrInterfaceDeclaration = (ClassOrInterfaceDeclaration) typeDeclaration; + + if (cid.getName().getParameters().size() > 0) { + classOrInterfaceDeclaration + .setTypeParameters(new ArrayList()); + + for (final JavaType param : cid.getName().getParameters()) { + NameExpr pNameExpr = JavaParserUtils + .importTypeIfRequired(cid.getName(), + compilationUnit.getImports(), param); + final String tempName = StringUtils.replace( + pNameExpr.toString(), param.getArgName() + + " extends ", "", 1); + pNameExpr = new NameExpr(tempName); + final ClassOrInterfaceType pResolvedName = JavaParserUtils + .getClassOrInterfaceType(pNameExpr); + classOrInterfaceDeclaration.getTypeParameters().add( + new TypeParameter(param.getArgName() + .getSymbolName(), Collections + .singletonList(pResolvedName))); + } + } + } + + // Superclass handling + final List extendsList = new ArrayList(); + for (final JavaType current : cid.getExtendsTypes()) { + if (!OBJECT.equals(current)) { + extendsList.add(JavaParserUtils.getResolvedName( + cid.getName(), current, compilationUnit)); + } + } + if (extendsList.size() > 0) { + classOrInterfaceDeclaration.setExtends(extendsList); + } + + // Implements handling + if (implementsList.size() > 0) { + classOrInterfaceDeclaration.setImplements(implementsList); + } + } + else { + typeDeclaration = new EnumDeclaration(javaParserModifier, cid + .getName().getSimpleTypeName()); + } + typeDeclaration.setMembers(new ArrayList()); + + Validate.notNull(typeDeclaration.getName(), + "Missing type declaration name for '%s'", cid.getName()); + + // If adding a new top-level type, must add it to the compilation unit + // types + Validate.notNull( + compilationUnit.getTypes(), + "Compilation unit types must not be null when attempting to add '%s'", + cid.getName()); + + if (parent == null) { + // Top-level class + compilationUnit.getTypes().add(typeDeclaration); + } + else { + // Inner class + parent.add(typeDeclaration); + } + + // If the enclosing CompilationUnitServices was not provided a default + // CompilationUnitServices needs to be created + if (enclosingCompilationUnitServices == null) { + // Create a compilation unit so that we can use JavaType*Metadata + // static methods directly + enclosingCompilationUnitServices = new CompilationUnitServices() { + @Override + public JavaPackage getCompilationUnitPackage() { + return cid.getName().getPackage(); + } + + @Override + public JavaType getEnclosingTypeName() { + return cid.getName(); + } + + @Override + public List getImports() { + return compilationUnit.getImports(); + } + + @Override + public List getInnerTypes() { + return compilationUnit.getTypes(); + } + + @Override + public PhysicalTypeCategory getPhysicalTypeCategory() { + return cid.getPhysicalTypeCategory(); + } + }; + } + + final CompilationUnitServices finalCompilationUnitServices = enclosingCompilationUnitServices; + // A hybrid CompilationUnitServices must be provided that references the + // enclosing types imports and package + final CompilationUnitServices compilationUnitServices = new CompilationUnitServices() { + @Override + public JavaPackage getCompilationUnitPackage() { + return finalCompilationUnitServices.getCompilationUnitPackage(); + } + + @Override + public JavaType getEnclosingTypeName() { + return cid.getName(); + } + + @Override + public List getImports() { + return finalCompilationUnitServices.getImports(); + } + + @Override + public List getInnerTypes() { + return compilationUnit.getTypes(); + } + + @Override + public PhysicalTypeCategory getPhysicalTypeCategory() { + return cid.getPhysicalTypeCategory(); + } + }; + + // Add type annotations + final List annotations = new ArrayList(); + typeDeclaration.setAnnotations(annotations); + for (final AnnotationMetadata candidate : cid.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, candidate); + } + + // Add enum constants and interfaces + if (typeDeclaration instanceof EnumDeclaration + && cid.getEnumConstants().size() > 0) { + final EnumDeclaration enumDeclaration = (EnumDeclaration) typeDeclaration; + + final List constants = new ArrayList(); + enumDeclaration.setEntries(constants); + + for (final JavaSymbolName constant : cid.getEnumConstants()) { + addEnumConstant(constants, constant); + } + + // Implements handling + if (implementsList.size() > 0) { + enumDeclaration.setImplements(implementsList); + } + } + + // Add fields + for (final FieldMetadata candidate : cid.getDeclaredFields()) { + JavaParserFieldMetadataBuilder.addField(compilationUnitServices, + typeDeclaration.getMembers(), candidate); + } + + // Add constructors + for (final ConstructorMetadata candidate : cid + .getDeclaredConstructors()) { + JavaParserConstructorMetadataBuilder.addConstructor( + compilationUnitServices, typeDeclaration.getMembers(), + candidate, null); + } + + // Add methods + for (final MethodMetadata candidate : cid.getDeclaredMethods()) { + JavaParserMethodMetadataBuilder.addMethod(compilationUnitServices, + typeDeclaration.getMembers(), candidate, null); + } + + // Add inner types + for (final ClassOrInterfaceTypeDetails candidate : cid + .getDeclaredInnerTypes()) { + updateOutput(compilationUnit, compilationUnitServices, candidate, + typeDeclaration.getMembers()); + } + + final HashSet imported = new HashSet(); + final ArrayList imports = new ArrayList(); + for (final ImportDeclaration importDeclaration : compilationUnit + .getImports()) { + JavaPackage importPackage = null; + JavaType importType = null; + if (importDeclaration.isAsterisk()) { + importPackage = new JavaPackage(importDeclaration.getName() + .toString()); + } + else { + importType = new JavaType(importDeclaration.getName() + .toString()); + importPackage = importType.getPackage(); + } + + if (importPackage.equals(cid.getName().getPackage()) + && importDeclaration.isAsterisk()) { + continue; + } + + if (importPackage.equals(cid.getName().getPackage()) + && importType != null + && importType.getEnclosingType() == null) { + continue; + } + + if (importType != null && importType.equals(cid.getName())) { + continue; + } + + if (!imported.contains(importDeclaration.getName().toString())) { + imports.add(importDeclaration); + imported.add(importDeclaration.getName().toString()); + } + } + + Collections.sort(imports, new Comparator() { + @Override + public int compare(final ImportDeclaration importDeclaration, + final ImportDeclaration importDeclaration1) { + return importDeclaration.getName().toString() + .compareTo(importDeclaration1.getName().toString()); + } + }); + + compilationUnit.setImports(imports); + } + + @Override + public String updateAndGetCompilationUnitContents( + final String fileIdentifier, final ClassOrInterfaceTypeDetails cid) { + // Validate parameters + Validate.notBlank(fileIdentifier, "Oringinal unit path required"); + Validate.notNull(cid, "Type details required"); + + // Load original compilation unit from file + final File file = new File(fileIdentifier); + String fileContents = ""; + try { + fileContents = FileUtils.readFileToString(file); + } + catch (final IOException ignored) { + } + if (StringUtils.isBlank(fileContents)) { + return getCompilationUnitContents(cid); + } + CompilationUnit compilationUnit; + try { + compilationUnit = JavaParser.parse(new ByteArrayInputStream( + fileContents.getBytes())); + + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + catch (final ParseException e) { + throw new IllegalStateException(e); + } + + // Load new compilation unit from cid information + final String cidContents = getCompilationUnitContents(cid); + CompilationUnit cidCompilationUnit; + try { + cidCompilationUnit = JavaParser.parse(new ByteArrayInputStream( + cidContents.getBytes())); + + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + catch (final ParseException e) { + throw new IllegalStateException(e); + } + + // Update package + if (!compilationUnit.getPackage().getName().getName() + .equals(cidCompilationUnit.getPackage().getName().getName())) { + compilationUnit.setPackage(cidCompilationUnit.getPackage()); + } + + // Update imports + UpdateCompilationUnitUtils.updateCompilationUnitImports( + compilationUnit, cidCompilationUnit); + + // Update types + UpdateCompilationUnitUtils.updateCompilationUnitTypes(compilationUnit, + cidCompilationUnit); + + // Return new contents + return compilationUnit.toString(); + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeResolutionService.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeResolutionService.java new file mode 100644 index 000000000..3d455c96b --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeResolutionService.java @@ -0,0 +1,101 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeResolutionService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.JavaParser; +import com.github.antlrjavaparser.ParseException; +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.body.TypeDeclaration; + +@Component +@Service +public class JavaParserTypeResolutionService implements TypeResolutionService { + + @Override + public final JavaType getJavaType(final String fileIdentifier) { + Validate.notBlank(fileIdentifier, "Compilation unit path required"); + Validate.isTrue(new File(fileIdentifier).exists(), + "The file doesn't exist"); + Validate.isTrue(new File(fileIdentifier).isFile(), + "The identifier doesn't represent a file"); + try { + final File file = new File(fileIdentifier); + String typeContents = ""; + try { + typeContents = FileUtils.readFileToString(file); + } + catch (final IOException ignored) { + } + if (StringUtils.isBlank(typeContents)) { + return null; + } + final CompilationUnit compilationUnit = JavaParser + .parse(new ByteArrayInputStream(typeContents.getBytes())); + final String typeName = fileIdentifier.substring( + fileIdentifier.lastIndexOf(File.separator) + 1, + fileIdentifier.lastIndexOf(".")); + for (final TypeDeclaration typeDeclaration : compilationUnit + .getTypes()) { + if (typeName.equals(typeDeclaration.getName())) { + return new JavaType(compilationUnit.getPackage().getName() + .getName() + + "." + typeDeclaration.getName()); + } + } + return null; + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + catch (final ParseException e) { + throw new IllegalStateException("Failed to parse " + fileIdentifier + + " : " + e.getMessage()); + } + } + + @Override + public final JavaPackage getPackage(final String fileIdentifier) { + Validate.notBlank(fileIdentifier, "Compilation unit path required"); + Validate.isTrue(new File(fileIdentifier).exists(), + "The file doesn't exist"); + Validate.isTrue(new File(fileIdentifier).isFile(), + "The identifier doesn't represent a file"); + try { + final File file = new File(fileIdentifier); + String typeContents = ""; + try { + typeContents = FileUtils.readFileToString(file); + } + catch (final IOException ignored) { + } + if (StringUtils.isBlank(typeContents)) { + return null; + } + final CompilationUnit compilationUnit = JavaParser + .parse(new ByteArrayInputStream(typeContents.getBytes())); + if (compilationUnit == null || compilationUnit.getPackage() == null) { + return null; + } + return new JavaPackage(compilationUnit.getPackage().getName() + .toString()); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + catch (final ParseException e) { + throw new IllegalStateException("Failed to parse " + fileIdentifier + + " : " + e.getMessage()); + } + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserUtils.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserUtils.java new file mode 100644 index 000000000..b5a5f2d87 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserUtils.java @@ -0,0 +1,1074 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import static org.springframework.roo.model.JavaType.OBJECT; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; + +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.ImportDeclaration; +import com.github.antlrjavaparser.api.TypeParameter; +import com.github.antlrjavaparser.api.body.ClassOrInterfaceDeclaration; +import com.github.antlrjavaparser.api.body.ModifierSet; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.ClassExpr; +import com.github.antlrjavaparser.api.expr.Expression; +import com.github.antlrjavaparser.api.expr.FieldAccessExpr; +import com.github.antlrjavaparser.api.expr.MarkerAnnotationExpr; +import com.github.antlrjavaparser.api.expr.NameExpr; +import com.github.antlrjavaparser.api.expr.NormalAnnotationExpr; +import com.github.antlrjavaparser.api.expr.QualifiedNameExpr; +import com.github.antlrjavaparser.api.expr.SingleMemberAnnotationExpr; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; +import com.github.antlrjavaparser.api.type.PrimitiveType; +import com.github.antlrjavaparser.api.type.PrimitiveType.Primitive; +import com.github.antlrjavaparser.api.type.ReferenceType; +import com.github.antlrjavaparser.api.type.Type; +import com.github.antlrjavaparser.api.type.VoidType; +import com.github.antlrjavaparser.api.type.WildcardType; + +/** + * Assists with the usage of Java Parser. + *

    + * This class is for internal use by the Java Parser module and should NOT be + * used by other code. + * + * @author Ben Alex + * @since 1.0 + */ +public final class JavaParserUtils { + + /** + * Constructor is private to prevent instantiation + */ + private JavaParserUtils() { + } + + /** + * Converts the indicated {@link NameExpr} into a + * {@link ClassOrInterfaceType}. + *

    + * Note that no effort is made to manage imports etc. + * + * @param nameExpr to convert (required) + * @return the corresponding {@link ClassOrInterfaceType} (never null) + */ + public static ClassOrInterfaceType getClassOrInterfaceType( + final NameExpr nameExpr) { + Validate.notNull(nameExpr, "Java type required"); + if (nameExpr instanceof QualifiedNameExpr) { + final QualifiedNameExpr qne = (QualifiedNameExpr) nameExpr; + if (StringUtils.isNotBlank(qne.getQualifier().getName())) { + return new ClassOrInterfaceType(qne.getQualifier().getName() + + "." + qne.getName()); + } + return new ClassOrInterfaceType(qne.getName()); + } + return new ClassOrInterfaceType(nameExpr.getName()); + } + + /** + * Looks up the import declaration applicable to the presented name + * expression. + *

    + * If a fully-qualified name is passed to this method, the corresponding + * import will be evaluated for a complete match. If a simple name is passed + * to this method, the corresponding import will be evaluated if its simple + * name matches. This therefore reflects the normal Java semantics for using + * simple type names that have been imported. + * + * @param compilationUnitServices the types in the compilation unit + * (required) + * @param nameExpr the expression to locate an import for (which would + * generally be a {@link NameExpr} and thus not have a package + * identifier; required) + * @return the relevant import, or null if there is no import for the + * expression + */ + private static ImportDeclaration getImportDeclarationFor( + final CompilationUnitServices compilationUnitServices, + final NameExpr nameExpr) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(nameExpr, "Name expression required"); + + final List imports = compilationUnitServices + .getImports(); + + for (final ImportDeclaration candidate : imports) { + final NameExpr candidateNameExpr = candidate.getName(); + if (!candidate.toString().contains("*")) { + Validate.isInstanceOf( + QualifiedNameExpr.class, + candidateNameExpr, + "Expected import '%s' to use a fully-qualified type name", + candidate); + } + if (nameExpr instanceof QualifiedNameExpr) { + // User is asking for a fully-qualified name; let's see if there + // is a full match + if (isEqual(nameExpr, candidateNameExpr)) { + return candidate; + } + } + else { + // User is not asking for a fully-qualified name, so let's do a + // simple name comparison that discards the import's + // qualified-name package + if (candidateNameExpr.getName().equals(nameExpr.getName())) { + return candidate; + } + } + } + return null; + } + + /** + * Converts a JDK {@link Modifier} integer into the equivalent Java Parser + * modifier. + * + * @param modifiers the JDK int + * @return the equivalent Java Parser int + */ + public static int getJavaParserModifier(final int modifiers) { + int result = 0; + if (Modifier.isAbstract(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.ABSTRACT, result); + } + if (Modifier.isFinal(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.FINAL, result); + } + if (Modifier.isInterface(modifiers)) { + // Unsupported by Java Parser ModifierSet + } + if (Modifier.isNative(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.NATIVE, result); + } + if (Modifier.isPrivate(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.PRIVATE, result); + } + if (Modifier.isProtected(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.PROTECTED, result); + } + if (Modifier.isPublic(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.PUBLIC, result); + } + if (Modifier.isStatic(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.STATIC, result); + } + if (Modifier.isStrict(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.STRICTFP, result); + } + if (Modifier.isSynchronized(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.SYNCHRONIZED, result); + } + if (Modifier.isTransient(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.TRANSIENT, result); + } + if (Modifier.isVolatile(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.VOLATILE, result); + } + return result; + } + + /** + * Resolves the effective {@link JavaType} a {@link NameExpr} represents. + *

    + * You should use {@link #getJavaType(CompilationUnitServices, Type, Set)} + * where possible so that type arguments are preserved (a {@link NameExpr} + * does not contain type arguments). + *

    + * A name expression can be either qualified or unqualified. + *

    + * If a name expression is qualified and the qualification starts with a + * lowercase letter, that represents the fully-qualified name. If the + * qualification starts with an uppercase letter, the package name is + * prepended to the qualifier. + *

    + * If a name expression is unqualified, the imports are scanned. If the + * unqualified name expression is found in the imports, that import + * declaration represents the fully-qualified name. If the unqualified name + * expression is not found in the imports, it indicates the name to find is + * either in the same package as the qualified name expression, or the type + * relates to a member of java.lang. If part of java.lang, the fully + * qualified name is treated as part of java.lang. Otherwise the compilation + * unit package plus unqualified name expression represents the fully + * qualified name expression. + * + * @param compilationUnitServices for package management (required) + * @param nameToFind to locate (required) + * @param typeParameters names to consider type parameters (can be null if + * there are none) + * @return the effective Java type (never null) + */ + public static JavaType getJavaType( + final CompilationUnitServices compilationUnitServices, + final NameExpr nameToFind, final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(nameToFind, "Name to find is required"); + + final JavaPackage compilationUnitPackage = compilationUnitServices + .getCompilationUnitPackage(); + + if (nameToFind instanceof QualifiedNameExpr) { + final QualifiedNameExpr qne = (QualifiedNameExpr) nameToFind; + + // Handle qualified name expressions that are related to inner types + // (eg Foo.Bar) + final NameExpr qneQualifier = qne.getQualifier(); + final NameExpr enclosedBy = getNameExpr(compilationUnitServices + .getEnclosingTypeName().getSimpleTypeName()); + if (isEqual(qneQualifier, enclosedBy)) { + // This qualified name expression is simply an inner type + // reference + final String name = compilationUnitServices + .getEnclosingTypeName().getFullyQualifiedTypeName() + + "." + nameToFind.getName(); + return new JavaType(name, + compilationUnitServices.getEnclosingTypeName()); + } + + // Refers to a different enclosing type, so calculate the package + // name based on convention of an uppercase letter denotes same + // package (ROO-1210) + if (qne.toString().length() > 1 + && Character.isUpperCase(qne.toString().charAt(0))) { + // First letter is uppercase, so this likely requires prepending + // of some package name + final ImportDeclaration importDeclaration = getImportDeclarationFor( + compilationUnitServices, qne.getQualifier()); + if (importDeclaration == null) { + if (!compilationUnitPackage.getFullyQualifiedPackageName() + .equals("")) { + // It was not imported, so let's assume it's in the same + // package + return new JavaType(compilationUnitServices + .getCompilationUnitPackage() + .getFullyQualifiedPackageName() + + "." + qne.toString()); + } + } + else { + return new JavaType(importDeclaration.getName() + "." + + qne.getName()); + } + + // This name expression (which contains a dot) had its qualifier + // imported, so let's use the import + } + else { + // First letter is lowercase, so the reference already includes + // a package + return new JavaType(qne.toString()); + } + } + + if ("?".equals(nameToFind.getName())) { + return new JavaType(OBJECT.getFullyQualifiedTypeName(), 0, + DataType.TYPE, JavaType.WILDCARD_NEITHER, null); + } + + // Unqualified name detected, so check if it's in the type parameter + // list + if (typeParameters != null + && typeParameters.contains(new JavaSymbolName(nameToFind + .getName()))) { + return new JavaType(nameToFind.getName(), 0, DataType.VARIABLE, + null, null); + } + + // Check if we are looking for the enclosingType itself + final NameExpr enclosingTypeName = getNameExpr(compilationUnitServices + .getEnclosingTypeName().getSimpleTypeName()); + if (isEqual(enclosingTypeName, nameToFind)) { + return compilationUnitServices.getEnclosingTypeName(); + } + + // We are searching for a non-qualified name expression (nameToFind), so + // check if the compilation unit itself declares that type + for (final TypeDeclaration internalType : compilationUnitServices + .getInnerTypes()) { + final NameExpr nameExpr = getNameExpr(internalType.getName()); + if (isEqual(nameExpr, nameToFind)) { + // Found, so now we need to convert the internalType to a proper + // JavaType + final String name = compilationUnitServices + .getEnclosingTypeName().getFullyQualifiedTypeName() + + "." + nameToFind.getName(); + return new JavaType(name); + } + } + + final ImportDeclaration importDeclaration = getImportDeclarationFor( + compilationUnitServices, nameToFind); + if (importDeclaration == null) { + if (JdkJavaType.isPartOfJavaLang(nameToFind.getName())) { + return new JavaType("java.lang." + nameToFind.getName()); + } + final String name = compilationUnitPackage + .getFullyQualifiedPackageName().equals("") ? nameToFind + .getName() : compilationUnitPackage + .getFullyQualifiedPackageName() + + "." + + nameToFind.getName(); + return new JavaType(name); + } + + return new JavaType(importDeclaration.getName().toString()); + } + + /** + * Resolves the effective {@link JavaType} a {@link Type} represents. A + * {@link Type} includes low-level types such as void, arrays and + * primitives. + * + * @param compilationUnitServices to use for package resolution (required) + * @param type to locate (required) + * @param typeParameters names to consider type parameters (can be null if + * there are none) + * @return the {@link JavaType}, with proper indication of primitive and + * array status (never null) + */ + public static JavaType getJavaType( + final CompilationUnitServices compilationUnitServices, + final Type type, final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(type, "The reference type must be provided"); + + if (type instanceof VoidType) { + return JavaType.VOID_PRIMITIVE; + } + + int array = 0; + + Type internalType = type; + if (internalType instanceof ReferenceType) { + array = ((ReferenceType) internalType).getArrayCount(); + if (array > 0) { + internalType = ((ReferenceType) internalType).getType(); + } + } + + if (internalType instanceof PrimitiveType) { + final PrimitiveType pt = (PrimitiveType) internalType; + if (pt.getType().equals(Primitive.Boolean)) { + return new JavaType(Boolean.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Char)) { + return new JavaType(Character.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Byte)) { + return new JavaType(Byte.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Short)) { + return new JavaType(Short.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Int)) { + return new JavaType(Integer.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Long)) { + return new JavaType(Long.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Float)) { + return new JavaType(Float.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Double)) { + return new JavaType(Double.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + throw new IllegalStateException("Unsupported primitive '" + + pt.getType() + "'"); + } + + if (internalType instanceof WildcardType) { + // We only provide very primitive support for wildcard types; Roo + // only needs metadata at the end of the day, + // not complete binding support from an AST + final WildcardType wt = (WildcardType) internalType; + if (wt.getSuper() != null) { + final ReferenceType rt = wt.getSuper(); + final ClassOrInterfaceType cit = (ClassOrInterfaceType) rt + .getType(); + final JavaType effectiveType = getJavaTypeNow( + compilationUnitServices, cit, typeParameters); + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + rt.getArrayCount(), effectiveType.getDataType(), + JavaType.WILDCARD_SUPER, effectiveType.getParameters()); + } + else if (wt.getExtends() != null) { + final ReferenceType rt = wt.getExtends(); + final ClassOrInterfaceType cit = (ClassOrInterfaceType) rt + .getType(); + final JavaType effectiveType = getJavaTypeNow( + compilationUnitServices, cit, typeParameters); + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + rt.getArrayCount(), effectiveType.getDataType(), + JavaType.WILDCARD_EXTENDS, + effectiveType.getParameters()); + } + else { + return new JavaType(OBJECT.getFullyQualifiedTypeName(), 0, + DataType.TYPE, JavaType.WILDCARD_NEITHER, null); + } + } + + ClassOrInterfaceType cit; + if (internalType instanceof ClassOrInterfaceType) { + cit = (ClassOrInterfaceType) internalType; + } + else if (internalType instanceof ReferenceType) { + cit = (ClassOrInterfaceType) ((ReferenceType) type).getType(); + } + else { + throw new IllegalStateException("The presented type '" + + internalType.getClass() + "' with value '" + internalType + + "' is unsupported by JavaParserUtils"); + } + + final JavaType effectiveType = getJavaTypeNow(compilationUnitServices, + cit, typeParameters); + if (array > 0) { + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + array, effectiveType.getDataType(), + effectiveType.getArgName(), effectiveType.getParameters()); + } + + return effectiveType; + } + + /** + * Resolves the effective {@link JavaType} a + * {@link ClassOrInterfaceDeclaration} represents, including any type + * parameters. + * + * @param compilationUnitServices for package management (required) + * @param typeDeclaration the type declaration to resolve (required) + * @return the effective Java type (never null) + */ + public static JavaType getJavaType( + final CompilationUnitServices compilationUnitServices, + final TypeDeclaration typeDeclaration) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(typeDeclaration, "Type declaration required"); + + // Convert the ClassOrInterfaceDeclaration name into a JavaType + final NameExpr nameExpr = getNameExpr(typeDeclaration.getName()); + final JavaType effectiveType = getJavaType(compilationUnitServices, + nameExpr, null); + + final List parameterTypes = new ArrayList(); + if (typeDeclaration instanceof ClassOrInterfaceDeclaration) { + final ClassOrInterfaceDeclaration clazz = (ClassOrInterfaceDeclaration) typeDeclaration; + // Populate JavaType with type parameters + final List typeParameters = clazz + .getTypeParameters(); + if (typeParameters != null) { + final Set locatedTypeParameters = new HashSet(); + for (final TypeParameter candidate : typeParameters) { + final JavaSymbolName currentTypeParam = new JavaSymbolName( + candidate.getName()); + locatedTypeParameters.add(currentTypeParam); + JavaType javaType = null; + if (candidate.getTypeBound() == null) { + javaType = new JavaType( + OBJECT.getFullyQualifiedTypeName(), 0, + DataType.TYPE, currentTypeParam, null); + } + else { + final ClassOrInterfaceType cit = candidate + .getTypeBound().get(0); + javaType = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, cit, + locatedTypeParameters); + javaType = new JavaType( + javaType.getFullyQualifiedTypeName(), + javaType.getArray(), javaType.getDataType(), + currentTypeParam, javaType.getParameters()); + } + parameterTypes.add(javaType); + } + } + } + + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + effectiveType.getArray(), effectiveType.getDataType(), null, + parameterTypes); + } + + /** + * Resolves the effective {@link JavaType} a {@link ClassOrInterfaceType} + * represents, including any type arguments. + * + * @param compilationUnitServices for package management (required) + * @param cit the class or interface type to resolve (required) + * @return the effective Java type (never null) + */ + public static JavaType getJavaTypeNow( + final CompilationUnitServices compilationUnitServices, + final ClassOrInterfaceType cit, + final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(cit, "ClassOrInterfaceType required"); + + final JavaPackage compilationUnitPackage = compilationUnitServices + .getCompilationUnitPackage(); + Validate.notNull(compilationUnitPackage, + "Compilation unit package required"); + + String typeName = cit.getName(); + ClassOrInterfaceType scope = cit.getScope(); + while (scope != null) { + typeName = scope.getName() + "." + typeName; + scope = scope.getScope(); + } + final NameExpr nameExpr = getNameExpr(typeName); + + final JavaType effectiveType = getJavaType(compilationUnitServices, + nameExpr, typeParameters); + + // Handle any type arguments + final List parameterTypes = new ArrayList(); + if (cit.getTypeArgs() != null) { + for (final Type ta : cit.getTypeArgs()) { + parameterTypes.add(getJavaType(compilationUnitServices, ta, + typeParameters)); + } + } + + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + effectiveType.getArray(), effectiveType.getDataType(), null, + parameterTypes); + } + + /** + * Converts a Java Parser modifier integer into a JDK {@link Modifier} + * integer. + * + * @param modifiers the Java Parser int + * @return the equivalent JDK int + */ + public static int getJdkModifier(final int modifiers) { + int result = 0; + if (ModifierSet.isAbstract(modifiers)) { + result |= Modifier.ABSTRACT; + } + if (ModifierSet.isFinal(modifiers)) { + result |= Modifier.FINAL; + } + if (ModifierSet.isNative(modifiers)) { + result |= Modifier.NATIVE; + } + if (ModifierSet.isPrivate(modifiers)) { + result |= Modifier.PRIVATE; + } + if (ModifierSet.isProtected(modifiers)) { + result |= Modifier.PROTECTED; + } + if (ModifierSet.isPublic(modifiers)) { + result |= Modifier.PUBLIC; + } + if (ModifierSet.isStatic(modifiers)) { + result |= Modifier.STATIC; + } + if (ModifierSet.isStrictfp(modifiers)) { + result |= Modifier.STRICT; + } + if (ModifierSet.isSynchronized(modifiers)) { + result |= Modifier.SYNCHRONIZED; + } + if (ModifierSet.isTransient(modifiers)) { + result |= Modifier.TRANSIENT; + } + if (ModifierSet.isVolatile(modifiers)) { + result |= Modifier.VOLATILE; + } + return result; + } + + /** + * Obtains the name expression ({@link NameExpr}) for the passed + * {@link AnnotationExpr}, which is the annotation's type. + * + * @param annotationExpr to retrieve the type name from (required) + * @return the name (never null) + */ + public static NameExpr getNameExpr(final AnnotationExpr annotationExpr) { + Validate.notNull(annotationExpr, "Annotation expression required"); + if (annotationExpr instanceof MarkerAnnotationExpr) { + final MarkerAnnotationExpr a = (MarkerAnnotationExpr) annotationExpr; + final NameExpr nameToFind = a.getName(); + Validate.notNull(nameToFind, + "Unable to determine annotation name from '%s'", + annotationExpr); + return nameToFind; + } + else if (annotationExpr instanceof SingleMemberAnnotationExpr) { + final SingleMemberAnnotationExpr a = (SingleMemberAnnotationExpr) annotationExpr; + final NameExpr nameToFind = a.getName(); + Validate.notNull(nameToFind, + "Unable to determine annotation name from '%s'", + annotationExpr); + return nameToFind; + } + else if (annotationExpr instanceof NormalAnnotationExpr) { + final NormalAnnotationExpr a = (NormalAnnotationExpr) annotationExpr; + final NameExpr nameToFind = a.getName(); + Validate.notNull(nameToFind, + "Unable to determine annotation name from '%s'", + annotationExpr); + return nameToFind; + } + throw new UnsupportedOperationException( + "Unknown annotation expression type '" + + annotationExpr.getClass().getName() + "'"); + } + + /** + * Converts the presented class name into a name expression (either a + * {@link NameExpr} or {@link QualifiedNameExpr} depending on whether a + * package was presented). + * + * @param className to convert (required; can be fully qualified or simple + * name only) + * @return a compatible expression (never returns null) + */ + public static NameExpr getNameExpr(final String className) { + Validate.notBlank(className, "Class name required"); + if (className.contains(".")) { + final int offset = className.lastIndexOf("."); + final String packageName = className.substring(0, offset); + final String typeName = className.substring(offset + 1); + return new QualifiedNameExpr(new NameExpr(packageName), typeName); + } + return new NameExpr(className); + } + + /** + * Converts the indicated {@link JavaType} into a {@link ReferenceType}. + *

    + * Note that no effort is made to manage imports etc. + * + * @param nameExpr to convert (required) + * @return the corresponding {@link ReferenceType} (never null) + */ + public static ReferenceType getReferenceType(final NameExpr nameExpr) { + Validate.notNull(nameExpr, "Java type required"); + return new ReferenceType(getClassOrInterfaceType(nameExpr)); + } + + public static ClassOrInterfaceType getResolvedName(final JavaType target, + final JavaType current, final CompilationUnit compilationUnit) { + final NameExpr nameExpr = JavaParserUtils.importTypeIfRequired(target, + compilationUnit.getImports(), current); + final ClassOrInterfaceType resolvedName = JavaParserUtils + .getClassOrInterfaceType(nameExpr); + if (current.getParameters() != null + && current.getParameters().size() > 0) { + resolvedName.setTypeArgs(new ArrayList()); + for (final JavaType param : current.getParameters()) { + resolvedName.getTypeArgs().add( + getResolvedName(target, param, compilationUnit)); + } + } + + return resolvedName; + } + + public static Type getResolvedName(final JavaType target, + final JavaType current, + final CompilationUnitServices compilationUnit) { + final NameExpr nameExpr = JavaParserUtils.importTypeIfRequired(target, + compilationUnit.getImports(), current); + final ClassOrInterfaceType resolvedName = JavaParserUtils + .getClassOrInterfaceType(nameExpr); + if (current.getParameters() != null + && current.getParameters().size() > 0) { + resolvedName.setTypeArgs(new ArrayList()); + for (final JavaType param : current.getParameters()) { + resolvedName.getTypeArgs().add( + getResolvedName(target, param, compilationUnit)); + } + } + + if (current.getArray() > 0) { + // Primitives includes array declaration in resolvedName + if (!current.isPrimitive()) { + return new ReferenceType(resolvedName, current.getArray()); + } + } + + return resolvedName; + } + + /** + * Given a primitive type, computes the corresponding Java Parser type. + *

    + * Presenting a non-primitive type to this method will throw an exception. + * If you have a non-primitive type, use + * {@link #importTypeIfRequired(JavaType, List, JavaType)} and then present + * the {@link NameExpr} it returns to + * {@link #getClassOrInterfaceType(NameExpr)}. + * + * @param javaType a primitive type (required, and must be primitive) + * @return the equivalent Java Parser {@link Type} + */ + public static Type getType(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + Validate.isTrue(javaType.isPrimitive(), + "Java type must be primitive to be presented to this method"); + if (javaType.equals(JavaType.VOID_PRIMITIVE)) { + return new VoidType(); + } + else if (javaType.equals(JavaType.BOOLEAN_PRIMITIVE)) { + return new PrimitiveType(Primitive.Boolean); + } + else if (javaType.equals(JavaType.BYTE_PRIMITIVE)) { + return new PrimitiveType(Primitive.Byte); + } + else if (javaType.equals(JavaType.CHAR_PRIMITIVE)) { + return new PrimitiveType(Primitive.Char); + } + else if (javaType.equals(JavaType.DOUBLE_PRIMITIVE)) { + return new PrimitiveType(Primitive.Double); + } + else if (javaType.equals(JavaType.FLOAT_PRIMITIVE)) { + return new PrimitiveType(Primitive.Float); + } + else if (javaType.equals(JavaType.INT_PRIMITIVE)) { + return new PrimitiveType(Primitive.Int); + } + else if (javaType.equals(JavaType.LONG_PRIMITIVE)) { + return new PrimitiveType(Primitive.Long); + } + else if (javaType.equals(JavaType.SHORT_PRIMITIVE)) { + return new PrimitiveType(Primitive.Short); + } + throw new IllegalStateException("Unknown primitive " + javaType); + } + + /** + * Recognises {@link Expression}s of type {@link FieldAccessExpr} and + * {@link ClassExpr} and automatically imports them if required, returning + * the correct {@link Expression} that should subsequently be used. + *

    + * Even if an {@link Expression} is not resolved by this method into a type + * and/or imported, the method guarantees to always return an + * {@link Expression} that the caller can subsequently use in place of the + * passed {@link Expression}. In practical terms, the {@link Expression} + * passed to this method will be returned unless the type was already + * imported, just imported, or represented a java.lang type. + * + * @param targetType the compilation unit target type (required) + * @param imports the existing imports (required) + * @param value that expression, which need not necessarily be resolvable to + * a type (required) + * @return the expression to now use, as appropriately resolved (never + * returns null) + */ + public static Expression importExpressionIfRequired( + final JavaType targetType, final List imports, + final Expression value) { + Validate.notNull(targetType, "Target type required"); + Validate.notNull(imports, "Imports required"); + Validate.notNull(value, "Expression value required"); + + if (value instanceof FieldAccessExpr) { + final Expression scope = ((FieldAccessExpr) value).getScope(); + final String field = ((FieldAccessExpr) value).getField(); + if (scope instanceof QualifiedNameExpr) { + final String packageName = ((QualifiedNameExpr) scope) + .getQualifier().getName(); + final String simpleName = ((QualifiedNameExpr) scope).getName(); + final String fullyQualifiedName = packageName + "." + + simpleName; + final JavaType javaType = new JavaType(fullyQualifiedName); + final NameExpr nameToUse = importTypeIfRequired(targetType, + imports, javaType); + if (!(nameToUse instanceof QualifiedNameExpr)) { + return new FieldAccessExpr(nameToUse, field); + } + } + } + else if (value instanceof ClassExpr) { + final Type type = ((ClassExpr) value).getType(); + if (type instanceof ClassOrInterfaceType) { + final JavaType javaType = new JavaType( + ((ClassOrInterfaceType) type).getName()); + final NameExpr nameToUse = importTypeIfRequired(targetType, + imports, javaType); + if (!(nameToUse instanceof QualifiedNameExpr)) { + return new ClassExpr(new ClassOrInterfaceType( + javaType.getSimpleTypeName())); + } + } + else if (type instanceof ReferenceType + && ((ReferenceType) type).getType() instanceof ClassOrInterfaceType) { + final ClassOrInterfaceType cit = (ClassOrInterfaceType) ((ReferenceType) type) + .getType(); + final JavaType javaType = new JavaType(cit.getName()); + final NameExpr nameToUse = importTypeIfRequired(targetType, + imports, javaType); + if (!(nameToUse instanceof QualifiedNameExpr)) { + return new ClassExpr(new ClassOrInterfaceType( + javaType.getSimpleTypeName())); + } + } + } + + // Make no changes + return value; + } + + public static ReferenceType importParametersForType( + final JavaType targetType, final List imports, + final JavaType typeToImport) { + Validate.notNull(targetType, "Target type is required"); + Validate.notNull(imports, "Compilation unit imports required"); + Validate.notNull(typeToImport, "Java type to import is required"); + + final ClassOrInterfaceType cit = getClassOrInterfaceType(importTypeIfRequired( + targetType, imports, typeToImport)); + + // Add any type arguments presented for the return type + if (typeToImport.getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : typeToImport.getParameters()) { + typeArgs.add(JavaParserUtils.importParametersForType( + targetType, imports, parameter)); + } + } + + final ReferenceType refType = new ReferenceType(cit); + + // Handle arrays + if (typeToImport.isArray()){ + refType.setArrayCount(typeToImport.getArray()); + } + return refType; + } + + /** + * Attempts to import the presented {@link JavaType}. + *

    + * Whether imported or not, the method returns a {@link NameExpr} suitable + * for subsequent use when referring to that type. + *

    + * If an attempt is made to import a java.lang type, it is ignored. + *

    + * If an attempt is made to import a type without a package, it is ignored. + *

    + * We import every type usage even if the type usage is within the same + * package and would theoretically not require an import. This is undertaken + * so that there is no requirement to separately parse every unqualified + * type usage within the compilation unit so as to refrain from importing + * subsequently conflicting types. + * + * @param targetType the compilation unit target type (required) + * @param imports the compilation unit's imports (required) + * @param typeToImport the type to be imported (required) + * @return the name expression to be used when referring to that type (never + * null) + */ + public static NameExpr importTypeIfRequired(final JavaType targetType, + final List imports, final JavaType typeToImport) { + Validate.notNull(targetType, "Target type is required"); + final JavaPackage compilationUnitPackage = targetType.getPackage(); + Validate.notNull(imports, "Compilation unit imports required"); + Validate.notNull(typeToImport, "Java type to import is required"); + + // If it's a primitive, it's really easy + if (typeToImport.isPrimitive()) { + return new NameExpr(typeToImport.getNameIncludingTypeParameters()); + } + + // Handle if the type doesn't have a package at all + if (typeToImport.isDefaultPackage()) { + return new NameExpr(typeToImport.getSimpleTypeName()); + } + + final JavaPackage typeToImportPackage = typeToImport.getPackage(); + if (typeToImportPackage.equals(compilationUnitPackage)) { + return new NameExpr(typeToImport.getSimpleTypeName()); + } + + NameExpr typeToImportExpr; + if (typeToImport.getEnclosingType() == null) { + typeToImportExpr = new QualifiedNameExpr(new NameExpr(typeToImport + .getPackage().getFullyQualifiedPackageName()), + typeToImport.getSimpleTypeName()); + } + else { + typeToImportExpr = new QualifiedNameExpr(new NameExpr(typeToImport + .getEnclosingType().getFullyQualifiedTypeName()), + typeToImport.getSimpleTypeName()); + } + + final ImportDeclaration newImport = new ImportDeclaration( + typeToImportExpr, false, false); + + boolean addImport = true; + boolean useSimpleTypeName = false; + for (final ImportDeclaration existingImport : imports) { + if (existingImport.getName().getName() + .equals(newImport.getName().getName())) { + // Do not import, as there is already an import with the simple + // type name + addImport = false; + + // If this is a complete match, it indicates we can use the + // simple type name + if (isEqual(existingImport.getName(), newImport.getName())) { + useSimpleTypeName = true; + break; + } + } + } + + if (addImport + && JdkJavaType.isPartOfJavaLang(typeToImport + .getSimpleTypeName())) { + // This simple type name would be part of java.lang if left as the + // simple name. We want a fully-qualified name. + addImport = false; + useSimpleTypeName = false; + } + + if (JdkJavaType.isPartOfJavaLang(typeToImport)) { + // So we would have imported, but we don't need to + addImport = false; + + // The fact we could have imported means there was no other + // conflicting simple type names + useSimpleTypeName = true; + } + + if (addImport + && typeToImport.getPackage().equals(compilationUnitPackage)) { + // It is not theoretically necessary to add an import for something + // in the same package, + // but we elect to explicitly perform an import so future + // conflicting types are not imported + // addImport = true; + // useSimpleTypeName = false; + } + + if (addImport + && targetType.getSimpleTypeName().equals( + typeToImport.getSimpleTypeName())) { + // So we would have imported it, but then it would conflict with the + // simple name of the type + addImport = false; + useSimpleTypeName = false; + } + + if (addImport) { + imports.add(newImport); + useSimpleTypeName = true; + } + + // This is pretty crude, but at least it emits source code for people + // (forget imports, though!) + if (typeToImport.getArgName() != null) { + return new NameExpr(typeToImport.toString()); + } + + if (useSimpleTypeName) { + return new NameExpr(typeToImport.getSimpleTypeName()); + } + return new QualifiedNameExpr(new NameExpr(typeToImport.getPackage() + .getFullyQualifiedPackageName()), + typeToImport.getSimpleTypeName()); + } + + /** + * Indicates whether two {@link NameExpr} expressions are equal. + *

    + * This method is necessary given {@link NameExpr} does not offer an equals + * method. + * + * @param o1 the first entry to compare (null is acceptable) + * @param o2 the second entry to compare (null is acceptable) + * @return true if and only if both entries are identical + */ + private static boolean isEqual(final NameExpr o1, final NameExpr o2) { + if (o1 == null && o2 == null) { + return true; + } + if (o1 == null && o2 != null) { + return false; + } + if (o1 != null && o2 == null) { + return false; + } + if (o1 != null && !o1.getName().equals(o2.getName())) { + return false; + } + return o1 != null && o1.toString().equals(o2.toString()); + } + + /** + * Searches a compilation unit and locates the declaration with the given + * type's simple name. + * + * @param compilationUnit to scan (required) + * @param javaType the target to locate (required) + * @return the located type declaration or null if it could not be found + */ + public static TypeDeclaration locateTypeDeclaration( + final CompilationUnit compilationUnit, final JavaType javaType) { + Validate.notNull(compilationUnit, "Compilation unit required"); + Validate.notNull(javaType, "Java type to search for required"); + if (compilationUnit.getTypes() == null) { + return null; + } + for (final TypeDeclaration candidate : compilationUnit.getTypes()) { + if (javaType.getSimpleTypeName().equals(candidate.getName())) { + // We have the required type declaration + return candidate; + } + } + return null; + } + + /** + * Returns the final {@link ClassOrInterfaceType} from a {@link Type} + * + * @param initType + * @return the final {@link ClassOrInterfaceType} or null if no + * {@link ClassOrInterfaceType} found + */ + public static ClassOrInterfaceType getClassOrInterfaceType(final Type type) { + Type tmp = type; + while (tmp instanceof ReferenceType) { + tmp = ((ReferenceType) tmp).getType(); + } + if (tmp instanceof ClassOrInterfaceType) { + return (ClassOrInterfaceType) tmp; + } + return null; + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/UpdateCompilationUnitUtils.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/UpdateCompilationUnitUtils.java new file mode 100644 index 000000000..0ed689668 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/UpdateCompilationUnitUtils.java @@ -0,0 +1,999 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.ObjectUtils; + +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.ImportDeclaration; +import com.github.antlrjavaparser.api.body.BodyDeclaration; +import com.github.antlrjavaparser.api.body.ConstructorDeclaration; +import com.github.antlrjavaparser.api.body.EnumConstantDeclaration; +import com.github.antlrjavaparser.api.body.EnumDeclaration; +import com.github.antlrjavaparser.api.body.FieldDeclaration; +import com.github.antlrjavaparser.api.body.MethodDeclaration; +import com.github.antlrjavaparser.api.body.Parameter; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.body.VariableDeclarator; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.MarkerAnnotationExpr; +import com.github.antlrjavaparser.api.expr.MemberValuePair; +import com.github.antlrjavaparser.api.expr.NormalAnnotationExpr; +import com.github.antlrjavaparser.api.expr.SingleMemberAnnotationExpr; +import com.github.antlrjavaparser.api.stmt.BlockStmt; +import com.github.antlrjavaparser.api.stmt.Statement; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; +import com.github.antlrjavaparser.api.type.PrimitiveType; +import com.github.antlrjavaparser.api.type.Type; +import com.github.antlrjavaparser.api.type.VoidType; +import com.github.antlrjavaparser.api.type.WildcardType; + +/** + * Utilities to update a Java Parser compilation unit from other + * + * @author DiSiD Technologies + * @since 1.2.2 + */ +public class UpdateCompilationUnitUtils { + + /** + * Structure to store a {@link VariableDeclarator} and its + * {@link FieldDeclaration} together + * + * @author DiSiD Technologies + */ + private static class FieldEntry { + private final FieldDeclaration fieldDeclaration; + private final VariableDeclarator variableDeclarator; + + FieldEntry(final FieldDeclaration fieldDeclaration, + final VariableDeclarator variableDeclarator) { + this.fieldDeclaration = fieldDeclaration; + this.variableDeclarator = variableDeclarator; + } + } + + /** + * Compare two {@link ImportDeclaration} + * + * @param declaration1 + * @param declaration2 + * @return true if are equals + */ + public static boolean equals(final ImportDeclaration declaration1, + final ImportDeclaration declaration2) { + return declaration1.isAsterisk() == declaration2.isAsterisk() + && declaration1.isStatic() == declaration2.isStatic() + && declaration1.getName().getName() + .equals(declaration2.getName().getName()); + } + + /** + * Compare two {@link Type} + * + * @param type + * @param type2 + * @return + */ + private static boolean equals(final Type type, final Type type2) { + if (ObjectUtils.equals(type, type2)) { + return true; + } + if (type.getClass() != type2.getClass()) { + return false; + } + if (type instanceof ClassOrInterfaceType) { + final ClassOrInterfaceType cType = (ClassOrInterfaceType) type; + final ClassOrInterfaceType cType2 = (ClassOrInterfaceType) type2; + return cType.getName().equals(cType2.getName()); + + } + else if (type instanceof PrimitiveType) { + final PrimitiveType pType = (PrimitiveType) type; + final PrimitiveType pType2 = (PrimitiveType) type2; + return pType.getType() == pType2.getType(); + + } + else if (type instanceof VoidType) { + return true; + } + else if (type instanceof WildcardType) { + final WildcardType wType = (WildcardType) type; + final WildcardType wType2 = (WildcardType) type2; + return equals(wType.getSuper(), wType2.getSuper()) + && equals(wType.getExtends(), wType2.getExtends()); + } + return false; + } + + /** + * Update {@code compilationUnit} imports, annotation, fields, methods... + * from {@code cidCompilationUnit} information + * + * @param compilationUnit + * @param cidCompilationUnit + */ + public static void updateCompilationUnitImports( + final CompilationUnit compilationUnit, + final CompilationUnit cidCompilationUnit) { + boolean notFound; + final List cidImports = new ArrayList(); + if (cidCompilationUnit.getImports() != null) { + cidImports.addAll(cidCompilationUnit.getImports()); + } + if (compilationUnit.getImports() != null) { + for (final Iterator originalImportIter = compilationUnit + .getImports().iterator(); originalImportIter.hasNext();) { + final ImportDeclaration originalImport = originalImportIter + .next(); + notFound = true; + for (final Iterator newImportIter = cidImports + .iterator(); newImportIter.hasNext();) { + final ImportDeclaration newImport = newImportIter.next(); + if (equals(originalImport, newImport)) { + // new Import found in original imports + // remove from newImports to check + newImportIter.remove(); + + // Mark as found + notFound = false; + } + } + if (notFound) { + // If not found in newImports remove from compilation unit + originalImportIter.remove(); + } + } + } + + if (cidImports.isEmpty()) { + // Done it + return; + } + + // Add missing new imports + compilationUnit.getImports().addAll(cidImports); + } + + /** + * Updates {@code compilationUnit} types from {@code cidCompilationUnit} + * information + * + * @param compilationUnit + * @param cidCompilationUnit + */ + public static void updateCompilationUnitTypes( + final CompilationUnit compilationUnit, + final CompilationUnit cidCompilationUnit) { + boolean notFound; + final List cidTypes = new ArrayList( + cidCompilationUnit.getTypes()); + + for (final Iterator originalTypestIter = compilationUnit + .getTypes().iterator(); originalTypestIter.hasNext();) { + final TypeDeclaration originalType = originalTypestIter.next(); + notFound = true; + for (final Iterator newTypeIter = cidTypes + .iterator(); newTypeIter.hasNext();) { + final TypeDeclaration newType = newTypeIter.next(); + if (originalType.getName().equals(newType.getName()) + && originalType.getClass() == newType.getClass()) { + // new Type found in original imports + if (originalType instanceof EnumDeclaration) { + updateCompilationUnitEnumeration( + (EnumDeclaration) originalType, + (EnumDeclaration) newType); + } + else { + updateCompilationUnitType(originalType, newType); + } + + // remove from newImports to check + newTypeIter.remove(); + + // Mark as found + notFound = false; + } + } + + if (notFound) { + // If not found in newTypes so remove from compilation unit + originalTypestIter.remove(); + } + } + + if (cidTypes.isEmpty()) { + // Done it + return; + } + + // Add missing new imports + compilationUnit.getTypes().addAll(cidTypes); + } + + /** + * Update {@code originalType} annotation, fields, methods... from + * {@code cidCompilationUnit} information + * + * @param originalType + * @param newType + */ + public static void updateCompilationUnitType( + final TypeDeclaration originalType, final TypeDeclaration newType) { + + if (originalType.getModifiers() != newType.getModifiers()) { + originalType.setModifiers(newType.getModifiers()); + } + + if (originalType.getAnnotations() == null + && newType.getAnnotations() != null) { + originalType.setAnnotations(new ArrayList()); + } + updateAnnotations(originalType.getAnnotations(), + newType.getAnnotations()); + + updateFields(originalType, newType); + + updateConstructors(originalType, newType); + + updateMethods(originalType, newType); + + updateInnerTypes(originalType, newType); + } + + /** + * Update {@code originalType} constructors from {@code cidCompilationUnit} + * information + * + * @param originalType + * @param newType + */ + private static void updateConstructors(final TypeDeclaration originalType, + final TypeDeclaration newType) { + // Get a list of all constructors + final List cidConstructor = new ArrayList(); + if (newType.getMembers() != null) { + for (final BodyDeclaration element : newType.getMembers()) { + if (element instanceof ConstructorDeclaration) { + cidConstructor.add((ConstructorDeclaration) element); + } + } + } + + ConstructorDeclaration originalConstructor, newConstructor; + boolean notFound; + // Iterate over every method definition + if (originalType.getMembers() != null) { + for (final Iterator originalMemberstIter = originalType + .getMembers().iterator(); originalMemberstIter.hasNext();) { + final BodyDeclaration originalMember = originalMemberstIter + .next(); + if (!(originalMember instanceof ConstructorDeclaration)) { + // this is not a method definition + continue; + } + originalConstructor = (ConstructorDeclaration) originalMember; + + notFound = true; + + // look at cidConstructor for originalConstructor + for (final Iterator newConstructorIter = cidConstructor + .iterator(); newConstructorIter.hasNext();) { + newConstructor = newConstructorIter.next(); + // Check if is the same constructor (comparing its + // parameters) + if (equalsDeclaration(originalConstructor, newConstructor)) { + notFound = false; + + // Remove from cid methods to check + newConstructorIter.remove(); + + // Update modifier if is changed + if (originalConstructor.getModifiers() != newConstructor + .getModifiers()) { + originalConstructor.setModifiers(newConstructor + .getModifiers()); + } + + // Update annotations if are changed + if (!equalsAnnotations( + originalConstructor.getAnnotations(), + newConstructor.getAnnotations())) { + originalConstructor.setAnnotations(newConstructor + .getAnnotations()); + } + + // Update body if is changed + if (!equals(originalConstructor.getBlock(), + newConstructor.getBlock())) { + originalConstructor.setBlock(newConstructor + .getBlock()); + } + break; + + } + } + if (notFound) { + originalMemberstIter.remove(); + } + } + } + + if (cidConstructor.isEmpty()) { + // Done it + return; + } + + // add new constructors + if (originalType.getMembers() == null) { + originalType.setMembers(new ArrayList()); + } + originalType.getMembers().addAll(cidConstructor); + } + + /** + * Compares {@code originalConstructor} declaration to + * {@code newConstructor}
    + * This compares constructor parameters and its types + * + * @param originalConstructor + * @param newConstructor + * @return true if declaration are equals + */ + private static boolean equalsDeclaration( + final ConstructorDeclaration originalConstructor, + final ConstructorDeclaration newConstructor) { + if (!equalsParameters(originalConstructor.getParameters(), + newConstructor.getParameters())) { + return false; + } + return true; + } + + /** + * Updates all subclasses of {@code originalType} from {@code newType} + * information + * + * @param originalType + * @param newType + */ + private static void updateInnerTypes(final TypeDeclaration originalType, + final TypeDeclaration newType) { + + // Get a list of all types + final List cidTypes = new ArrayList(); + if (newType.getMembers() != null) { + for (final BodyDeclaration element : newType.getMembers()) { + if (element instanceof TypeDeclaration) { + cidTypes.add((TypeDeclaration) element); + } + } + } + + TypeDeclaration originalInner, newInner; + boolean notFound; + // Iterate over every type definition + if (originalType.getMembers() != null) { + for (final Iterator originalMemberstIter = originalType + .getMembers().iterator(); originalMemberstIter.hasNext();) { + final BodyDeclaration originalMember = originalMemberstIter + .next(); + if (!(originalMember instanceof TypeDeclaration)) { + // this is not a method definition + continue; + } + originalInner = (TypeDeclaration) originalMember; + + notFound = true; + // look at ciMethods for method + for (final Iterator newInnerIter = cidTypes + .iterator(); newInnerIter.hasNext();) { + newInner = newInnerIter.next(); + + if (originalInner.getName().equals(newInner.getName()) + && originalInner.getClass() == newInner.getClass()) { + notFound = false; + + if (originalInner instanceof EnumDeclaration) { + updateCompilationUnitEnumeration( + (EnumDeclaration) originalInner, + (EnumDeclaration) newInner); + } + else { + updateCompilationUnitType(originalInner, newInner); + } + + newInnerIter.remove(); + break; + } + + } + + if (notFound) { + originalMemberstIter.remove(); + } + } + } + + if (cidTypes.isEmpty()) { + // Done it + return; + } + + // Add new methods + if (originalType.getMembers() == null) { + originalType.setMembers(new ArrayList()); + } + originalType.getMembers().addAll(cidTypes); + + } + + /** + * Updates {@code originalType} enumeration from {@code newType} information + * + * @param originalType + * @param newType + */ + private static void updateCompilationUnitEnumeration( + final EnumDeclaration originalType, final EnumDeclaration newType) { + if (originalType.getModifiers() != newType.getModifiers()) { + originalType.setModifiers(newType.getModifiers()); + } + + if (originalType.getAnnotations() == null + && newType.getAnnotations() != null) { + originalType.setAnnotations(new ArrayList()); + } + if (!equalsEnumConstants(originalType.getEntries(), + newType.getEntries())) { + originalType.setEntries(newType.getEntries()); + } + updateAnnotations(originalType.getAnnotations(), + newType.getAnnotations()); + + updateFields(originalType, newType); + + updateConstructors(originalType, newType); + + updateMethods(originalType, newType); + + } + + /** + * Compares two {@link EnumConstantDeclaration} list + * + * @param entries + * @param entries2 + * @return + */ + private static boolean equalsEnumConstants( + final List entries, + final List entries2) { + if (ObjectUtils.equals(entries, entries2)) { + return true; + } + if (entries == null || entries2 == null) { + return false; + } + if (entries.size() != entries2.size()) { + return false; + } + for (int i = 0; i < entries.size(); i++) { + final EnumConstantDeclaration constant = entries.get(i); + final EnumConstantDeclaration constant2 = entries2.get(i); + + if (!equals(constant, constant2)) { + return false; + } + } + return true; + } + + /** + * Compares to {@link EnumConstantDeclaration} + * + * @param constant + * @param constant2 + * @return + */ + private static boolean equals(final EnumConstantDeclaration constant, + final EnumConstantDeclaration constant2) { + if (!constant.getName().equals(constant2.getName())) { + return false; + } + if (!equalsAnnotations(constant.getAnnotations(), + constant2.getAnnotations())) { + return false; + } + if (ObjectUtils.equals(constant.getClassBody(), + constant2.getClassBody())) { + return true; + } + if (constant.getClassBody() == null || constant2.getClassBody() == null) { + return false; + } + final List body = constant.getClassBody(); + final List body2 = constant2.getClassBody(); + if (body.size() != body2.size()) { + return false; + } + for (int i = 0; i < body.size(); i++) { + final BodyDeclaration item = body.get(i); + final BodyDeclaration item2 = body2.get(i); + + if (item.getClass() != item2.getClass()) { + return false; + } + // Compares contents + if (!item.toString().equals(item2.toString())) { + return false; + } + } + return true; + } + + /** + * Updates {@code originalType} methods from {@code newType} information + * + * @param originalType + * @param newType + */ + private static void updateMethods(final TypeDeclaration originalType, + final TypeDeclaration newType) { + // Get a list of all methods + final List cidMethods = new ArrayList(); + if (newType.getMembers() != null) { + for (final BodyDeclaration element : newType.getMembers()) { + if (element instanceof MethodDeclaration) { + cidMethods.add((MethodDeclaration) element); + } + } + } + + MethodDeclaration originalMethod, newMethod; + boolean notFound; + // Iterate over every method definition + if (originalType.getMembers() != null) { + for (final Iterator originalMemberstIter = originalType + .getMembers().iterator(); originalMemberstIter.hasNext();) { + final BodyDeclaration originalMember = originalMemberstIter + .next(); + if (!(originalMember instanceof MethodDeclaration)) { + // this is not a method definition + continue; + } + originalMethod = (MethodDeclaration) originalMember; + + notFound = true; + + // look at ciMethos for method + for (final Iterator newMethodsIter = cidMethods + .iterator(); newMethodsIter.hasNext();) { + newMethod = newMethodsIter.next(); + if (equals(originalMethod, newMethod)) { + notFound = false; + + // Remove from cid methods to check + newMethodsIter.remove(); + break; + } + } + if (notFound) { + originalMemberstIter.remove(); + } + } + } + + if (cidMethods.isEmpty()) { + // Done it + return; + } + + // Add new methods + if (originalType.getMembers() == null) { + originalType.setMembers(new ArrayList()); + } + originalType.getMembers().addAll(cidMethods); + } + + /** + * Compares two {@link MethodDeclaration} + * + * @param originalMethod + * @param newMethod + * @return + */ + private static boolean equals(final MethodDeclaration originalMethod, + final MethodDeclaration newMethod) { + + return originalMethod.getModifiers() == newMethod.getModifiers() + && originalMethod.getName().equals(newMethod.getName()) + && equalsParameters(originalMethod.getParameters(), + newMethod.getParameters()) + && equals(originalMethod.getBody(), newMethod.getBody()); + } + + /** + * Compares two {@link BlockStmt} + * + * @param body + * @param body2 + * @return + */ + private static boolean equals(final BlockStmt body, final BlockStmt body2) { + if (ObjectUtils.equals(body, body2)) { + return true; + } + if (body == null || body2 == null) { + return false; + } + if (body.getStmts().size() != body2.getStmts().size()) { + return false; + } + final List statements = body.getStmts(); + final List statements2 = body2.getStmts(); + for (int i = 0; i < statements.size(); i++) { + if (!equals(statements.get(i), statements2.get(i))) { + return false; + } + } + return true; + } + + /** + * Compares tow {@link Statement} + * + * @param statement + * @param statement2 + * @return + */ + private static boolean equals(final Statement statement, + final Statement statement2) { + if (statement.getClass() != statement2.getClass()) { + return false; + } + // TODO As Roo doesn't make statement changes we can ignore it + return true; + } + + /** + * Compares two {@link Parameter} list + * + * @param parameters + * @param parameters2 + * @return + */ + private static boolean equalsParameters(final List parameters, + final List parameters2) { + if (parameters == parameters2) { + return true; + } + if (parameters == null || parameters2 == null) { + return false; + } + if (parameters.size() != parameters2.size()) { + return false; + } + + Parameter parameter, parameter2; + for (int i = 0; i < parameters.size(); i++) { + parameter = parameters.get(i); + parameter2 = parameters2.get(i); + if (!parameter.getId().getName() + .equals(parameter2.getId().getName())) { + return false; + } + if (!equals(parameter.getType(), parameter2.getType())) { + return false; + } + if (!equalsAnnotations(parameter.getAnnotations(), + parameter2.getAnnotations())) { + return false; + } + } + return true; + } + + /** + * Update {@code originalType} fields from {@code newType} information + * + * @param originalType + * @param newType + */ + private static void updateFields(final TypeDeclaration originalType, + final TypeDeclaration newType) { + + // Get a map of all fields (as FieldDeclaration could contain more than + // one field) + final Map cidFields = new HashMap(); + String fieldName; + FieldDeclaration field; + if (newType.getMembers() != null) { + for (final BodyDeclaration element : newType.getMembers()) { + if (element instanceof FieldDeclaration) { + field = (FieldDeclaration) element; + for (final VariableDeclarator variable : field + .getVariables()) { + fieldName = variable.getId().getName(); + cidFields.put(fieldName, + new FieldEntry(field, variable)); + } + } + } + } + + // Iterate over every field definition + if (originalType.getMembers() != null) { + for (final Iterator originalMemberstIter = originalType + .getMembers().iterator(); originalMemberstIter.hasNext();) { + final BodyDeclaration originalMember = originalMemberstIter + .next(); + if (!(originalMember instanceof FieldDeclaration)) { + // this is not a field definition + continue; + } + field = (FieldDeclaration) originalMember; + + // Check every variable declared in definition + for (final Iterator variablesIter = field + .getVariables().iterator(); variablesIter.hasNext();) { + final VariableDeclarator originalVariable = variablesIter + .next(); + fieldName = originalVariable.getId().getName(); + + // look for field name in cid + final FieldEntry entry = cidFields.get(fieldName); + if (entry == null) { + // Not found: remove field from original compilation + // unit + variablesIter.remove(); + continue; + } + + // Check modifiers, type and annotations + if (equalFieldTypeModifiersAnnotations(field, + entry.fieldDeclaration)) { + // Variable declaration is equals: + // remove from cid map as already exists + cidFields.remove(fieldName); + } + else { + // as there are more variable definition remove it + // from original. At the end, process will create it + // again + // using new modifiers and type + variablesIter.remove(); + // Modifiers changed + if (field.getVariables().size() == 1) { + // if no more variables update all field definition + field.setModifiers(entry.fieldDeclaration + .getModifiers()); + field.setType(entry.fieldDeclaration.getType()); + if (field.getAnnotations() == null + && entry.fieldDeclaration.getAnnotations() != null) { + field.setAnnotations(new ArrayList()); + } + updateAnnotations(field.getAnnotations(), + entry.fieldDeclaration.getAnnotations()); + + // remove processed field of cid + cidFields.remove(fieldName); + continue; + } + } + + } + if (field.getVariables().isEmpty()) { + originalMemberstIter.remove(); + } + } + } + + if (cidFields.isEmpty()) { + // Done it + return; + } + + if (originalType.getMembers() == null) { + originalType.setMembers(new ArrayList()); + } + + // Add new fields + List variables; + for (final FieldEntry entry : cidFields.values()) { + variables = new ArrayList(1); + variables.add(entry.variableDeclarator); + field = new FieldDeclaration(entry.fieldDeclaration.getModifiers(), + entry.fieldDeclaration.getType(), variables); + field.setAnnotations(entry.fieldDeclaration.getAnnotations()); + field.setBeginComments(entry.fieldDeclaration.getBeginComments()); + field.setInternalComments(entry.fieldDeclaration + .getInternalComments()); + field.setEndComments(entry.fieldDeclaration.getEndComments()); + originalType.getMembers().add(field); + } + } + + /** + * Compares Type, modifies and annotation of two {@link FieldDeclaration} + * + * @param fieldDeclaration + * @param fieldDeclaration2 + * @return + */ + public static boolean equalFieldTypeModifiersAnnotations( + final FieldDeclaration fieldDeclaration, + final FieldDeclaration fieldDeclaration2) { + return fieldDeclaration.getModifiers() == fieldDeclaration2 + .getModifiers() + && equals(fieldDeclaration.getType(), + fieldDeclaration2.getType()) + && equalsAnnotations(fieldDeclaration.getAnnotations(), + fieldDeclaration2.getAnnotations()); + } + + /** + * Compares two {@link AnnotationExpr} list + * + * @param annotations + * @param annotations2 + * @return + */ + private static boolean equalsAnnotations( + final List annotations, + final List annotations2) { + if (annotations == annotations2) { + return true; + } + else if (annotations == null || annotations2 == null) { + return false; + } + if (annotations.size() != annotations2.size()) { + return false; + } + boolean found; + for (final AnnotationExpr annotation1 : annotations) { + found = false; + for (final AnnotationExpr annotation2 : annotations2) { + if (equals(annotation1, annotation2)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + /** + * Compares two annotation expression + * + * @param annotation1 + * @param annotation2 + * @return + */ + public static boolean equals(final AnnotationExpr annotation1, + final AnnotationExpr annotation2) { + + if (annotation1 == annotation2) { + return true; + } + if (annotation1 == null || annotation2 == null) { + return false; + } + if (!annotation1.getName().getName() + .equals(annotation2.getName().getName())) { + return false; + } + if (!annotation1.getName().equals(annotation2.getName())){ + return false; + } + if (!annotation1.getClass().equals(annotation2.getClass())){ + return false; + } + if (annotation1 instanceof SingleMemberAnnotationExpr){ + // Compare expression + String expression1 = ((SingleMemberAnnotationExpr)annotation1).getMemberValue().toString(); + String expression2 = ((SingleMemberAnnotationExpr)annotation2).getMemberValue().toString(); + return expression1.equals(expression2); + } else if (annotation1 instanceof NormalAnnotationExpr) { + // Compare pairs + List pairs1 = ((NormalAnnotationExpr)annotation1).getPairs(); + List pairs2 = ((NormalAnnotationExpr)annotation2).getPairs(); + + return equals(pairs1,pairs2); + + } else if (annotation1 instanceof MarkerAnnotationExpr) { + // just compare name (and already done it) + return true; + } else { + // No other way to check are equals but toString output + return annotation1.toString().equals(annotation2.toString()); + } + } + + /** + * Compares to {@link MemberValuePair} list + * + * @param pairs1 + * @param pairs2 + * @return + */ + private static boolean equals(List pairs1, + List pairs2) { + if (ObjectUtils.equals(pairs1, pairs2)){ + return true; + } + if (pairs1 == null || pairs2 == null){ + return false; + } + if (pairs1.size() != pairs2.size()) { + return false; + } + + // Clone pair2 to better performance + List pairs2Cloned = new ArrayList(pairs2); + + MemberValuePair pair2; + Iterator pairIterator; + boolean found; + // For every pair in 1 + for (MemberValuePair pair1 : pairs1) { + found = false; + pairIterator = pairs2Cloned.iterator(); + // Iterate over remaining pair2 elements + while (pairIterator.hasNext()){ + pair2 = pairIterator.next(); + if (pair1.getName().equals(pair2.getName())){ + // Found name + found = true; + // Remove from remaining pair2 elements + pairIterator.remove(); + // compare value + if (ObjectUtils.equals(pair1.getValue(), pair2.getValue())){ + // Equals: check for pair1 finished + break; + } else { + String value1 = ObjectUtils.defaultIfNull(pair1.getValue(), "").toString(); + String value2 = ObjectUtils.defaultIfNull(pair2.getValue(), "").toString(); + if (value1.equals(value2)){ + // Equals: check for pair1 finished + break; + } else { + // Not equals: return false + return false; + } + } + } + } + if (!found) { + // Pair1 not found: return false + return false; + } + } + return true; + } + + /** + * Update {@code annotations} with {@code } + * + * @param annotations + * @param annotations2 + */ + public static void updateAnnotations( + final List annotations, + final List annotations2) { + if (!equalsAnnotations(annotations, annotations2)) { + // XXX japa version (1.0.7) has no way to manage + // AnnotationExpr + annotations.clear(); + annotations.addAll(annotations2); + } + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserAnnotationMetadataBuilder.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserAnnotationMetadataBuilder.java new file mode 100644 index 000000000..c9e0f0757 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserAnnotationMetadataBuilder.java @@ -0,0 +1,636 @@ +package org.springframework.roo.classpath.antlrjavaparser.details; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.antlrjavaparser.CompilationUnitServices; +import org.springframework.roo.classpath.antlrjavaparser.JavaParserUtils; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.CharAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.DoubleAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.classpath.details.annotations.LongAttributeValue; +import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.ArrayInitializerExpr; +import com.github.antlrjavaparser.api.expr.BinaryExpr; +import com.github.antlrjavaparser.api.expr.BooleanLiteralExpr; +import com.github.antlrjavaparser.api.expr.CharLiteralExpr; +import com.github.antlrjavaparser.api.expr.ClassExpr; +import com.github.antlrjavaparser.api.expr.DoubleLiteralExpr; +import com.github.antlrjavaparser.api.expr.Expression; +import com.github.antlrjavaparser.api.expr.FieldAccessExpr; +import com.github.antlrjavaparser.api.expr.IntegerLiteralExpr; +import com.github.antlrjavaparser.api.expr.LongLiteralExpr; +import com.github.antlrjavaparser.api.expr.MarkerAnnotationExpr; +import com.github.antlrjavaparser.api.expr.MemberValuePair; +import com.github.antlrjavaparser.api.expr.NameExpr; +import com.github.antlrjavaparser.api.expr.NormalAnnotationExpr; +import com.github.antlrjavaparser.api.expr.SingleMemberAnnotationExpr; +import com.github.antlrjavaparser.api.expr.StringLiteralExpr; +import com.github.antlrjavaparser.api.expr.UnaryExpr; +import com.github.antlrjavaparser.api.expr.UnaryExpr.Operator; +import com.github.antlrjavaparser.api.type.Type; + +/** + * Java Parser implementation of {@link AnnotationMetadata}. + * + * @author Ben Alex + * @author Andrew Swan + * @since 1.0 + */ +public class JavaParserAnnotationMetadataBuilder implements + Builder { + + /** + * Facilitates the addition of the annotation to the presented type. + * + * @param compilationUnitServices to use (required) + * @param annotations to add to the end of (required) + * @param annotation to add (required) + */ + public static void addAnnotationToList( + final CompilationUnitServices compilationUnitServices, + final List annotations, + final AnnotationMetadata annotation) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(annotations, "Annotations required"); + Validate.notNull(annotation, "Annotation metadata required"); + + // Create a holder for the annotation we're going to create + boolean foundExisting = false; + + // Search for an existing annotation of this type + for (final AnnotationExpr candidate : annotations) { + NameExpr existingName = null; + if (candidate instanceof NormalAnnotationExpr) { + existingName = ((NormalAnnotationExpr) candidate).getName(); + } + else if (candidate instanceof MarkerAnnotationExpr) { + existingName = ((MarkerAnnotationExpr) candidate).getName(); + } + else if (candidate instanceof SingleMemberAnnotationExpr) { + existingName = ((SingleMemberAnnotationExpr) candidate) + .getName(); + } + + // Convert the candidate annotation type's into a JavaType + final JavaType javaType = JavaParserUtils.getJavaType( + compilationUnitServices, existingName, null); + if (annotation.getAnnotationType().equals(javaType)) { + foundExisting = true; + break; + } + } + Validate.isTrue(!foundExisting, + "Found an existing annotation for type '%s'", + annotation.getAnnotationType()); + + // Import the annotation type, if needed + final NameExpr nameToUse = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + annotation.getAnnotationType()); + + // Create member-value pairs in accordance with Java Parser requirements + final List memberValuePairs = new ArrayList(); + for (final JavaSymbolName attributeName : annotation + .getAttributeNames()) { + final AnnotationAttributeValue value = annotation + .getAttribute(attributeName); + Validate.notNull(value, + "Unable to acquire value '%s' from annotation", + attributeName); + final MemberValuePair memberValuePair = convert(value); + // Validate.notNull(memberValuePair, + // "Member value pair should have been set"); + if (memberValuePair != null) { + memberValuePairs.add(memberValuePair); + } + } + + // Create the AnnotationExpr; it varies depending on how many + // member-value pairs we need to present + AnnotationExpr annotationExpression = null; + if (memberValuePairs.isEmpty()) { + annotationExpression = new MarkerAnnotationExpr(nameToUse); + } + else if (memberValuePairs.size() == 1 + && (memberValuePairs.get(0).getName() == null || "value" + .equals(memberValuePairs.get(0).getName()))) { + final Expression toUse = JavaParserUtils + .importExpressionIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + memberValuePairs.get(0).getValue()); + annotationExpression = new SingleMemberAnnotationExpr(nameToUse, + toUse); + } + else { + // We have a number of pairs being presented + annotationExpression = new NormalAnnotationExpr(nameToUse, + new ArrayList()); + } + + // Add our AnnotationExpr to the actual annotations that will eventually + // be flushed through to the compilation unit + JavaParserCommentMetadataBuilder.updateCommentsToJavaParser( + annotationExpression, annotation.getCommentStructure()); + annotations.add(annotationExpression); + + // Add member-value pairs to our AnnotationExpr + if (!memberValuePairs.isEmpty()) { + // Have to check here for cases where we need to change an existing + // MarkerAnnotationExpr to a NormalAnnotationExpr or + // SingleMemberAnnotationExpr + if (annotationExpression instanceof MarkerAnnotationExpr) { + final MarkerAnnotationExpr mae = (MarkerAnnotationExpr) annotationExpression; + + annotations.remove(mae); + + if (memberValuePairs.size() == 1 + && (memberValuePairs.get(0).getName() == null || "value" + .equals(memberValuePairs.get(0).getName()))) { + final Expression toUse = JavaParserUtils + .importExpressionIfRequired(compilationUnitServices + .getEnclosingTypeName(), + compilationUnitServices.getImports(), + memberValuePairs.get(0).getValue()); + annotationExpression = new SingleMemberAnnotationExpr( + nameToUse, toUse); + JavaParserCommentMetadataBuilder + .updateCommentsToJavaParser(annotationExpression, + annotation.getCommentStructure()); + annotations.add(annotationExpression); + } + else { + // We have a number of pairs being presented + annotationExpression = new NormalAnnotationExpr(nameToUse, + new ArrayList()); + JavaParserCommentMetadataBuilder + .updateCommentsToJavaParser(annotationExpression, + annotation.getCommentStructure()); + annotations.add(annotationExpression); + } + } + if (annotationExpression instanceof SingleMemberAnnotationExpr) { + // Potentially upgrade this expression to a NormalAnnotationExpr + final SingleMemberAnnotationExpr smae = (SingleMemberAnnotationExpr) annotationExpression; + if (memberValuePairs.size() == 1 + && memberValuePairs.get(0).getName() == null + || memberValuePairs.get(0).getName().equals("value") + || memberValuePairs.get(0).getName().equals("")) { + // They specified only a single member-value pair, and it is + // the default anyway, so we need not do anything except + // update the value + final Expression toUse = JavaParserUtils + .importExpressionIfRequired(compilationUnitServices + .getEnclosingTypeName(), + compilationUnitServices.getImports(), + memberValuePairs.get(0).getValue()); + smae.setMemberValue(toUse); + return; + } + + // There is > 1 expression, or they have provided some sort of + // non-default value, so it's time to upgrade the expression + // (whilst retaining any potentially existing expression values) + final Expression existingValue = smae.getMemberValue(); + annotationExpression = new NormalAnnotationExpr(smae.getName(), + new ArrayList()); + ((NormalAnnotationExpr) annotationExpression).getPairs().add( + new MemberValuePair("value", existingValue)); + } + Validate.isInstanceOf( + NormalAnnotationExpr.class, + annotationExpression, + "Attempting to add >1 annotation member-value pair requires an existing normal annotation expression"); + final List annotationPairs = ((NormalAnnotationExpr) annotationExpression) + .getPairs(); + annotationPairs.clear(); + for (final MemberValuePair pair : memberValuePairs) { + final Expression toUse = JavaParserUtils + .importExpressionIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + pair.getValue()); + pair.setValue(toUse); + annotationPairs.add(pair); + } + } + } + + @SuppressWarnings("unchecked") + private static MemberValuePair convert( + final AnnotationAttributeValue value) { + if (value instanceof NestedAnnotationAttributeValue) { + final NestedAnnotationAttributeValue castValue = (NestedAnnotationAttributeValue) value; + AnnotationExpr annotationExpr; + final AnnotationMetadata nestedAnnotation = castValue.getValue(); + if (castValue.getValue().getAttributeNames().size() == 0) { + annotationExpr = new MarkerAnnotationExpr( + JavaParserUtils.getNameExpr(nestedAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName())); + } + else if (castValue.getValue().getAttributeNames().size() == 1) { + annotationExpr = new SingleMemberAnnotationExpr( + JavaParserUtils.getNameExpr(nestedAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName()), convert( + nestedAnnotation.getAttribute(nestedAnnotation + .getAttributeNames().get(0))) + .getValue()); + } + else { + final List memberValuePairs = new ArrayList(); + for (final JavaSymbolName attributeName : nestedAnnotation + .getAttributeNames()) { + memberValuePairs.add(convert(nestedAnnotation + .getAttribute(attributeName))); + } + annotationExpr = new NormalAnnotationExpr( + JavaParserUtils.getNameExpr(nestedAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName()), memberValuePairs); + } + // Rely on the nested instance to know its member value pairs + return new MemberValuePair(value.getName().getSymbolName(), + annotationExpr); + } + + if (value instanceof BooleanAttributeValue) { + final boolean castValue = ((BooleanAttributeValue) value) + .getValue(); + final BooleanLiteralExpr convertedValue = new BooleanLiteralExpr( + castValue); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof CharAttributeValue) { + final char castValue = ((CharAttributeValue) value).getValue(); + final CharLiteralExpr convertedValue = new CharLiteralExpr( + new String(new char[] { castValue })); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof LongAttributeValue) { + final Long castValue = ((LongAttributeValue) value).getValue(); + final LongLiteralExpr convertedValue = new LongLiteralExpr( + castValue.toString() + "L"); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof IntegerAttributeValue) { + final Integer castValue = ((IntegerAttributeValue) value) + .getValue(); + final IntegerLiteralExpr convertedValue = new IntegerLiteralExpr( + castValue.toString()); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof DoubleAttributeValue) { + final DoubleAttributeValue doubleAttributeValue = (DoubleAttributeValue) value; + final Double castValue = doubleAttributeValue.getValue(); + DoubleLiteralExpr convertedValue; + if (doubleAttributeValue.isFloatingPrecisionOnly()) { + convertedValue = new DoubleLiteralExpr(castValue.toString() + + "F"); + } + else { + convertedValue = new DoubleLiteralExpr(castValue.toString() + + "D"); + } + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof StringAttributeValue) { + final String castValue = ((StringAttributeValue) value).getValue(); + final StringLiteralExpr convertedValue = new StringLiteralExpr( + castValue.toString()); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof EnumAttributeValue) { + final EnumDetails castValue = ((EnumAttributeValue) value) + .getValue(); + // This isn't as elegant as it could be (ie loss of type + // parameters), but it will do for now + final FieldAccessExpr convertedValue = new FieldAccessExpr( + JavaParserUtils.getNameExpr(castValue.getType() + .getFullyQualifiedTypeName()), castValue.getField() + .getSymbolName()); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof ClassAttributeValue) { + final JavaType castValue = ((ClassAttributeValue) value).getValue(); + // This doesn't preserve type parameters + final NameExpr nameExpr = JavaParserUtils.getNameExpr(castValue + .getFullyQualifiedTypeName()); + final ClassExpr convertedValue = new ClassExpr( + JavaParserUtils.getReferenceType(nameExpr)); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof ArrayAttributeValue) { + final ArrayAttributeValue> castValue = (ArrayAttributeValue>) value; + + final List arrayElements = new ArrayList(); + for (final AnnotationAttributeValue v : castValue.getValue()) { + final MemberValuePair converted = convert(v); + if (converted != null) { + arrayElements.add(converted.getValue()); + } + } + return new MemberValuePair(value.getName().getSymbolName(), + new ArrayInitializerExpr(arrayElements)); + } + + throw new UnsupportedOperationException("Unsupported attribute value '" + + value.getName() + "' of type '" + value.getClass().getName() + + "'"); + } + + public static JavaParserAnnotationMetadataBuilder getInstance( + final AnnotationExpr annotationExpr, + final CompilationUnitServices compilationUnitServices) { + return new JavaParserAnnotationMetadataBuilder(annotationExpr, + compilationUnitServices); + } + + private final JavaType annotationType; + + private final List> attributeValues; + + private final CommentStructure commentStructure; + + /** + * Factory method + * + * @param annotationExpr + * @param compilationUnitServices + * @return a non-null instance + * @since 1.2.0 + */ + private JavaParserAnnotationMetadataBuilder( + final AnnotationExpr annotationExpr, + final CompilationUnitServices compilationUnitServices) { + Validate.notNull(annotationExpr, "Annotation expression required"); + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + + // Obtain the annotation type name from the assorted types of + // annotations we might have received (ie marker annotations, single + // member annotations, normal annotations etc) + final NameExpr nameToFind = JavaParserUtils.getNameExpr(annotationExpr); + + // Compute the actual annotation type, having regard to the compilation + // unit package and imports + annotationType = JavaParserUtils.getJavaType(compilationUnitServices, + nameToFind, null); + + // Generate some member-value pairs for subsequent parsing + List annotationPairs = new ArrayList(); + if (annotationExpr instanceof MarkerAnnotationExpr) { + // A marker annotation has no values, so we can have no pairs to add + } + else if (annotationExpr instanceof SingleMemberAnnotationExpr) { + final SingleMemberAnnotationExpr a = (SingleMemberAnnotationExpr) annotationExpr; + // Add the "value=" member-value pair. + if (a.getMemberValue() != null) { + annotationPairs.add(new MemberValuePair("value", a + .getMemberValue())); + } + } + else if (annotationExpr instanceof NormalAnnotationExpr) { + final NormalAnnotationExpr a = (NormalAnnotationExpr) annotationExpr; + // Must iterate over the expressions + if (a.getPairs() != null) { + annotationPairs = a.getPairs(); + } + } + + // Iterate over the annotation attributes, creating our parsed + // attributes map + final List> attributeValues = new ArrayList>(); + for (final MemberValuePair p : annotationPairs) { + final JavaSymbolName annotationName = new JavaSymbolName( + p.getName()); + final AnnotationAttributeValue value = convert(annotationName, + p.getValue(), compilationUnitServices); + attributeValues.add(value); + } + this.attributeValues = attributeValues; + + commentStructure = new CommentStructure(); + JavaParserCommentMetadataBuilder.updateCommentsToRoo(commentStructure, + annotationExpr); + } + + @Override + public AnnotationMetadata build() { + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder( + annotationType, attributeValues); + + final AnnotationMetadata md = annotationMetadataBuilder.build(); + md.setCommentStructure(commentStructure); + + return md; + } + + private AnnotationAttributeValue convert(JavaSymbolName annotationName, + final Expression expression, + final CompilationUnitServices compilationUnitServices) { + if (annotationName == null) { + annotationName = new JavaSymbolName("__ARRAY_ELEMENT__"); + } + + if (expression instanceof AnnotationExpr) { + final AnnotationExpr annotationExpr = (AnnotationExpr) expression; + final AnnotationMetadata value = getInstance(annotationExpr, + compilationUnitServices).build(); + return new NestedAnnotationAttributeValue(annotationName, value); + } + + if (expression instanceof BooleanLiteralExpr) { + final boolean value = ((BooleanLiteralExpr) expression).getValue(); + return new BooleanAttributeValue(annotationName, value); + } + + if (expression instanceof CharLiteralExpr) { + final String value = ((CharLiteralExpr) expression).getValue(); + Validate.isTrue( + value.length() == 1, + "Expected a char expression, but instead received '%s' for attribute '%s'", + value, annotationName); + final char c = value.charAt(0); + return new CharAttributeValue(annotationName, c); + } + + if (expression instanceof LongLiteralExpr) { + String value = ((LongLiteralExpr) expression).getValue(); + Validate.isTrue( + value.toUpperCase().endsWith("L"), + "Expected long literal expression '%s' to end in 'l' or 'L'", + value); + value = value.substring(0, value.length() - 1); + final long l = new Long(value); + return new LongAttributeValue(annotationName, l); + } + + if (expression instanceof IntegerLiteralExpr) { + final String value = ((IntegerLiteralExpr) expression).getValue(); + final int i = new Integer(value); + return new IntegerAttributeValue(annotationName, i); + } + + if (expression instanceof DoubleLiteralExpr) { + String value = ((DoubleLiteralExpr) expression).getValue(); + boolean floatingPrecisionOnly = false; + if (value.toUpperCase().endsWith("F")) { + value = value.substring(0, value.length() - 1); + floatingPrecisionOnly = true; + } + if (value.toUpperCase().endsWith("D")) { + value = value.substring(0, value.length() - 1); + } + final double d = new Double(value); + return new DoubleAttributeValue(annotationName, d, + floatingPrecisionOnly); + } + + if (expression instanceof BinaryExpr) { + String result = ""; + BinaryExpr current = (BinaryExpr) expression; + while (current != null) { + String right = ""; + if (current.getRight() instanceof StringLiteralExpr) { + right = ((StringLiteralExpr) current.getRight()).getValue(); + } + else if (current.getRight() instanceof NameExpr) { + right = ((NameExpr) current.getRight()).getName(); + } + + result = right + result; + if (current.getLeft() instanceof StringLiteralExpr) { + final String left = ((StringLiteralExpr) current.getLeft()) + .getValue(); + result = left + result; + } + if (current.getLeft() instanceof BinaryExpr) { + current = (BinaryExpr) current.getLeft(); + } + else { + current = null; + } + } + return new StringAttributeValue(annotationName, result); + } + + if (expression instanceof StringLiteralExpr) { + final String value = ((StringLiteralExpr) expression).getValue(); + return new StringAttributeValue(annotationName, value); + } + + if (expression instanceof FieldAccessExpr) { + final FieldAccessExpr field = (FieldAccessExpr) expression; + final String fieldName = field.getField(); + + // Determine the type + final Expression scope = field.getScope(); + NameExpr nameToFind = null; + if (scope instanceof FieldAccessExpr) { + final FieldAccessExpr fScope = (FieldAccessExpr) scope; + nameToFind = JavaParserUtils.getNameExpr(fScope.toString()); + } + else if (scope instanceof NameExpr) { + nameToFind = (NameExpr) scope; + } + else { + throw new UnsupportedOperationException( + "A FieldAccessExpr for '" + + field.getScope() + + "' should return a NameExpr or FieldAccessExpr (was " + + field.getScope().getClass().getName() + ")"); + } + final JavaType fieldType = JavaParserUtils.getJavaType( + compilationUnitServices, nameToFind, null); + + final EnumDetails enumDetails = new EnumDetails(fieldType, + new JavaSymbolName(fieldName)); + return new EnumAttributeValue(annotationName, enumDetails); + } + + if (expression instanceof NameExpr) { + final NameExpr field = (NameExpr) expression; + final String name = field.getName(); + // As we have no way of finding out the real type + final JavaType fieldType = new JavaType("unknown.Object"); + final EnumDetails enumDetails = new EnumDetails(fieldType, + new JavaSymbolName(name)); + return new EnumAttributeValue(annotationName, enumDetails); + } + + if (expression instanceof ClassExpr) { + final ClassExpr clazz = (ClassExpr) expression; + final Type nameToFind = clazz.getType(); + final JavaType javaType = JavaParserUtils.getJavaType( + compilationUnitServices, nameToFind, null); + return new ClassAttributeValue(annotationName, javaType); + } + + if (expression instanceof ArrayInitializerExpr) { + final ArrayInitializerExpr castExp = (ArrayInitializerExpr) expression; + final List> arrayElements = new ArrayList>(); + for (final Expression e : castExp.getValues()) { + arrayElements.add(convert(null, e, compilationUnitServices)); + } + return new ArrayAttributeValue>( + annotationName, arrayElements); + } + + if (expression instanceof UnaryExpr) { + final UnaryExpr castExp = (UnaryExpr) expression; + if (castExp.getOperator() == Operator.negative) { + String value = castExp.toString(); + value = value.toUpperCase().endsWith("L") ? value.substring(0, + value.length() - 1) : value; + final long l = new Long(value); + return new LongAttributeValue(annotationName, l); + } + else { + throw new UnsupportedOperationException( + "Only negative operator in UnaryExpr is supported"); + } + } + + throw new UnsupportedOperationException( + "Unable to parse annotation attribute '" + annotationName + + "' due to unsupported annotation expression '" + + expression.getClass().getName() + "'"); + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserClassOrInterfaceTypeDetailsBuilder.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserClassOrInterfaceTypeDetailsBuilder.java new file mode 100644 index 000000000..bd9c50f18 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserClassOrInterfaceTypeDetailsBuilder.java @@ -0,0 +1,417 @@ +package org.springframework.roo.classpath.antlrjavaparser.details; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.antlrjavaparser.CompilationUnitServices; +import org.springframework.roo.classpath.antlrjavaparser.JavaParserUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ImportMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.ImportDeclaration; +import com.github.antlrjavaparser.api.body.BodyDeclaration; +import com.github.antlrjavaparser.api.body.ClassOrInterfaceDeclaration; +import com.github.antlrjavaparser.api.body.ConstructorDeclaration; +import com.github.antlrjavaparser.api.body.EnumConstantDeclaration; +import com.github.antlrjavaparser.api.body.EnumDeclaration; +import com.github.antlrjavaparser.api.body.FieldDeclaration; +import com.github.antlrjavaparser.api.body.MethodDeclaration; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.body.VariableDeclarator; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.QualifiedNameExpr; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; + +public class JavaParserClassOrInterfaceTypeDetailsBuilder implements + Builder { + + private static final Logger LOGGER = Logger.getLogger(JavaParserClassOrInterfaceTypeDetailsBuilder.class.getName()); + + static final String UNSUPPORTED_MESSAGE_PREFIX = "Only enum, class and interface files are supported"; + + /** + * Factory method for this builder class + * + * @param compilationUnit + * @param enclosingCompilationUnitServices + * @param typeDeclaration + * @param declaredByMetadataId + * @param typeName + * @param metadataService + * @param typeLocationService + * @return a non-null builder + */ + public static JavaParserClassOrInterfaceTypeDetailsBuilder getInstance( + final CompilationUnit compilationUnit, + final CompilationUnitServices enclosingCompilationUnitServices, + final TypeDeclaration typeDeclaration, + final String declaredByMetadataId, final JavaType typeName, + final MetadataService metadataService, + final TypeLocationService typeLocationService) { + return new JavaParserClassOrInterfaceTypeDetailsBuilder( + compilationUnit, enclosingCompilationUnitServices, + typeDeclaration, declaredByMetadataId, typeName, + metadataService, typeLocationService); + } + + private final CompilationUnit compilationUnit; + private JavaPackage compilationUnitPackage; + private final CompilationUnitServices compilationUnitServices; + private final String declaredByMetadataId; + private List imports = new ArrayList(); + private final List innerTypes = new ArrayList(); + private final MetadataService metadataService; + + private JavaType name; + private PhysicalTypeCategory physicalTypeCategory; + private final TypeDeclaration typeDeclaration; + private final TypeLocationService typeLocationService; + + /** + * Constructor + * + * @param compilationUnit + * @param enclosingCompilationUnitServices + * @param typeDeclaration + * @param declaredByMetadataId + * @param typeName + * @param metadataService + * @param typeLocationService + */ + private JavaParserClassOrInterfaceTypeDetailsBuilder( + final CompilationUnit compilationUnit, + final CompilationUnitServices enclosingCompilationUnitServices, + final TypeDeclaration typeDeclaration, + final String declaredByMetadataId, final JavaType typeName, + final MetadataService metadataService, + final TypeLocationService typeLocationService) { + // Check + Validate.notNull(compilationUnit, "Compilation unit required"); + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(typeDeclaration, + "Unable to locate the class or interface declaration"); + Validate.notNull(typeName, "Name required"); + + // Assign + this.compilationUnit = compilationUnit; + compilationUnitServices = enclosingCompilationUnitServices == null ? getDefaultCompilationUnitServices() + : enclosingCompilationUnitServices; + this.declaredByMetadataId = declaredByMetadataId; + this.metadataService = metadataService; + name = typeName; + this.typeDeclaration = typeDeclaration; + this.typeLocationService = typeLocationService; + } + + @Override + public ClassOrInterfaceTypeDetails build() { + Validate.notEmpty(compilationUnit.getTypes(), + "No types in compilation unit, so unable to continue parsing"); + + ClassOrInterfaceDeclaration clazz = null; + EnumDeclaration enumClazz = null; + + final StringBuilder sb = new StringBuilder(compilationUnit.getPackage() + .getName().toString()); + if (name.getEnclosingType() != null) { + sb.append(".").append(name.getEnclosingType().getSimpleTypeName()); + } + compilationUnitPackage = new JavaPackage(sb.toString()); + + // Determine the type name, adding type parameters if possible + final JavaType newName = JavaParserUtils.getJavaType( + compilationUnitServices, typeDeclaration); + + // Revert back to the original type name (thus avoiding unnecessary + // inferences about java.lang types; see ROO-244) + name = new JavaType(newName.getFullyQualifiedTypeName(), + newName.getEnclosingType(), newName.getArray(), + newName.getDataType(), newName.getArgName(), + newName.getParameters()); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId); + + physicalTypeCategory = PhysicalTypeCategory.CLASS; + if (typeDeclaration instanceof ClassOrInterfaceDeclaration) { + clazz = (ClassOrInterfaceDeclaration) typeDeclaration; + if (clazz.isInterface()) { + physicalTypeCategory = PhysicalTypeCategory.INTERFACE; + } + + } + else if (typeDeclaration instanceof EnumDeclaration) { + enumClazz = (EnumDeclaration) typeDeclaration; + physicalTypeCategory = PhysicalTypeCategory.ENUMERATION; + } + + Validate.notNull(physicalTypeCategory, "%s (%s for %s)", + UNSUPPORTED_MESSAGE_PREFIX, typeDeclaration.getClass() + .getSimpleName(), name); + + cidBuilder.setName(name); + cidBuilder.setPhysicalTypeCategory(physicalTypeCategory); + + imports = compilationUnit.getImports(); + if (imports == null) { + imports = new ArrayList(); + compilationUnit.setImports(imports); + } + + // Verify the package declaration appears to be correct + if(compilationUnitPackage.equals(name.getPackage()) != true) { + String warningStr = "[Warning] Compilation unit package '" + compilationUnitPackage + "' unexpected for type '" + name.getPackage() + "', it may be a nested class."; + LOGGER.log(Level.WARNING, warningStr); + } + + for (final ImportDeclaration importDeclaration : imports) { + if (importDeclaration.getName() instanceof QualifiedNameExpr) { + final String qualifier = ((QualifiedNameExpr) importDeclaration + .getName()).getQualifier().toString(); + final String simpleName = importDeclaration.getName().getName(); + final String fullName = qualifier + "." + simpleName; + // We want to calculate these... + + final JavaType type = new JavaType(fullName); + final JavaPackage typePackage = importDeclaration.isAsterisk() ? new JavaPackage( + fullName) : type.getPackage(); + + // Process any comments for the import + final CommentStructure commentStructure = new CommentStructure(); + JavaParserCommentMetadataBuilder.updateCommentsToRoo( + commentStructure, importDeclaration); + + final ImportMetadataBuilder newImport = new ImportMetadataBuilder( + declaredByMetadataId, 0, typePackage, type, + importDeclaration.isStatic(), + importDeclaration.isAsterisk()); + + newImport.setCommentStructure(commentStructure); + + cidBuilder.add(newImport.build()); + } + } + + // Convert Java Parser modifier into JDK modifier + cidBuilder.setModifier(JavaParserUtils.getJdkModifier(typeDeclaration + .getModifiers())); + + // Type parameters + final Set typeParameterNames = new HashSet(); + for (final JavaType param : name.getParameters()) { + final JavaSymbolName arg = param.getArgName(); + // Fortunately type names can only appear at the top-level + if (arg != null && !JavaType.WILDCARD_NEITHER.equals(arg) + && !JavaType.WILDCARD_EXTENDS.equals(arg) + && !JavaType.WILDCARD_SUPER.equals(arg)) { + typeParameterNames.add(arg); + } + } + + List implementsList; + List annotationsList = null; + List members = null; + + if (clazz != null) { + final List extendsList = clazz.getExtends(); + if (extendsList != null) { + for (final ClassOrInterfaceType candidate : extendsList) { + final JavaType javaType = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, candidate, + typeParameterNames); + cidBuilder.addExtendsTypes(javaType); + } + } + + final List extendsTypes = cidBuilder.getExtendsTypes(); + // Obtain the superclass, if this is a class and one is available + if (physicalTypeCategory == PhysicalTypeCategory.CLASS + && extendsTypes.size() == 1) { + final JavaType superclass = extendsTypes.get(0); + final String superclassId = typeLocationService + .getPhysicalTypeIdentifier(superclass); + PhysicalTypeMetadata superPtm = null; + if (superclassId != null) { + superPtm = (PhysicalTypeMetadata) metadataService + .get(superclassId); + } + if (superPtm != null + && superPtm.getMemberHoldingTypeDetails() != null) { + cidBuilder.setSuperclass(superPtm + .getMemberHoldingTypeDetails()); + } + } + + implementsList = clazz.getImplements(); + if (implementsList != null) { + for (final ClassOrInterfaceType candidate : implementsList) { + final JavaType javaType = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, candidate, + typeParameterNames); + cidBuilder.addImplementsType(javaType); + } + } + + annotationsList = typeDeclaration.getAnnotations(); + members = clazz.getMembers(); + } + + if (enumClazz != null) { + final List constants = enumClazz + .getEntries(); + if (constants != null) { + for (final EnumConstantDeclaration enumConstants : constants) { + cidBuilder.addEnumConstant(new JavaSymbolName(enumConstants + .getName())); + } + } + + implementsList = enumClazz.getImplements(); + annotationsList = enumClazz.getAnnotations(); + members = enumClazz.getMembers(); + } + + if (annotationsList != null) { + for (final AnnotationExpr candidate : annotationsList) { + final AnnotationMetadata md = JavaParserAnnotationMetadataBuilder + .getInstance(candidate, compilationUnitServices) + .build(); + + final CommentStructure commentStructure = new CommentStructure(); + JavaParserCommentMetadataBuilder.updateCommentsToRoo( + commentStructure, candidate); + md.setCommentStructure(commentStructure); + + cidBuilder.addAnnotation(md); + } + } + + if (members != null) { + // Now we've finished declaring the type, we should introspect for + // any inner types that can thus be referred to in other body + // members + // We defer this until now because it's illegal to refer to an inner + // type in the signature of the enclosing type + for (final BodyDeclaration bodyDeclaration : members) { + if (bodyDeclaration instanceof TypeDeclaration) { + // Found a type + innerTypes.add((TypeDeclaration) bodyDeclaration); + } + } + + for (final BodyDeclaration member : members) { + if (member instanceof FieldDeclaration) { + final FieldDeclaration castMember = (FieldDeclaration) member; + for (final VariableDeclarator var : castMember + .getVariables()) { + final FieldMetadata field = JavaParserFieldMetadataBuilder + .getInstance(declaredByMetadataId, castMember, + var, compilationUnitServices, + typeParameterNames).build(); + + final CommentStructure commentStructure = new CommentStructure(); + JavaParserCommentMetadataBuilder.updateCommentsToRoo( + commentStructure, member); + field.setCommentStructure(commentStructure); + + cidBuilder.addField(field); + } + } + if (member instanceof MethodDeclaration) { + final MethodDeclaration castMember = (MethodDeclaration) member; + final MethodMetadata method = JavaParserMethodMetadataBuilder + .getInstance(declaredByMetadataId, castMember, + compilationUnitServices, typeParameterNames) + .build(); + + final CommentStructure commentStructure = new CommentStructure(); + JavaParserCommentMetadataBuilder.updateCommentsToRoo( + commentStructure, member); + method.setCommentStructure(commentStructure); + + cidBuilder.addMethod(method); + } + if (member instanceof ConstructorDeclaration) { + final ConstructorDeclaration castMember = (ConstructorDeclaration) member; + final ConstructorMetadata constructor = JavaParserConstructorMetadataBuilder + .getInstance(declaredByMetadataId, castMember, + compilationUnitServices, typeParameterNames) + .build(); + + final CommentStructure commentStructure = new CommentStructure(); + JavaParserCommentMetadataBuilder.updateCommentsToRoo( + commentStructure, member); + constructor.setCommentStructure(commentStructure); + + cidBuilder.addConstructor(constructor); + } + if (member instanceof TypeDeclaration) { + final TypeDeclaration castMember = (TypeDeclaration) member; + final JavaType innerType = new JavaType( + castMember.getName(), name); + final String innerTypeMetadataId = PhysicalTypeIdentifier + .createIdentifier(innerType, PhysicalTypeIdentifier + .getPath(declaredByMetadataId)); + final ClassOrInterfaceTypeDetails cid = new JavaParserClassOrInterfaceTypeDetailsBuilder( + compilationUnit, compilationUnitServices, + castMember, innerTypeMetadataId, innerType, + metadataService, typeLocationService).build(); + cidBuilder.addInnerType(cid); + } + } + } + + return cidBuilder.build(); + } + + private CompilationUnitServices getDefaultCompilationUnitServices() { + return new CompilationUnitServices() { + @Override + public JavaPackage getCompilationUnitPackage() { + return compilationUnitPackage; + } + + @Override + public JavaType getEnclosingTypeName() { + return name; + } + + @Override + public List getImports() { + return imports; + } + + @Override + public List getInnerTypes() { + return innerTypes; + } + + @Override + public PhysicalTypeCategory getPhysicalTypeCategory() { + return physicalTypeCategory; + } + }; + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserCommentMetadataBuilder.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserCommentMetadataBuilder.java new file mode 100644 index 000000000..6f724f214 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserCommentMetadataBuilder.java @@ -0,0 +1,160 @@ +package org.springframework.roo.classpath.antlrjavaparser.details; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.roo.classpath.details.comments.CommentStructure; + +import com.github.antlrjavaparser.api.BlockComment; +import com.github.antlrjavaparser.api.Comment; +import com.github.antlrjavaparser.api.LineComment; +import com.github.antlrjavaparser.api.Node; +import com.github.antlrjavaparser.api.body.JavadocComment; + +/** + * @author Mike De Haan + */ +public class JavaParserCommentMetadataBuilder { + + /** + * Adapt the any comments to the roo interface + * + * @param parserNode The antlr-java-parser node from which the comments will + * be read + * @param commentStructure The roo structure from which to retrieve comments + * @return List of comments from the antlr-java-parser package + */ + public static void updateCommentsToRoo( + final CommentStructure commentStructure, final Node parserNode) { + + // Nothing to do here + if (parserNode == null || commentStructure == null) { + return; + } + + commentStructure.setBeginComments(adaptToRooComments(parserNode + .getBeginComments())); + commentStructure.setInternalComments(adaptToRooComments(parserNode + .getInternalComments())); + commentStructure.setEndComments(adaptToRooComments(parserNode + .getEndComments())); + } + + /** + * Adapt the any comments to the antlr-java-parser interface + * + * @param parserNode The antlr-java-parser node to where the comments will + * be set + * @param commentStructure The roo structure from which to retrieve comments + * @return List of comments from the antlr-java-parser package + */ + public static void updateCommentsToJavaParser(final Node parserNode, + final CommentStructure commentStructure) { + + // Nothing to do here + if (parserNode == null || commentStructure == null) { + return; + } + + parserNode.setBeginComments(adaptComments(commentStructure + .getBeginComments())); + parserNode.setInternalComments(adaptComments(commentStructure + .getInternalComments())); + parserNode.setEndComments(adaptComments(commentStructure + .getEndComments())); + } + + /** + * Adapt a roo comment to antlr-java-parser comment + * + * @param antlrComments List of comments from the antlr-java-parser package + * @return List of comments from the roo package + */ + private static List adaptToRooComments( + final List antlrComments) { + + // Nothing to do here + if (antlrComments == null || antlrComments.size() == 0) { + return null; + } + + final List comments = new LinkedList(); + for (final Comment antlrComment : antlrComments) { + comments.add(adaptToRooComment(antlrComment)); + } + + return comments; + } + + /** + * Adapt a roo comment to antlr-java-parser comment + * + * @param antlrComment + * @return + */ + private static org.springframework.roo.classpath.details.comments.AbstractComment adaptToRooComment( + final Comment antlrComment) { + org.springframework.roo.classpath.details.comments.AbstractComment comment; + + if (antlrComment instanceof LineComment) { + comment = new org.springframework.roo.classpath.details.comments.LineComment(); + } + else if (antlrComment instanceof JavadocComment) { + comment = new org.springframework.roo.classpath.details.comments.JavadocComment(); + } + else { + comment = new org.springframework.roo.classpath.details.comments.BlockComment(); + } + + comment.setComment(antlrComment.getContent()); + + return comment; + } + + /** + * Adapt the roo interface to the antlr-java-parser interface + * + * @param rooComments List of comments from the roo package + * @return List of comments from the antlr-java-parser package + */ + private static List adaptComments( + final List rooComments) { + + // Nothing to do here + if (rooComments == null || rooComments.size() == 0) { + return null; + } + + final List comments = new LinkedList(); + for (final org.springframework.roo.classpath.details.comments.AbstractComment rooComment : rooComments) { + comments.add(adaptComment(rooComment)); + } + + return comments; + } + + /** + * Adapt the roo interface to the antlr-java-parser interface + * + * @param rooComment + * @return + */ + private static Comment adaptComment( + final org.springframework.roo.classpath.details.comments.AbstractComment rooComment) { + Comment comment; + + if (rooComment instanceof org.springframework.roo.classpath.details.comments.LineComment) { + comment = new LineComment(); + } + else if (rooComment instanceof org.springframework.roo.classpath.details.comments.JavadocComment) { + comment = new JavadocComment(); + } + else { + comment = new BlockComment(); + } + + comment.setContent(rooComment.getComment()); + + return comment; + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserConstructorMetadataBuilder.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserConstructorMetadataBuilder.java new file mode 100644 index 000000000..125d04b6e --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserConstructorMetadataBuilder.java @@ -0,0 +1,330 @@ +package org.springframework.roo.classpath.antlrjavaparser.details; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.antlrjavaparser.CompilationUnitServices; +import org.springframework.roo.classpath.antlrjavaparser.JavaParserUtils; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.JavaParser; +import com.github.antlrjavaparser.ParseException; +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.TypeParameter; +import com.github.antlrjavaparser.api.body.BodyDeclaration; +import com.github.antlrjavaparser.api.body.ConstructorDeclaration; +import com.github.antlrjavaparser.api.body.Parameter; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.body.VariableDeclaratorId; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.stmt.BlockStmt; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; +import com.github.antlrjavaparser.api.type.Type; + +/** + * Java Parser implementation of {@link ConstructorMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaParserConstructorMetadataBuilder implements + Builder { + + // TODO: Should parse the throws types from JavaParser source + + public static void addConstructor( + final CompilationUnitServices compilationUnitServices, + final List members, + final ConstructorMetadata constructor, + final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(constructor, "Method required"); + + // Start with the basic constructor + final ConstructorDeclaration d = new ConstructorDeclaration(); + d.setModifiers(JavaParserUtils.getJavaParserModifier(constructor + .getModifier())); + d.setName(PhysicalTypeIdentifier.getJavaType( + constructor.getDeclaredByMetadataId()).getSimpleTypeName()); + + // Add any constructor-level annotations (not parameter annotations) + final List annotations = new ArrayList(); + d.setAnnotations(annotations); + for (final AnnotationMetadata annotation : constructor.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, annotation); + } + + // Add any constructor parameters, including their individual + // annotations and type parameters + final List parameters = new ArrayList(); + d.setParameters(parameters); + int index = -1; + for (final AnnotatedJavaType constructorParameter : constructor + .getParameterTypes()) { + index++; + + // Add the parameter annotations applicable for this parameter type + final List parameterAnnotations = new ArrayList(); + + for (final AnnotationMetadata parameterAnnotation : constructorParameter + .getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, parameterAnnotations, + parameterAnnotation); + } + + // Compute the parameter name + final String parameterName = constructor.getParameterNames() + .get(index).getSymbolName(); + + // Compute the parameter type + Type parameterType = null; + if (constructorParameter.getJavaType().isPrimitive()) { + parameterType = JavaParserUtils.getType(constructorParameter + .getJavaType()); + } + else { + final Type finalType = JavaParserUtils.getResolvedName( + constructorParameter.getJavaType(), + constructorParameter.getJavaType(), + compilationUnitServices); + final ClassOrInterfaceType cit = JavaParserUtils + .getClassOrInterfaceType(finalType); + + // Add any type arguments presented for the return type + if (constructorParameter.getJavaType().getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : constructorParameter + .getJavaType().getParameters()) { + // NameExpr importedParameterType = + // JavaParserUtils.importTypeIfRequired(compilationUnitServices.getEnclosingTypeName(), + // compilationUnitServices.getImports(), parameter); + // typeArgs.add(JavaParserUtils.getReferenceType(importedParameterType)); + typeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + + } + parameterType = finalType; + } + + // Create a Java Parser constructor parameter and add it to the list + // of parameters + final Parameter p = new Parameter(parameterType, + new VariableDeclaratorId(parameterName)); + p.setAnnotations(parameterAnnotations); + parameters.add(p); + } + + // Set the body + if (constructor.getBody() == null + || constructor.getBody().length() == 0) { + d.setBlock(new BlockStmt()); + } + else { + // There is a body. + // We need to make a fake constructor that we can have JavaParser + // parse. + // Easiest way to do that is to build a simple source class + // containing the required method and re-parse it. + final StringBuilder sb = new StringBuilder(); + sb.append("class TemporaryClass {\n"); + sb.append(" TemporaryClass() {\n"); + sb.append(constructor.getBody()); + sb.append("\n"); + sb.append(" }\n"); + sb.append("}\n"); + final ByteArrayInputStream bais = new ByteArrayInputStream(sb + .toString().getBytes()); + CompilationUnit ci; + try { + ci = JavaParser.parse(bais); + } + catch (final IOException e) { + throw new IllegalStateException( + "Illegal state: Unable to parse input stream", e); + } + catch (final ParseException pe) { + throw new IllegalStateException( + "Illegal state: JavaParser did not parse correctly", pe); + } + final List types = ci.getTypes(); + if (types == null || types.size() != 1) { + throw new IllegalArgumentException("Method body invalid"); + } + final TypeDeclaration td = types.get(0); + final List bodyDeclarations = td.getMembers(); + if (bodyDeclarations == null || bodyDeclarations.size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return body declarations correctly"); + } + final BodyDeclaration bd = bodyDeclarations.get(0); + if (!(bd instanceof ConstructorDeclaration)) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a method declaration correctly"); + } + final ConstructorDeclaration cd = (ConstructorDeclaration) bd; + d.setBlock(cd.getBlock()); + } + + // Locate where to add this constructor; also verify if this method + // already exists + for (final BodyDeclaration bd : members) { + if (bd instanceof ConstructorDeclaration) { + // Next constructor should appear after this current constructor + final ConstructorDeclaration cd = (ConstructorDeclaration) bd; + if (cd.getParameters().size() == d.getParameters().size()) { + // Possible match, we need to consider parameter types as + // well now + final ConstructorMetadata constructorMetadata = new JavaParserConstructorMetadataBuilder( + constructor.getDeclaredByMetadataId(), cd, + compilationUnitServices, typeParameters).build(); + boolean matchesFully = true; + for (final AnnotatedJavaType existingParameter : constructorMetadata + .getParameterTypes()) { + if (!existingParameter.getJavaType().equals( + constructor.getParameterTypes().get(index))) { + matchesFully = false; + break; + } + } + if (matchesFully) { + throw new IllegalStateException("Constructor '" + + constructor.getParameterNames() + + "' already exists with identical parameters"); + } + } + } + } + + // Add the constructor to the end of the compilation unit + members.add(d); + } + + public static JavaParserConstructorMetadataBuilder getInstance( + final String declaredByMetadataId, + final ConstructorDeclaration constructorDeclaration, + final CompilationUnitServices compilationUnitServices, + final Set typeParameterNames) { + return new JavaParserConstructorMetadataBuilder(declaredByMetadataId, + constructorDeclaration, compilationUnitServices, + typeParameterNames); + } + + private final List annotations = new ArrayList(); + private String body; + private final String declaredByMetadataId; + private final int modifier; + private final List parameterNames = new ArrayList(); + + private final List parameterTypes = new ArrayList(); + + private final List throwsTypes = new ArrayList(); + + private JavaParserConstructorMetadataBuilder( + final String declaredByMetadataId, + final ConstructorDeclaration constructorDeclaration, + final CompilationUnitServices compilationUnitServices, + Set typeParameterNames) { + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(constructorDeclaration, + "Constructor declaration is mandatory"); + Validate.notNull(compilationUnitServices, + "Compilation unit services are required"); + + // Convert Java Parser modifier into JDK modifier + modifier = JavaParserUtils.getJdkModifier(constructorDeclaration + .getModifiers()); + + this.declaredByMetadataId = declaredByMetadataId; + + if (typeParameterNames == null) { + typeParameterNames = new HashSet(); + } + + // Add method-declared type parameters (if any) to the list of type + // parameters + final Set fullTypeParameters = new HashSet(); + fullTypeParameters.addAll(typeParameterNames); + final List params = constructorDeclaration + .getTypeParameters(); + if (params != null) { + for (final TypeParameter candidate : params) { + final JavaSymbolName currentTypeParam = new JavaSymbolName( + candidate.getName()); + fullTypeParameters.add(currentTypeParam); + } + } + + // Get the body + body = constructorDeclaration.getBlock().toString(); + body = StringUtils.replace(body, "{", "", 1); + body = body.substring(0, body.lastIndexOf("}")); + + // Lookup the parameters and their names + if (constructorDeclaration.getParameters() != null) { + for (final Parameter p : constructorDeclaration.getParameters()) { + final Type pt = p.getType(); + final JavaType parameterType = JavaParserUtils.getJavaType( + compilationUnitServices, pt, fullTypeParameters); + + final List annotationsList = p.getAnnotations(); + final List annotations = new ArrayList(); + if (annotationsList != null) { + for (final AnnotationExpr candidate : annotationsList) { + final JavaParserAnnotationMetadataBuilder md = JavaParserAnnotationMetadataBuilder + .getInstance(candidate, compilationUnitServices); + annotations.add(md.build()); + } + } + + parameterTypes.add(new AnnotatedJavaType(parameterType, + annotations)); + parameterNames.add(new JavaSymbolName(p.getId().getName())); + } + } + + if (constructorDeclaration.getAnnotations() != null) { + for (final AnnotationExpr annotation : constructorDeclaration + .getAnnotations()) { + annotations.add(JavaParserAnnotationMetadataBuilder + .getInstance(annotation, compilationUnitServices) + .build()); + } + } + } + + @Override + public ConstructorMetadata build() { + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + declaredByMetadataId); + constructorBuilder.setAnnotations(annotations); + constructorBuilder.setBodyBuilder(InvocableMemberBodyBuilder + .getInstance().append(body)); + constructorBuilder.setModifier(modifier); + constructorBuilder.setParameterNames(parameterNames); + constructorBuilder.setParameterTypes(parameterTypes); + constructorBuilder.setThrowsTypes(throwsTypes); + return constructorBuilder.build(); + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserFieldMetadataBuilder.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserFieldMetadataBuilder.java new file mode 100644 index 000000000..dea8881b8 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserFieldMetadataBuilder.java @@ -0,0 +1,326 @@ +package org.springframework.roo.classpath.antlrjavaparser.details; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.antlrjavaparser.CompilationUnitServices; +import org.springframework.roo.classpath.antlrjavaparser.JavaParserUtils; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.ASTHelper; +import com.github.antlrjavaparser.JavaParser; +import com.github.antlrjavaparser.ParseException; +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.body.BodyDeclaration; +import com.github.antlrjavaparser.api.body.FieldDeclaration; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.body.VariableDeclarator; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.Expression; +import com.github.antlrjavaparser.api.expr.NameExpr; +import com.github.antlrjavaparser.api.expr.ObjectCreationExpr; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; +import com.github.antlrjavaparser.api.type.Type; + +/** + * Java Parser implementation of {@link FieldMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaParserFieldMetadataBuilder implements Builder { + + public static void addField( + final CompilationUnitServices compilationUnitServices, + final List members, final FieldMetadata field) { + Validate.notNull(compilationUnitServices, + "Flushable compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(field, "Field required"); + + JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), field.getFieldType()); + final Type initType = JavaParserUtils.getResolvedName( + compilationUnitServices.getEnclosingTypeName(), + field.getFieldType(), compilationUnitServices); + final ClassOrInterfaceType finalType = JavaParserUtils + .getClassOrInterfaceType(initType); + + final FieldDeclaration newField = ASTHelper.createFieldDeclaration( + JavaParserUtils.getJavaParserModifier(field.getModifier()), + initType, field.getFieldName().getSymbolName()); + + // Add parameterized types for the field type (not initializer) + if (field.getFieldType().getParameters().size() > 0) { + final List fieldTypeArgs = new ArrayList(); + finalType.setTypeArgs(fieldTypeArgs); + for (final JavaType parameter : field.getFieldType() + .getParameters()) { + fieldTypeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + } + + final List vars = newField.getVariables(); + Validate.notEmpty(vars, + "Expected ASTHelper to have provided a single VariableDeclarator"); + Validate.isTrue(vars.size() == 1, + "Expected ASTHelper to have provided a single VariableDeclarator"); + final VariableDeclarator vd = vars.iterator().next(); + + if (StringUtils.isNotBlank(field.getFieldInitializer())) { + // There is an initializer. + // We need to make a fake field that we can have JavaParser parse. + // Easiest way to do that is to build a simple source class + // containing the required field and re-parse it. + final StringBuilder sb = new StringBuilder(); + sb.append("class TemporaryClass {\n"); + sb.append(" private " + field.getFieldType() + " " + + field.getFieldName() + " = " + + field.getFieldInitializer() + ";\n"); + sb.append("}\n"); + final ByteArrayInputStream bais = new ByteArrayInputStream(sb + .toString().getBytes()); + CompilationUnit ci; + try { + ci = JavaParser.parse(bais); + } + catch (final IOException e) { + throw new IllegalStateException( + "Illegal state: Unable to parse input stream", e); + } + catch (final ParseException pe) { + throw new IllegalStateException( + "Illegal state: JavaParser did not parse correctly", pe); + } + final List types = ci.getTypes(); + if (types == null || types.size() != 1) { + throw new IllegalArgumentException("Field member invalid"); + } + final TypeDeclaration td = types.get(0); + final List bodyDeclarations = td.getMembers(); + if (bodyDeclarations == null || bodyDeclarations.size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return body declarations correctly"); + } + final BodyDeclaration bd = bodyDeclarations.get(0); + if (!(bd instanceof FieldDeclaration)) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a field declaration correctly"); + } + final FieldDeclaration fd = (FieldDeclaration) bd; + if (fd.getVariables() == null || fd.getVariables().size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a field declaration correctly"); + } + + final Expression init = fd.getVariables().get(0).getInit(); + + // Resolve imports (ROO-1505) + if (init instanceof ObjectCreationExpr) { + final ObjectCreationExpr ocr = (ObjectCreationExpr) init; + final JavaType typeToImport = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, ocr.getType(), null); + final NameExpr nameExpr = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), typeToImport); + final ClassOrInterfaceType classOrInterfaceType = JavaParserUtils + .getClassOrInterfaceType(nameExpr); + ocr.setType(classOrInterfaceType); + + if (typeToImport.getParameters().size() > 0) { + final List initTypeArgs = new ArrayList(); + finalType.setTypeArgs(initTypeArgs); + for (final JavaType parameter : typeToImport + .getParameters()) { + initTypeArgs.add(JavaParserUtils + .importParametersForType( + compilationUnitServices + .getEnclosingTypeName(), + compilationUnitServices.getImports(), + parameter)); + } + classOrInterfaceType.setTypeArgs(initTypeArgs); + } + } + + vd.setInit(init); + } + + // Add annotations + final List annotations = new ArrayList(); + newField.setAnnotations(annotations); + for (final AnnotationMetadata annotation : field.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, annotation); + } + + // Locate where to add this field; also verify if this field already + // exists + int nextFieldIndex = 0; + int i = -1; + for (final BodyDeclaration bd : members) { + i++; + if (bd instanceof FieldDeclaration) { + // Next field should appear after this current field + nextFieldIndex = i + 1; + final FieldDeclaration bdf = (FieldDeclaration) bd; + for (final VariableDeclarator v : bdf.getVariables()) { + Validate.isTrue(!field.getFieldName().getSymbolName() + .equals(v.getId().getName()), + "A field with name '%s' already exists", field + .getFieldName().getSymbolName()); + } + } + } + + if (field.getCommentStructure() != null) { + + // if the field has annotations, add JavaDoc comments to the first + // annotation + if (annotations != null && annotations.size() > 0) { + AnnotationExpr firstAnnotation = annotations.get(0); + + JavaParserCommentMetadataBuilder.updateCommentsToJavaParser( + firstAnnotation, field.getCommentStructure()); + + // Otherwise, add comments to the field declaration line + } + else { + JavaParserCommentMetadataBuilder.updateCommentsToJavaParser( + newField, field.getCommentStructure()); + } + } + + // Add the field to the compilation unit + members.add(nextFieldIndex, newField); + } + + public static JavaParserFieldMetadataBuilder getInstance( + final String declaredByMetadataId, + final FieldDeclaration fieldDeclaration, + final VariableDeclarator var, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + return new JavaParserFieldMetadataBuilder(declaredByMetadataId, + fieldDeclaration, var, compilationUnitServices, typeParameters); + } + + public static void removeField( + final CompilationUnitServices compilationUnitServices, + final List members, final JavaSymbolName fieldName) { + Validate.notNull(compilationUnitServices, + "Flushable compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(fieldName, "Field name to remove is required"); + + // Locate the field + int i = -1; + int toDelete = -1; + for (final BodyDeclaration bd : members) { + i++; + if (bd instanceof FieldDeclaration) { + final FieldDeclaration fieldDeclaration = (FieldDeclaration) bd; + for (final VariableDeclarator var : fieldDeclaration + .getVariables()) { + if (var.getId().getName().equals(fieldName.getSymbolName())) { + toDelete = i; + break; + } + } + } + } + + Validate.isTrue(toDelete > -1, "Could not locate field '%s' to delete", + fieldName); + + // Do removal outside iteration of body declaration members, to avoid + // concurrent modification exceptions + members.remove(toDelete); + } + + private final List annotations = new ArrayList(); + private final String declaredByMetadataId; + private String fieldInitializer; + + private final JavaSymbolName fieldName; + + private JavaType fieldType; + + private final int modifier; + + private JavaParserFieldMetadataBuilder(final String declaredByMetadataId, + final FieldDeclaration fieldDeclaration, + final VariableDeclarator var, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + Validate.notNull(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(fieldDeclaration, "Field declaration is mandatory"); + Validate.notNull(var, "Variable declarator required"); + Validate.isTrue(fieldDeclaration.getVariables().contains(var), + "Cannot request a variable not already in the field declaration"); + Validate.notNull(compilationUnitServices, + "Compilation unit services are required"); + + // Convert Java Parser modifier into JDK modifier + modifier = JavaParserUtils.getJdkModifier(fieldDeclaration + .getModifiers()); + + this.declaredByMetadataId = declaredByMetadataId; + + final Type type = fieldDeclaration.getType(); + fieldType = JavaParserUtils.getJavaType(compilationUnitServices, type, + typeParameters); + + // Convert into an array if this variable ID uses array notation + if (var.getId().getArrayCount() > 0) { + fieldType = new JavaType(fieldType.getFullyQualifiedTypeName(), var + .getId().getArrayCount() + fieldType.getArray(), + fieldType.getDataType(), fieldType.getArgName(), + fieldType.getParameters()); + } + + fieldName = new JavaSymbolName(var.getId().getName()); + + // Lookup initializer, if one was requested and easily determinable + final Expression e = var.getInit(); + if (e != null) { + fieldInitializer = e.toString(); + } + + final List annotations = fieldDeclaration + .getAnnotations(); + if (annotations != null) { + for (final AnnotationExpr annotation : annotations) { + this.annotations.add(JavaParserAnnotationMetadataBuilder + .getInstance(annotation, compilationUnitServices) + .build()); + } + } + } + + @Override + public FieldMetadata build() { + final FieldMetadataBuilder fieldMetadataBuilder = new FieldMetadataBuilder( + declaredByMetadataId); + fieldMetadataBuilder.setAnnotations(annotations); + fieldMetadataBuilder.setFieldInitializer(fieldInitializer); + fieldMetadataBuilder.setFieldName(fieldName); + fieldMetadataBuilder.setFieldType(fieldType); + fieldMetadataBuilder.setModifier(modifier); + return fieldMetadataBuilder.build(); + } +} diff --git a/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserMethodMetadataBuilder.java b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserMethodMetadataBuilder.java new file mode 100644 index 000000000..9e74e9e92 --- /dev/null +++ b/classpath-antlrjavaparser/src/main/java/org/springframework/roo/classpath/antlrjavaparser/details/JavaParserMethodMetadataBuilder.java @@ -0,0 +1,431 @@ +package org.springframework.roo.classpath.antlrjavaparser.details; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.antlrjavaparser.CompilationUnitServices; +import org.springframework.roo.classpath.antlrjavaparser.JavaParserUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +import com.github.antlrjavaparser.JavaParser; +import com.github.antlrjavaparser.ParseException; +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.TypeParameter; +import com.github.antlrjavaparser.api.body.BodyDeclaration; +import com.github.antlrjavaparser.api.body.MethodDeclaration; +import com.github.antlrjavaparser.api.body.Parameter; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import com.github.antlrjavaparser.api.body.VariableDeclaratorId; +import com.github.antlrjavaparser.api.expr.AnnotationExpr; +import com.github.antlrjavaparser.api.expr.NameExpr; +import com.github.antlrjavaparser.api.stmt.BlockStmt; +import com.github.antlrjavaparser.api.type.ClassOrInterfaceType; +import com.github.antlrjavaparser.api.type.ReferenceType; +import com.github.antlrjavaparser.api.type.Type; + +/** + * Java Parser implementation of {@link MethodMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaParserMethodMetadataBuilder implements Builder { + + public static void addMethod( + final CompilationUnitServices compilationUnitServices, + final List members, final MethodMetadata method, + Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Flushable compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(method, "Method required"); + + if (typeParameters == null) { + typeParameters = new HashSet(); + } + + // Create the return type we should use + Type returnType = null; + if (method.getReturnType().isPrimitive()) { + returnType = JavaParserUtils.getType(method.getReturnType()); + } + else { + final NameExpr importedType = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + method.getReturnType()); + final ClassOrInterfaceType cit = JavaParserUtils + .getClassOrInterfaceType(importedType); + + // Add any type arguments presented for the return type + if (method.getReturnType().getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : method.getReturnType() + .getParameters()) { + typeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + } + + // Handle arrays + if (method.getReturnType().isArray()) { + final ReferenceType rt = new ReferenceType(); + rt.setArrayCount(method.getReturnType().getArray()); + rt.setType(cit); + returnType = rt; + } + else { + returnType = cit; + } + } + + // Start with the basic method + final MethodDeclaration d = new MethodDeclaration(); + d.setModifiers(JavaParserUtils.getJavaParserModifier(method + .getModifier())); + d.setName(method.getMethodName().getSymbolName()); + d.setType(returnType); + + // Add any method-level annotations (not parameter annotations) + final List annotations = new ArrayList(); + d.setAnnotations(annotations); + for (final AnnotationMetadata annotation : method.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, annotation); + } + + // Add any method parameters, including their individual annotations and + // type parameters + final List parameters = new ArrayList(); + d.setParameters(parameters); + + int index = -1; + for (final AnnotatedJavaType methodParameter : method + .getParameterTypes()) { + index++; + + // Add the parameter annotations applicable for this parameter type + final List parameterAnnotations = new ArrayList(); + + for (final AnnotationMetadata parameterAnnotation : methodParameter + .getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, parameterAnnotations, + parameterAnnotation); + } + + // Compute the parameter name + final String parameterName = method.getParameterNames().get(index) + .getSymbolName(); + + // Compute the parameter type + Type parameterType = null; + if (methodParameter.getJavaType().isPrimitive()) { + parameterType = JavaParserUtils.getType(methodParameter + .getJavaType()); + } + else { + final NameExpr type = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + methodParameter.getJavaType()); + final ClassOrInterfaceType cit = JavaParserUtils + .getClassOrInterfaceType(type); + + // Add any type arguments presented for the return type + if (methodParameter.getJavaType().getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : methodParameter + .getJavaType().getParameters()) { + typeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + } + + // Handle arrays + if (methodParameter.getJavaType().isArray()) { + final ReferenceType rt = new ReferenceType(); + rt.setArrayCount(methodParameter.getJavaType().getArray()); + rt.setType(cit); + parameterType = rt; + } + else { + parameterType = cit; + } + } + + // Create a Java Parser method parameter and add it to the list of + // parameters + final Parameter p = new Parameter(parameterType, + new VariableDeclaratorId(parameterName)); + p.setVarArgs(methodParameter.isVarArgs()); + p.setAnnotations(parameterAnnotations); + parameters.add(p); + } + + // Add exceptions which the method my throw + if (method.getThrowsTypes().size() > 0) { + final List throwsTypes = new ArrayList(); + for (final JavaType javaType : method.getThrowsTypes()) { + final NameExpr importedType = JavaParserUtils + .importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), javaType); + throwsTypes.add(importedType); + } + d.setThrows(throwsTypes); + } + + // Set the body + if (StringUtils.isBlank(method.getBody())) { + // Never set the body if an abstract method + if (!Modifier.isAbstract(method.getModifier()) + && !PhysicalTypeCategory.INTERFACE + .equals(compilationUnitServices + .getPhysicalTypeCategory())) { + d.setBody(new BlockStmt()); + } + } + else { + // There is a body. + // We need to make a fake method that we can have JavaParser parse. + // Easiest way to do that is to build a simple source class + // containing the required method and re-parse it. + final StringBuilder sb = new StringBuilder(); + sb.append("class TemporaryClass {\n"); + sb.append(" public void temporaryMethod() {\n"); + sb.append(method.getBody()); + sb.append("\n"); + sb.append(" }\n"); + sb.append("}\n"); + final ByteArrayInputStream bais = new ByteArrayInputStream(sb + .toString().getBytes()); + CompilationUnit ci; + try { + ci = JavaParser.parse(bais); + } + catch (final IOException e) { + throw new IllegalStateException( + "Illegal state: Unable to parse input stream", e); + } + catch (final ParseException pe) { + throw new IllegalStateException( + "Illegal state: JavaParser did not parse correctly", pe); + } + final List types = ci.getTypes(); + if (types == null || types.size() != 1) { + throw new IllegalArgumentException("Method body invalid"); + } + final TypeDeclaration td = types.get(0); + final List bodyDeclarations = td.getMembers(); + if (bodyDeclarations == null || bodyDeclarations.size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return body declarations correctly"); + } + final BodyDeclaration bd = bodyDeclarations.get(0); + if (!(bd instanceof MethodDeclaration)) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a method declaration correctly"); + } + final MethodDeclaration md = (MethodDeclaration) bd; + d.setBody(md.getBody()); + } + + // Locate where to add this method; also verify if this method already + // exists + for (final BodyDeclaration bd : members) { + if (bd instanceof MethodDeclaration) { + // Next method should appear after this current method + final MethodDeclaration md = (MethodDeclaration) bd; + if (md.getName().equals(d.getName())) { + if ((md.getParameters() == null || md.getParameters() + .isEmpty()) + && (d.getParameters() == null || d.getParameters() + .isEmpty())) { + throw new IllegalStateException("Method '" + + method.getMethodName().getSymbolName() + + "' already exists"); + } + else if (md.getParameters() != null + && md.getParameters().size() == d.getParameters() + .size()) { + // Possible match, we need to consider parameter types + // as well now + final MethodMetadata methodMetadata = JavaParserMethodMetadataBuilder + .getInstance(method.getDeclaredByMetadataId(), + md, compilationUnitServices, + typeParameters).build(); + boolean matchesFully = true; + index = -1; + for (final AnnotatedJavaType existingParameter : methodMetadata + .getParameterTypes()) { + index++; + final AnnotatedJavaType parameterType = method + .getParameterTypes().get(index); + if (!existingParameter.getJavaType().equals( + parameterType.getJavaType())) { + matchesFully = false; + break; + } + } + if (matchesFully) { + throw new IllegalStateException( + "Method '" + + method.getMethodName() + .getSymbolName() + + "' already exists with identical parameters"); + } + } + } + } + } + + // Add the method to the end of the compilation unit + members.add(d); + } + + public static JavaParserMethodMetadataBuilder getInstance( + final String declaredByMetadataId, + final MethodDeclaration methodDeclaration, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + return new JavaParserMethodMetadataBuilder(declaredByMetadataId, + methodDeclaration, compilationUnitServices, typeParameters); + } + + private final List annotations = new ArrayList(); + private String body; + private final String declaredByMetadataId; + private final JavaSymbolName methodName; + private final int modifier; + private final List parameterNames = new ArrayList(); + private final List parameterTypes = new ArrayList(); + + private final JavaType returnType; + + private final List throwsTypes = new ArrayList(); + + private JavaParserMethodMetadataBuilder(final String declaredByMetadataId, + final MethodDeclaration methodDeclaration, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(methodDeclaration, "Method declaration is mandatory"); + Validate.notNull(compilationUnitServices, + "Compilation unit services are required"); + + this.declaredByMetadataId = declaredByMetadataId; + + // Convert Java Parser modifier into JDK modifier + modifier = JavaParserUtils.getJdkModifier(methodDeclaration + .getModifiers()); + + // Add method-declared type parameters (if any) to the list of type + // parameters + final Set fullTypeParameters = new HashSet(); + fullTypeParameters.addAll(typeParameters); + final List params = methodDeclaration + .getTypeParameters(); + if (params != null) { + for (final TypeParameter candidate : params) { + final JavaSymbolName currentTypeParam = new JavaSymbolName( + candidate.getName()); + fullTypeParameters.add(currentTypeParam); + } + } + + // Compute the return type + final Type rt = methodDeclaration.getType(); + returnType = JavaParserUtils.getJavaType(compilationUnitServices, rt, + fullTypeParameters); + + // Compute the method name + methodName = new JavaSymbolName(methodDeclaration.getName()); + + // Get the body + body = methodDeclaration.getBody() == null ? null : methodDeclaration + .getBody().toString(); + if (body != null) { + body = StringUtils.replace(body, "{", "", 1); + body = body.substring(0, body.lastIndexOf("}")); + } + + // Lookup the parameters and their names + if (methodDeclaration.getParameters() != null) { + for (final Parameter p : methodDeclaration.getParameters()) { + final Type pt = p.getType(); + final JavaType parameterType = JavaParserUtils.getJavaType( + compilationUnitServices, pt, fullTypeParameters); + final List annotationsList = p.getAnnotations(); + final List annotations = new ArrayList(); + if (annotationsList != null) { + for (final AnnotationExpr candidate : annotationsList) { + final AnnotationMetadata annotationMetadata = JavaParserAnnotationMetadataBuilder + .getInstance(candidate, compilationUnitServices) + .build(); + annotations.add(annotationMetadata); + } + } + final AnnotatedJavaType param = new AnnotatedJavaType( + parameterType, annotations); + param.setVarArgs(p.isVarArgs()); + parameterTypes.add(param); + parameterNames.add(new JavaSymbolName(p.getId().getName())); + } + } + + if (methodDeclaration.getThrows() != null) { + for (final NameExpr throwsType : methodDeclaration.getThrows()) { + final JavaType throwing = JavaParserUtils + .getJavaType(compilationUnitServices, throwsType, + fullTypeParameters); + throwsTypes.add(throwing); + } + } + + if (methodDeclaration.getAnnotations() != null) { + for (final AnnotationExpr annotation : methodDeclaration + .getAnnotations()) { + annotations.add(JavaParserAnnotationMetadataBuilder + .getInstance(annotation, compilationUnitServices) + .build()); + } + } + } + + @Override + public MethodMetadata build() { + final MethodMetadataBuilder methodMetadataBuilder = new MethodMetadataBuilder( + declaredByMetadataId); + methodMetadataBuilder.setMethodName(methodName); + methodMetadataBuilder.setReturnType(returnType); + methodMetadataBuilder.setAnnotations(annotations); + methodMetadataBuilder.setBodyBuilder(InvocableMemberBodyBuilder + .getInstance().append(body)); + methodMetadataBuilder.setModifier(modifier); + methodMetadataBuilder.setParameterNames(parameterNames); + methodMetadataBuilder.setParameterTypes(parameterTypes); + methodMetadataBuilder.setThrowsTypes(throwsTypes); + return methodMetadataBuilder.build(); + } +} diff --git a/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeParsingServiceTest.java b/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeParsingServiceTest.java new file mode 100644 index 000000000..4a03f5d4e --- /dev/null +++ b/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/JavaParserTypeParsingServiceTest.java @@ -0,0 +1,117 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import com.github.antlrjavaparser.JavaParser; +import com.github.antlrjavaparser.api.CompilationUnit; +import com.github.antlrjavaparser.api.body.TypeDeclaration; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.antlrjavaparser.details.JavaParserClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Unit test of {@link JavaParserTypeParsingService} + * + * @author Andrew Swan + * @since 1.2.0 + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ JavaParserClassOrInterfaceTypeDetailsBuilder.class, + JavaParser.class, JavaParserUtils.class }) +public class JavaParserTypeParsingServiceTest { + + private static final String DECLARED_BY_MID = "MID:foo#bar"; + private static final String EMPTY_FILE = "package com.example;"; + + private static final String SOURCE_FILE = "package com.example;" + "" + + "public class MyClass {}" + "" + "class TargetClass {}" + "" + + "class OtherClass {}"; + @Mock private MetadataService mockMetadataService; + @Mock private TypeLocationService mockTypeLocationService; + + // Fixture + private JavaParserTypeParsingService typeParsingService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + typeParsingService = new JavaParserTypeParsingService(); + typeParsingService.metadataService = mockMetadataService; + typeParsingService.typeLocationService = mockTypeLocationService; + } + + @Test + public void testGetTypeFromStringWhenFileContainsNoSuchType() { + // Set up + final JavaType mockTargetType = mock(JavaType.class); + when(mockTargetType.getSimpleTypeName()).thenReturn("NoSuchType"); + + // Invoke + final ClassOrInterfaceTypeDetails locatedType = typeParsingService + .getTypeFromString(SOURCE_FILE, DECLARED_BY_MID, mockTargetType); + + // Check + assertNull(locatedType); + } + + @Test + public void testGetTypeFromStringWhenFileContainsNoTypes() { + // Set up + final JavaType mockTargetType = mock(JavaType.class); + + // Invoke + final ClassOrInterfaceTypeDetails locatedType = typeParsingService + .getTypeFromString(EMPTY_FILE, DECLARED_BY_MID, mockTargetType); + + // Check + assertNull(locatedType); + } + + @Test + public void testGetTypeFromStringWhenFileContainsThatType() + throws Exception { + // Set up + final JavaType mockTargetType = mock(JavaType.class); + final TypeDeclaration mockTypeDeclaration = mock(TypeDeclaration.class); + final ClassOrInterfaceTypeDetails mockClassOrInterfaceTypeDetails = mock(ClassOrInterfaceTypeDetails.class); + final JavaParserClassOrInterfaceTypeDetailsBuilder mockBuilder = mock(JavaParserClassOrInterfaceTypeDetailsBuilder.class); + when(mockBuilder.build()).thenReturn(mockClassOrInterfaceTypeDetails); + + mockStatic(JavaParserUtils.class); + when( + JavaParserUtils.locateTypeDeclaration( + any(CompilationUnit.class), eq(mockTargetType))) + .thenReturn(mockTypeDeclaration); + + mockStatic(JavaParserClassOrInterfaceTypeDetailsBuilder.class); + when( + JavaParserClassOrInterfaceTypeDetailsBuilder.getInstance( + any(CompilationUnit.class), + (CompilationUnitServices) eq(null), + eq(mockTypeDeclaration), eq(DECLARED_BY_MID), + eq(mockTargetType), eq(mockMetadataService), + eq(mockTypeLocationService))).thenReturn(mockBuilder); + + // Invoke + final ClassOrInterfaceTypeDetails locatedType = typeParsingService + .getTypeFromString(SOURCE_FILE, DECLARED_BY_MID, mockTargetType); + + // Check + assertSame(mockClassOrInterfaceTypeDetails, locatedType); + } +} diff --git a/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/NewUpdateCompilationUnitTest.java b/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/NewUpdateCompilationUnitTest.java new file mode 100644 index 000000000..460da7f84 --- /dev/null +++ b/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/NewUpdateCompilationUnitTest.java @@ -0,0 +1,669 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URL; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Functional test of + * {@link JavaParserTypeParsingService#updateAndGetCompilationUnitContents(String, org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails)} + * + * @author DiSiD Technologies + * @since 1.2.1 + */ +@RunWith(PowerMockRunner.class) +public class NewUpdateCompilationUnitTest { + + private static final String SIMPLE_INTERFACE_FILE_PATH = "SimpleInterface.java.test"; + private static final String SIMPLE_CLASS_FILE_PATH = "SimpleClass.java.test"; + private static final String SIMPLE_CLASS2_FILE_PATH = "SimpleClass2.java.test"; + private static final String SIMPLE_CLASS3_FILE_PATH = "SimpleClass3.java.test"; + private static final String ROO1505_CLASS_FILE_PATH = "Roo_1505.java.test"; + private static final String ENUM_FILE_PATH = "AEnumerate.java.test"; + + private static final JavaType SIMPLE_INTERFACE_TYPE = new JavaType( + "org.myPackage.SimpleInterface"); + private static final JavaType SIMPLE_CLASS_TYPE = new JavaType( + "org.myPackage.SimpleClass"); + private static final JavaType SIMPLE_CLASS2_TYPE = new JavaType( + "org.myPackage.SimpleClass2"); + private static final JavaType SIMPLE_CLASS3_TYPE = new JavaType( + "org.myPackage.SimpleClass3"); + private static final JavaType ROO1505_CLASS_TYPE = new JavaType( + "com.pet.Roo_1505"); + private static final JavaType ENUM_TYPE = new JavaType( + "org.myPackage.AEnumerate"); + + private static final String SIMPLE_INTERFACE_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#bar?SimpleInterface"; + private static final String SIMPLE_CLASS_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass"; + private static final String SIMPLE_CLASS2_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass2"; + private static final String SIMPLE_CLASS3_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass3"; + private static final String ROO1505_CLASS_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?Roo_1505"; + private static final String ENUM_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?AEnumerate"; + + @Mock private MetadataService mockMetadataService; + @Mock private TypeLocationService mockTypeLocationService; + + // Fixture + private JavaParserTypeParsingService typeParsingService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + typeParsingService = new JavaParserTypeParsingService(); + typeParsingService.metadataService = mockMetadataService; + typeParsingService.typeLocationService = mockTypeLocationService; + } + + @Test + public void testSimpleInterfaceNoChanges() throws Exception { + + // Set up + final File file = getResource(SIMPLE_INTERFACE_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, + SIMPLE_INTERFACE_DECLARED_BY_MID, SIMPLE_INTERFACE_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getCanonicalPath(), + simpleInterfaceDetails); + + saveResult(file, result); + + checkSimpleInterface(result, true); + } + + @Test + public void testEnumNoChanges() throws Exception { + // Set up + final File file = getResource(ENUM_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, ENUM_DECLARED_BY_MID, + ENUM_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getCanonicalPath(), + simpleInterfaceDetails); + + saveResult(file, result); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails2 = typeParsingService + .getTypeFromString(result, ENUM_DECLARED_BY_MID, ENUM_TYPE); + + typeParsingService.updateAndGetCompilationUnitContents( + file.getCanonicalPath(), simpleInterfaceDetails2); + + checkEnum(result); + } + + @Test + public void testEnumAddElement() throws Exception { + + // Set up + final File file = getResource(ENUM_FILE_PATH); + final String fileContents = getResourceContents(file); + final String filePath = file.getCanonicalPath(); + + final ClassOrInterfaceTypeDetails enumDetails = typeParsingService + .getTypeFromString(fileContents, ENUM_DECLARED_BY_MID, + ENUM_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(filePath, enumDetails); + + saveResult(file, result); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + enumDetails); + + cidBuilder.addEnumConstant(new JavaSymbolName("ALIEN")); + + final ClassOrInterfaceTypeDetails enumDetails2 = cidBuilder.build(); + + // Invoke + final String result2 = typeParsingService + .updateAndGetCompilationUnitContents(filePath, enumDetails2); + + saveResult(file, result2, "addedConst"); + + checkEnum(result2); + + assertTrue(result2.contains("MALE, FEMALE, ALIEN")); + + } + + @Test + public void testSimpleInterfaceVoidFile() throws Exception { + + // Set up + final File file = getResource(SIMPLE_INTERFACE_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, + SIMPLE_INTERFACE_DECLARED_BY_MID, SIMPLE_INTERFACE_TYPE); + + final File voidFile = new File(file.getCanonicalFile() + ".void"); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents( + voidFile.getCanonicalPath(), simpleInterfaceDetails); + + saveResult(file, result, "-void"); + + checkSimpleInterface(result, false); + } + + @Test + public void testSimpleClassNoChanges() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getCanonicalPath(), + simpleInterfaceDetails); + + saveResult(file, result); + + checkSimpleClass(result); + } + + @Test + public void testSimpleClass2NoChanges() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS2_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS2_DECLARED_BY_MID, + SIMPLE_CLASS2_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getAbsolutePath(), + simpleInterfaceDetails); + + saveResult(file, result); + + checkSimple2Class(result); + } + + @Test + public void testSimpleClass3NoChanges() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS3_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getAbsolutePath(), + simpleInterfaceDetails); + + saveResult(file, result); + + checkSimple3Class(result); + } + + @Test + public void testRegresion_ROO_1505() throws Exception { + + // Set up + final File file = getResource(ROO1505_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, ROO1505_CLASS_DECLARED_BY_MID, + ROO1505_CLASS_TYPE); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getAbsolutePath(), + simpleInterfaceDetails); + + saveResult(file, result); + + check_ROO_1505_Class(result); + } + + @Test + public void testSimpleClassAddField() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + SIMPLE_CLASS_DECLARED_BY_MID, Modifier.PRIVATE, + new JavaSymbolName("newFieldAddedByCode"), new JavaType( + String.class), "\"Create by code\""); + final ClassOrInterfaceTypeDetails newSimpleInterfaceDetails = addField( + simpleInterfaceDetails, fieldBuilder.build()); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getCanonicalPath(), + newSimpleInterfaceDetails); + + saveResult(file, result, "-addedField"); + + checkSimpleClass(result); + + assertTrue(result + .contains("private String newFieldAddedByCode = \"Create by code\";")); + } + + @Test + public void testSimpleClassAddAnnotation() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + new JavaType( + "org.springframework.roo.addon.tostring.RooToString")); + final ClassOrInterfaceTypeDetails newSimpleInterfaceDetails = addAnnotation( + simpleInterfaceDetails, annotationBuilder.build()); + + // Invoke + final String result = typeParsingService + .updateAndGetCompilationUnitContents(file.getCanonicalPath(), + newSimpleInterfaceDetails); + + saveResult(file, result, "-addedAnnotation"); + + checkSimpleClass(result); + + assertTrue(result + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result.contains("@RooToString")); + + // Invoke again + final ClassOrInterfaceTypeDetails simpleInterfaceDetails2 = typeParsingService + .getTypeFromString(result, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final String result2 = typeParsingService + .updateAndGetCompilationUnitContents(file.getCanonicalPath(), + simpleInterfaceDetails2); + + saveResult(file, result2, "-addedAnnotation2"); + + checkSimpleClass(result2); + + assertTrue(result2 + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result2.contains("@RooToString")); + + } + + public static ClassOrInterfaceTypeDetails addField( + final ClassOrInterfaceTypeDetails ptd, final FieldMetadata field) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + ptd); + cidBuilder.addField(field); + return cidBuilder.build(); + } + + public static ClassOrInterfaceTypeDetails addAnnotation( + final ClassOrInterfaceTypeDetails ptd, + final AnnotationMetadata annotation) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + ptd); + cidBuilder.addAnnotation(annotation); + return cidBuilder.build(); + } + + public static void checkSimpleClass(final String result) { + // check headers and import + assertTrue(result.contains("* File header")); + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("// Simple comment1")); + assertTrue(result.contains("import test.importtest.pkg;")); + assertTrue(result.contains("Comment about import")); + assertTrue(result + .contains("import static test.importtest.pkg.Hola.proofMethod;")); + assertTrue(result.contains("import java.util.*;")); + + // Check class declaration + assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public class SimpleClass")); + assertTrue(result.contains("extends OtheClass")); + assertTrue(result.contains("implements SimpleInterface")); + + assertTrue(result.contains("== Comment in code ==")); + + // Check fields + assertTrue(result.contains("* Javadoc for field")); + assertTrue(result.contains("private final String[] params;")); + + assertTrue(result.contains("* Javadoc for column field")); + assertTrue(result.contains("@Column(name = \"VALOR\", length = 500)")); + assertTrue(result.contains("@Size(max = 500)")); + assertTrue(result.contains("@NotNull")); + assertTrue(result.contains("private final String valor;")); + + + assertTrue(result.contains("* Javadoc for transient field")); + assertTrue(result.contains("@Transient")); + assertTrue(result.contains("protected Double param1 = new Double(12);")); + assertTrue(result + .contains("private List[] listArray = new List[3];")); + assertTrue(result + .contains("Set[] setArray = new Set[] { null, null, null };")); + assertTrue(result.contains("* Enum javaDoc")); + assertTrue(result.contains("public enum theNumbers")); + assertTrue(result.contains("uno, dos, tres")); + + // Check constructors + assertTrue(result.contains("@Documented(\"aaadbbbdd\")")); + // XXX Not supported Variable Args in Spring Rooo 'public + // SimpleClass(Double param1, String + // params)' + // assertTrue(result.contains("public SimpleClass(Double param1, String... params)")); + assertTrue(result + .contains("public SimpleClass(Double param1, String[] params, Set[] setArrayParam)")); + assertTrue(result.contains("* Public constructor")); + assertTrue(result.contains("// Comment in public constructor")); + assertTrue(result.contains("this.param1 = param1;")); + assertTrue(result.contains("this.params = params;")); + assertTrue(result.contains("* Private constructor")); + assertTrue(result.contains("private SimpleClass()")); + assertTrue(result.contains("* Comment in private constructor")); + assertTrue(result.contains("this.param1 = null;")); + assertTrue(result.contains("// Comment inline1")); + assertTrue(result.contains("// other comment between inline")); + assertTrue(result.contains("this.params = null;")); + // XXX JavaParser Bug + // assertTrue(result.contains("// Comment inline2")); + + // Check method hello declaration + assertTrue(result.contains("* Javadoc of hello method")); + assertTrue(result + .contains("@Deprecated(message = \"Do not use\", more = \"Nothing\")")); + assertTrue(result.contains("@Override")); + assertTrue(result.contains("public Sting hello(String value)")); + + // Check method methodVoid + assertTrue(result.contains("* methodVoid JavaDoc")); + assertTrue(result + .contains("void methodVoid(Double param1, String[] params, Map[] mapArrayParam)")); + assertTrue(result.contains("// Comment before for")); + assertTrue(result + .contains("for (Map map : mapArrayParam)")); + assertTrue(result.contains("// comment inside for")); + assertTrue(result.contains("map.isEmpty()")); + + // Check content method hello + assertTrue(result.contains("Comment block inside method")); + assertTrue(result.contains("Simple comment inside method")); + assertTrue(result.contains("Simple comment inside method (2nd line)")); + assertTrue(result.contains("return \"Hello\";")); + + // Check private method declaration + assertTrue(result.contains(" Map")); + assertTrue(result.contains("privateMethod")); + assertTrue(result.contains("@ParameterAnnotation(\"xXX\")")); + assertTrue(result.contains("Second method comment")); + + // Check subclass + assertTrue(result.contains("* SubClass JavaDoc")); + assertTrue(result.contains("private static class SubClass")); + assertTrue(result.contains("String string1;")); + assertTrue(result.contains("final theNumbers enumValue = dos;")); + assertTrue(result.contains("int aInteger = 2;")); + assertTrue(result.contains("long aLong = 10l;")); + assertTrue(result.contains("SubClass(theNumbers enumValue)")); + assertTrue(result.contains("super();")); + assertTrue(result.contains("// if comment")); + assertTrue(result.contains("if (enumValue.equals(this.enumValue))")); + assertTrue(result.contains("// comment 'if' true")); + assertTrue(result.contains("string1 = \"equals\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("// comment inline 'if' true")); + assertTrue(result + .contains("else if (theNumbers.tres.equals(enumValue))")); + assertTrue(result.contains("// comment 'elseif' true")); + assertTrue(result.contains("string1 = \"elseif\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("// comment inline elseif true")); + assertTrue(result.contains("* comment in 'else'")); + assertTrue(result.contains("string1 = \"else\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("* comment inline else")); + + // Check newList method + assertTrue(result + .contains("List>>> newList(List>> theList)")); + assertTrue(result + .contains("List>>> newListResult = new ArrayList>>>();")); + assertTrue(result.contains("newListResult.add(theList);")); + assertTrue(result.contains("return newListResult;")); + + } + + public static void checkSimple2Class(final String result) { + // Check headers and import + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("import java.util.*;")); + + // Check class declaration + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public class SimpleClass2")); + assertTrue(result.contains("extends OtheClass")); + assertTrue(result.contains("implements SimpleInterface")); + + // Check newList method + assertTrue(result + .contains("List>>> newList(List>> theList)")); + assertTrue(result + .contains("List>>> newListResult = new ArrayList>>>();")); + assertTrue(result.contains("newListResult.add(theList);")); + assertTrue(result.contains("return newListResult;")); + + // Check listStatic method + assertTrue(result.contains("* Javadoc listStatic...")); + assertTrue(result + .contains("public static List listStatic(List objects)")); + assertTrue(result + .contains("List result = new ArrayList();")); + assertTrue(result.contains("for (Object[] object : objects) {")); + assertTrue(result.contains("// ...")); + assertTrue(result.contains("}")); + assertTrue(result.contains("return result;")); + } + + public static void checkSimple3Class(final String result) { + // check headers and import + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + assertTrue(result.contains("public class SimpleClass3")); + + // Check int + assertTrue(result.contains("int mInteger = 0;")); + assertTrue(result.contains("int[] mIntegerArray;")); + assertTrue(result.contains("int[][] mIntegerArray2;")); + assertTrue(result.contains("int[][][] mIntegerArray3;")); + + // Check byte + assertTrue(result.contains("byte mByte;")); + assertTrue(result.contains("byte[] mByteArray;")); + assertTrue(result.contains("byte[][] mByteArray2;")); + assertTrue(result.contains("byte[][][] mByteArray3;")); + + // Check Long + assertTrue(result.contains("Long mLongObject;")); + assertTrue(result.contains("Long[] mLongObjectArray;")); + assertTrue(result.contains("Long[][] mLongObjectArray2;")); + assertTrue(result.contains("Long[][][] mLongObjectArray3;")); + + // Check Set of Strings + assertTrue(result.contains("Set mSetString;")); + assertTrue(result.contains("Set[] mSetStringArray;")); + assertTrue(result.contains("Set[][] mSetStringArray2;")); + assertTrue(result.contains("Set[][][] mSetStringArray3;")); + + // Check Map of Strings and Doubles + assertTrue(result.contains("Map mMapStringDouble;")); + assertTrue(result + .contains("Map[] mMapStringDoubleArray;")); + assertTrue(result + .contains("Map[][] mMapStringDoubleArray2;")); + assertTrue(result + .contains("Map[][][] mMapStringDoubleArray3;")); + + // Check Map of Strings and Doubles + assertTrue(result + .contains("List>> mListMapStringIteratorDouble;")); + assertTrue(result + .contains("List>>[] mListMapStringIteratorDoubleArray;")); + assertTrue(result + .contains("List>>[][] mListMapStringIteratorDoubleArray2;")); + assertTrue(result + .contains("List>>[][][] mListMapStringIteratorDoubleArray3;")); + } + + public static void check_ROO_1505_Class(final String result) { + // Check package + assertTrue(result.contains("package com.pet;")); + + // Check imports + assertTrue(result.contains("import javax.persistence.Entity;")); + assertTrue(result + .contains("import org.springframework.roo.addon.javabean.RooJavaBean;")); + assertTrue(result + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result + .contains("import org.springframework.roo.addon.entity.RooEntity;")); + assertTrue(result + .contains("import javax.validation.constraints.NotNull;")); + assertTrue(result.contains("import java.util.Set;")); + assertTrue(result.contains("import javax.persistence.OneToMany;")); + assertTrue(result.contains("import javax.persistence.CascadeType;")); + + assertTrue(result.contains("@Entity")); + assertTrue(result.contains("@RooJavaBean")); + assertTrue(result.contains("@RooToString")); + assertTrue(result.contains("@RooEntity")); + assertTrue(result.contains("public class Roo_1505 {")); + assertTrue(result.contains("@NotNull")); + assertTrue(result.contains("private String name;")); + + assertTrue(result + .contains("@OneToMany(cascade = CascadeType.ALL, mappedBy = \"owner\")")); + assertTrue(result.contains("private Set pets = new HashSet();")); + } + + public static void checkSimpleInterface(final String result, + final boolean testComments) { + if (testComments) { + assertTrue(result.contains("* File header")); + } + assertTrue(result.contains("package org.myPackage;")); + if (testComments) { + assertTrue(result.contains("// Simple comment1")); + } + assertTrue(result.contains("import test.importtest.pkg;")); + // assertTrue(result.contains("Comment about import")); + assertTrue(result + .contains("import static test.importtest.pkg.Hola.proofMethod;")); + if (testComments) { + assertTrue(result.contains("* @author DiSiD Technologies")); + } + assertTrue(result.contains("public interface SimpleInterface")); + assertTrue(result + .contains("extends Comparable, Iterable")); + if (testComments) { + assertTrue(result.contains("* Javadoc of hello method")); + } + assertTrue(result.contains("@Deprecated")); + assertTrue(result.contains("String hello(String value);")); + } + + public static void checkEnum(final String result) { + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("public enum AEnumerate")); + assertTrue(result.contains("MALE, FEMALE")); + } + + private File getResource(final String pathname) { + final URL res = this.getClass().getClassLoader().getResource(pathname); + return new File(res.getPath()); + } + + private String getResourceContents(final File file) throws IOException { + return FileUtils.readFileToString(file); + } + + private void saveResult(final File orgininalFile, final String result, + String suffix) throws IOException { + if (suffix == null) { + suffix = ".update.result"; + } + else { + suffix = ".update" + suffix + ".result"; + } + final File resultFile = new File(orgininalFile.getParentFile(), + FilenameUtils.getName(orgininalFile.getName()) + suffix); + FileUtils.write(resultFile, result); + } + + private void saveResult(final File orgininalFile, final String result) + throws IOException { + saveResult(orgininalFile, result, null); + } + +} diff --git a/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/UpdateCompilationUnitTest.java b/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/UpdateCompilationUnitTest.java new file mode 100644 index 000000000..37c91a70e --- /dev/null +++ b/classpath-antlrjavaparser/src/test/java/org/springframework/roo/classpath/antlrjavaparser/UpdateCompilationUnitTest.java @@ -0,0 +1,628 @@ +package org.springframework.roo.classpath.antlrjavaparser; + +import static org.junit.Assert.assertTrue; +import static org.springframework.roo.model.JdkJavaType.SET; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.operations.Cardinality; +import org.springframework.roo.classpath.operations.jsr303.ReferenceField; +import org.springframework.roo.classpath.operations.jsr303.SetField; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Functional test of + * {@link JavaParserTypeParsingService#getCompilationUnitContents(org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails)} + * + * @author DiSiD Technologies + * @since 1.2.4 + */ +@RunWith(PowerMockRunner.class) +public class UpdateCompilationUnitTest { + + private static final String SIMPLE_INTERFACE_FILE_PATH = "SimpleInterface.java.test"; + private static final String SIMPLE_CLASS_FILE_PATH = "SimpleClass.java.test"; + private static final String SIMPLE_CLASS2_FILE_PATH = "SimpleClass2.java.test"; + private static final String SIMPLE_CLASS3_FILE_PATH = "SimpleClass3.java.test"; + private static final String ROO1505_CLASS_FILE_PATH = "Roo_1505.java.test"; + + private static final JavaType SIMPLE_INTERFACE_TYPE = new JavaType( + "org.myPackage.SimpleInterface"); + private static final JavaType SIMPLE_CLASS_TYPE = new JavaType( + "org.myPackage.SimpleClass"); + private static final JavaType SIMPLE_CLASS2_TYPE = new JavaType( + "org.myPackage.SimpleClass2"); + private static final JavaType SIMPLE_CLASS3_TYPE = new JavaType( + "org.myPackage.SimpleClass3"); + private static final JavaType ROO1505_CLASS_TYPE = new JavaType( + "com.pet.Roo_1505"); + + private static final String SIMPLE_INTERFACE_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#bar?SimpleInterface"; + private static final String SIMPLE_CLASS_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass"; + private static final String SIMPLE_CLASS2_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass2"; + private static final String SIMPLE_CLASS3_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass3"; + private static final String ROO1505_CLASS_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?Roo_1505"; + + @Mock private MetadataService mockMetadataService; + @Mock private TypeLocationService mockTypeLocationService; + + // Fixture + private JavaParserTypeParsingService typeParsingService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + typeParsingService = new JavaParserTypeParsingService(); + typeParsingService.metadataService = mockMetadataService; + typeParsingService.typeLocationService = mockTypeLocationService; + } + + @Test + public void testSimpleInterfaceNoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_INTERFACE_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, + SIMPLE_INTERFACE_DECLARED_BY_MID, SIMPLE_INTERFACE_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimpleInterface(result); + } + + @Test + public void testSimpleClassNoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimpleClass(result); + } + + @Test + public void testSimpleClass2NoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS2_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS2_DECLARED_BY_MID, + SIMPLE_CLASS2_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // save to file for debug + saveResult(file, result); + + checkSimple2Class(result); + } + + @Test + public void testSimpleClass3NoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS3_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimple3Class(result); + } + + @Test + public void testSimpleClass3AddField() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS3_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + final SetField fieldDetails = new SetField( + SIMPLE_CLASS3_DECLARED_BY_MID, new JavaType( + SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(SIMPLE_CLASS3_TYPE)), + new JavaSymbolName("children"), SIMPLE_CLASS3_TYPE, + Cardinality.ONE_TO_MANY); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + fieldDetails.getPhysicalTypeIdentifier(), Modifier.PRIVATE, + new ArrayList(), + fieldDetails.getFieldName(), fieldDetails.getFieldType()); + fieldBuilder.setFieldInitializer("new HashSet()"); + + final ClassOrInterfaceTypeDetails newClassDetails = addField( + simpleInterfaceDetails, fieldBuilder.build()); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(newClassDetails); + + saveResult(file, result, "-addField"); + + checkSimple3Class(result); + + assertTrue(result + .contains("private Set children = new HashSet();")); + + // Add another + final ClassOrInterfaceTypeDetails simpleInterfaceDetails2 = typeParsingService + .getTypeFromString(result, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + final ReferenceField fieldDetails2 = new ReferenceField( + SIMPLE_CLASS3_DECLARED_BY_MID, SIMPLE_CLASS2_TYPE, + new JavaSymbolName("referenceField"), Cardinality.MANY_TO_ONE); + + final FieldMetadataBuilder fieldBuilder2 = new FieldMetadataBuilder( + fieldDetails2.getPhysicalTypeIdentifier(), Modifier.PRIVATE, + new ArrayList(), + fieldDetails2.getFieldName(), fieldDetails2.getFieldType()); + + final ClassOrInterfaceTypeDetails newClassDetails2 = addField( + simpleInterfaceDetails2, fieldBuilder2.build()); + + // Invoke + final String result2 = typeParsingService + .getCompilationUnitContents(newClassDetails2); + + // Save to file for debug + saveResult(file, result2, "-addField2"); + + checkSimple3Class(result2); + + assertTrue(result + .contains("private Set children = new HashSet();")); + assertTrue(result2.contains("private SimpleClass2 referenceField;")); + + } + + @Test + public void testRegresion_ROO_1505() throws Exception { + // Set up + final File file = getResource(ROO1505_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, ROO1505_CLASS_DECLARED_BY_MID, + ROO1505_CLASS_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // save to file for debug + saveResult(file, result); + + check_ROO_1505_Class(result); + } + + @Test + public void testSimpleClassAddField() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + SIMPLE_CLASS_DECLARED_BY_MID, Modifier.PRIVATE, + new JavaSymbolName("newFieldAddedByCode"), new JavaType( + String.class), "\"Create by code\""); + final ClassOrInterfaceTypeDetails newSimpleInterfaceDetails = addField( + simpleInterfaceDetails, fieldBuilder.build()); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(newSimpleInterfaceDetails); + + // save to file for debug + saveResult(file, result, "-addedField"); + + checkSimpleClass(result); + + assertTrue(result + .contains("private String newFieldAddedByCode = \"Create by code\";")); + } + + @Test + public void testSimpleClassAddAnnotation() throws Exception { + + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + new JavaType( + "org.springframework.roo.addon.tostring.RooToString")); + final ClassOrInterfaceTypeDetails newSimpleInterfaceDetails = addAnnotation( + simpleInterfaceDetails, annotationBuilder.build()); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(newSimpleInterfaceDetails); + + // save to file for debug + saveResult(file, result, "-addedAnnotation"); + + checkSimpleClass(result); + + assertTrue(result + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result.contains("@RooToString")); + + // Invoke2 + final ClassOrInterfaceTypeDetails simpleInterfaceDetails2 = typeParsingService + .getTypeFromString(result, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final String result2 = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails2); + + // Save to file for debug + saveResult(file, result2, "-addedAnnotation2"); + + checkSimpleClass(result2); + + assertTrue(result2 + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result2.contains("@RooToString")); + + } + + public static ClassOrInterfaceTypeDetails addField( + final ClassOrInterfaceTypeDetails ptd, final FieldMetadata field) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + ptd); + cidBuilder.addField(field); + return cidBuilder.build(); + } + + public static ClassOrInterfaceTypeDetails addAnnotation( + final ClassOrInterfaceTypeDetails ptd, + final AnnotationMetadata annotation) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + ptd); + cidBuilder.addAnnotation(annotation); + return cidBuilder.build(); + } + + public static void checkSimpleClass(final String result) { + // check headers and import + // assertTrue(result.contains("* File header")); + assertTrue(result.contains("package org.myPackage;")); + // assertTrue(result.contains("// Simple comment1")); + assertTrue(result.contains("import test.importtest.pkg;")); + // assertTrue(result.contains("Comment about import")); + assertTrue(result + .contains("import static test.importtest.pkg.Hola.proofMethod;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public class SimpleClass")); + assertTrue(result.contains("extends OtheClass")); + assertTrue(result.contains("implements SimpleInterface")); + + // assertTrue(result.contains("== Comment in code ==")); + + // Check fields + // assertTrue(result.contains("* Javadoc for field")); + assertTrue(result.contains("private final String[] params;")); + + // assertTrue(result.contains("* Javadoc for column field")); + assertTrue(result.contains("@Column(name = \"VALOR\", length = 500)")); + assertTrue(result.contains("@Size(max = 500)")); + assertTrue(result.contains("@NotNull")); + assertTrue(result.contains("private final String valor;")); + + + // assertTrue(result.contains("* Javadoc for transient field")); + assertTrue(result.contains("@Transient")); + assertTrue(result.contains("protected Double param1 = new Double(12);")); + assertTrue(result + .contains("private List[] listArray = new List[3];")); + assertTrue(result + .contains("Set[] setArray = new Set[] { null, null, null };")); + // assertTrue(result.contains("* Enum javaDoc")); + assertTrue(result.contains("public enum theNumbers")); + assertTrue(result.contains("uno, dos, tres")); + + // Check constructors + assertTrue(result.contains("@Documented(\"aaadbbbdd\")")); + // XXX Not supported Variable Args in Spring Rooo 'public + // SimpleClass(Double param1, String + // params)' + // assertTrue(result.contains("public SimpleClass(Double param1, String... params)")); + assertTrue(result + .contains("public SimpleClass(Double param1, String[] params, Set[] setArrayParam)")); + // assertTrue(result.contains("* Public constructor")); + // assertTrue(result.contains("// Comment in public constructor")); + assertTrue(result.contains("this.param1 = param1;")); + assertTrue(result.contains("this.params = params;")); + // assertTrue(result.contains("* Private constructor")); + assertTrue(result.contains("private SimpleClass()")); + // assertTrue(result.contains("* Comment in private constructor")); + assertTrue(result.contains("this.param1 = null;")); + // assertTrue(result.contains("// Comment inline1")); + // assertTrue(result.contains("// other comment between inline")); + assertTrue(result.contains("this.params = null;")); + // XXX JavaParser Bug + // assertTrue(result.contains("// Comment inline2")); + + // Check method hello declaration + // assertTrue(result.contains("* Javadoc of hello method")); + assertTrue(result + .contains("@Deprecated(message = \"Do not use\", more = \"Nothing\")")); + assertTrue(result.contains("@Override")); + assertTrue(result.contains("public Sting hello(String value)")); + + // Check method methodVoid + // assertTrue(result.contains("* methodVoid JavaDoc")); + assertTrue(result + .contains("void methodVoid(Double param1, String[] params, Map[] mapArrayParam)")); + // assertTrue(result.contains("// Comment before for")); + assertTrue(result + .contains("for (Map map : mapArrayParam)")); + // assertTrue(result.contains("// comment inside for")); + assertTrue(result.contains("map.isEmpty()")); + + // Check content method hello + // assertTrue(result.contains("Comment block inside method")); + // assertTrue(result.contains("Simple comment inside method")); + // assertTrue(result.contains("Simple comment inside method (2nd line)")); + assertTrue(result.contains("return \"Hello\";")); + + // Check private method declaration + // XXX Roo metadata doesn't support this + // assertTrue(result.contains(" Map")); + assertTrue(result.contains("Map")); + assertTrue(result.contains("privateMethod")); + assertTrue(result.contains("@ParameterAnnotation(\"xXX\")")); + // assertTrue(result.contains("Second method comment")); + + // Check subclass + // assertTrue(result.contains("* SubClass JavaDoc")); + assertTrue(result.contains("private static class SubClass")); + assertTrue(result.contains("String string1;")); + assertTrue(result.contains("final theNumbers enumValue = dos;")); + assertTrue(result.contains("int aInteger = 2;")); + assertTrue(result.contains("long aLong = 10l;")); + assertTrue(result.contains("SubClass(theNumbers enumValue)")); + assertTrue(result.contains("super();")); + // assertTrue(result.contains("// if comment")); + assertTrue(result.contains("if (enumValue.equals(this.enumValue))")); + // assertTrue(result.contains("// comment 'if' true")); + assertTrue(result.contains("string1 = \"equals\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("// comment inline 'if' true")); + assertTrue(result + .contains("else if (theNumbers.tres.equals(enumValue))")); + // assertTrue(result.contains("// comment 'elseif' true")); + assertTrue(result.contains("string1 = \"elseif\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("// comment inline elseif true")); + // assertTrue(result.contains("* comment in 'else'")); + assertTrue(result.contains("string1 = \"else\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("* comment inline else")); + + // Check newList method + assertTrue(result + .contains("List>>> newList(List>> theList)")); + assertTrue(result + .contains("List>>> newListResult = new ArrayList>>>();")); + assertTrue(result.contains("newListResult.add(theList);")); + assertTrue(result.contains("return newListResult;")); + + } + + public static void checkSimple2Class(final String result) { + // check headers and import + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public class SimpleClass2")); + assertTrue(result.contains("extends OtheClass")); + assertTrue(result.contains("implements SimpleInterface")); + + // Check newList method + assertTrue(result + .contains("List>>> newList(List>> theList)")); + assertTrue(result + .contains("List>>> newListResult = new ArrayList>>>();")); + assertTrue(result.contains("newListResult.add(theList);")); + assertTrue(result.contains("return newListResult;")); + } + + public static void checkSimple3Class(final String result) { + // check headers and import + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + assertTrue(result.contains("public class SimpleClass3")); + + // Check int + assertTrue(result.contains("int mInteger = 0;")); + assertTrue(result.contains("int[] mIntegerArray;")); + assertTrue(result.contains("int[][] mIntegerArray2;")); + assertTrue(result.contains("int[][][] mIntegerArray3;")); + + // Check byte + assertTrue(result.contains("byte mByte;")); + assertTrue(result.contains("byte[] mByteArray;")); + assertTrue(result.contains("byte[][] mByteArray2;")); + assertTrue(result.contains("byte[][][] mByteArray3;")); + + // Check Long + assertTrue(result.contains("Long mLongObject;")); + assertTrue(result.contains("Long[] mLongObjectArray;")); + assertTrue(result.contains("Long[][] mLongObjectArray2;")); + assertTrue(result.contains("Long[][][] mLongObjectArray3;")); + + // Check Set of Strings + assertTrue(result.contains("Set mSetString;")); + assertTrue(result.contains("Set[] mSetStringArray;")); + assertTrue(result.contains("Set[][] mSetStringArray2;")); + assertTrue(result.contains("Set[][][] mSetStringArray3;")); + + // Check Map of Strings and Doubles + assertTrue(result.contains("Map mMapStringDouble;")); + assertTrue(result + .contains("Map[] mMapStringDoubleArray;")); + assertTrue(result + .contains("Map[][] mMapStringDoubleArray2;")); + assertTrue(result + .contains("Map[][][] mMapStringDoubleArray3;")); + + // Check Map of Strings and Doubles + assertTrue(result + .contains("List>> mListMapStringIteratorDouble;")); + assertTrue(result + .contains("List>>[] mListMapStringIteratorDoubleArray;")); + assertTrue(result + .contains("List>>[][] mListMapStringIteratorDoubleArray2;")); + assertTrue(result + .contains("List>>[][][] mListMapStringIteratorDoubleArray3;")); + } + + public static void check_ROO_1505_Class(final String result) { + // Check package + assertTrue(result.contains("package com.pet;")); + + // Check imports + assertTrue(result.contains("import javax.persistence.Entity;")); + assertTrue(result + .contains("import org.springframework.roo.addon.javabean.RooJavaBean;")); + assertTrue(result + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result + .contains("import org.springframework.roo.addon.entity.RooEntity;")); + assertTrue(result + .contains("import javax.validation.constraints.NotNull;")); + assertTrue(result.contains("import java.util.Set;")); + assertTrue(result.contains("import javax.persistence.OneToMany;")); + assertTrue(result.contains("import javax.persistence.CascadeType;")); + + assertTrue(result.contains("@Entity")); + assertTrue(result.contains("@RooJavaBean")); + assertTrue(result.contains("@RooToString")); + assertTrue(result.contains("@RooEntity")); + assertTrue(result.contains("public class Roo_1505 {")); + assertTrue(result.contains("@NotNull")); + assertTrue(result.contains("private String name;")); + + assertTrue(result + .contains("@OneToMany(cascade = CascadeType.ALL, mappedBy = \"owner\")")); + assertTrue(result.contains("private Set pets = new HashSet();")); + + } + + public static void checkSimpleInterface(final String result) { + // assertTrue(result.contains("* File header")); + assertTrue(result.contains("package org.myPackage;")); + // assertTrue(result.contains("// Simple comment1")); + assertTrue(result.contains("import test.importtest.pkg;")); + // assertTrue(result.contains("Comment about import")); + assertTrue(result + .contains("import static test.importtest.pkg.Hola.proofMethod;")); + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public interface SimpleInterface")); + assertTrue(result + .contains("extends Comparable, Iterable")); + + // assertTrue(result.contains("* Javadoc of hello method")); + assertTrue(result.contains("@Deprecated")); + assertTrue(result.contains("String hello(String value);")); + } + + private File getResource(final String pathname) { + final URL res = this.getClass().getClassLoader().getResource(pathname); + return new File(res.getPath()); + } + + private String getResourceContents(final File file) throws IOException { + return FileUtils.readFileToString(file); + } + + private void saveResult(final File orgininalFile, final String result, + String suffix) throws IOException { + if (suffix == null) { + suffix = ".update.result"; + } + else { + suffix = ".update" + suffix + ".result"; + } + final File resultFile = new File(orgininalFile.getParentFile(), + FilenameUtils.getName(orgininalFile.getName()) + suffix); + FileUtils.write(resultFile, result); + } + + private void saveResult(final File orgininalFile, final String result) + throws IOException { + saveResult(orgininalFile, result, null); + } + +} diff --git a/classpath-antlrjavaparser/src/test/resources/AEnumerate.java.test b/classpath-antlrjavaparser/src/test/resources/AEnumerate.java.test new file mode 100644 index 000000000..f53c6fcfa --- /dev/null +++ b/classpath-antlrjavaparser/src/test/resources/AEnumerate.java.test @@ -0,0 +1,7 @@ +package org.myPackage; + + +public enum AEnumerate { + + MALE, FEMALE; +} diff --git a/classpath-antlrjavaparser/src/test/resources/Roo_1505.java.test b/classpath-antlrjavaparser/src/test/resources/Roo_1505.java.test new file mode 100644 index 000000000..506543ac6 --- /dev/null +++ b/classpath-antlrjavaparser/src/test/resources/Roo_1505.java.test @@ -0,0 +1,23 @@ +package com.pet; + +import javax.persistence.Entity; +import org.springframework.roo.addon.javabean.RooJavaBean; +import org.springframework.roo.addon.tostring.RooToString; +import org.springframework.roo.addon.entity.RooEntity; +import javax.validation.constraints.NotNull; +import java.util.Set; +import javax.persistence.OneToMany; +import javax.persistence.CascadeType; + +@Entity +@RooJavaBean +@RooToString +@RooEntity +public class Roo_1505 { + + @NotNull + private String name; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") + private Set pets = new HashSet(); +} \ No newline at end of file diff --git a/classpath-antlrjavaparser/src/test/resources/SimpleClass.java.test b/classpath-antlrjavaparser/src/test/resources/SimpleClass.java.test new file mode 100644 index 000000000..f7fc77d3f --- /dev/null +++ b/classpath-antlrjavaparser/src/test/resources/SimpleClass.java.test @@ -0,0 +1,144 @@ +/** + * File header + */ +package org.myPackage; + +// Simple comment1 + +import java.lang.annotation.Documented; + +import test.importtest.pkg; + +/** + * Comment about import + */ +import static test.importtest.pkg.Hola.proofMethod; +import java.util.*; + +/** + * @author DiSiD Technologies + */ +public class SimpleClass extends OtheClass implements SimpleInterface{ + + /* + * =========== Comment in code ================ + */ + + /** + * Javadoc for field + */ + private final String[] params; + + + /** + * Javadoc for column field + */ + @Column(name = "VALOR", length = 500) + @Size(max = 500) + @NotNull + private final String valor; + + /** + * Javadoc for transient field + */ + @Transient + protected Double param1 = new Double(12); + private List[] listArray = new List[3]; + Set[] setArray = new Set[] {null, null, null}; + + /** + * Enum javaDoc + */ + public enum theNumbers {uno, dos, tres}; + + /** + * Public constructor + */ + @Documented("aaadbbbdd") + // Not supported varing arguments public SimpleClass(Double param1, String...params) { + public SimpleClass(Double param1, String[] params, Set[] setArrayParam) { + // Comment in public constructor + this.param1 = param1; + this.params = params; + } + + /** + * methodVoid JavaDoc + */ + void methodVoid(Double param1, String[] params, Map[] mapArrayParam){ + // Comment before for + for (Map map : mapArrayParam){ + // comment inside for + map.isEmpty(); + } + } + + /** + * Private constructor + */ + private SimpleClass() { + /* + * Comment in private constructor + */ + this.param1 = null; // Comment inline1 + // other comment between inline + this.params = null; // Comment inline2 + } + + /** + * Javadoc of hello method + * + * @param value + * @return + */ + @Deprecated(message="Do not use",more="Nothing") + @Override + public Sting hello(String value){ + /* + * Comment block inside method + */ + + // Simple comment inside method + // Simple comment inside method (2nd line) + + + return "Hello"; + } + + private Map privateMethod(@ParameterAnnotation("xXX")T tValue, X xValue){ + // Second method comment + return null; + } + + /** + * SubClass JavaDoc + */ + private static class SubClass { + String string1; + final theNumbers enumValue = dos; + int aInteger = 2; + long aLong = 10l; + + SubClass(theNumbers enumValue) { + super(); + // if comment + if (enumValue.equals(this.enumValue)) { + // comment 'if' true + string1 = "equals"; // comment inline 'if' true + } else if (theNumbers.tres.equals(enumValue)){ + // comment 'elseif' true + string1 = "elseif"; // comment inline elseif true + } else { + /* comment in 'else' */ + string1 = "else"; /* comment inline else*/ + } + } + + } + + List>>> newList(List>> theList){ + List>>> newListResult = new ArrayList>>>(); + newListResult.add(theList); + return newListResult; + } +} diff --git a/classpath-antlrjavaparser/src/test/resources/SimpleClass2.java.test b/classpath-antlrjavaparser/src/test/resources/SimpleClass2.java.test new file mode 100644 index 000000000..c46bea41c --- /dev/null +++ b/classpath-antlrjavaparser/src/test/resources/SimpleClass2.java.test @@ -0,0 +1,25 @@ +package org.myPackage; + +import java.util.*; + +public class SimpleClass2 extends OtheClass implements SimpleInterface{ + + List>>> newList(List>> theList){ + List>>> newListResult = new ArrayList>>>(); + newListResult.add(theList); + return newListResult; + } + + /** + * + * Javadoc listStatic... + * + */ + public static List listStatic(List objects) { + List result = new ArrayList(); + for (Object[] object : objects) { + // ... + } + return result; + } +} diff --git a/classpath-antlrjavaparser/src/test/resources/SimpleClass3.java.test b/classpath-antlrjavaparser/src/test/resources/SimpleClass3.java.test new file mode 100644 index 000000000..48905b2cc --- /dev/null +++ b/classpath-antlrjavaparser/src/test/resources/SimpleClass3.java.test @@ -0,0 +1,36 @@ +package org.myPackage; + +import java.util.*; + +public class SimpleClass3{ + + int mInteger = 0; + int[] mIntegerArray; + int[][] mIntegerArray2; + int[][][] mIntegerArray3; + + byte mByte; + byte mByteArray[]; + byte mByteArray2[][]; + byte mByteArray3[][][]; + + Long mLongObject; + Long[] mLongObjectArray; + Long[][] mLongObjectArray2; + Long[][][] mLongObjectArray3; + + Set mSetString; + Set[] mSetStringArray; + Set[][] mSetStringArray2; + Set[][][] mSetStringArray3; + + Map mMapStringDouble; + Map[] mMapStringDoubleArray; + Map[][] mMapStringDoubleArray2; + Map[][][] mMapStringDoubleArray3; + + List>> mListMapStringIteratorDouble; + List>>[] mListMapStringIteratorDoubleArray; + List>>[][] mListMapStringIteratorDoubleArray2; + List>>[][][] mListMapStringIteratorDoubleArray3; +} diff --git a/classpath-antlrjavaparser/src/test/resources/SimpleInterface.java.test b/classpath-antlrjavaparser/src/test/resources/SimpleInterface.java.test new file mode 100644 index 000000000..e25b1b250 --- /dev/null +++ b/classpath-antlrjavaparser/src/test/resources/SimpleInterface.java.test @@ -0,0 +1,27 @@ +/** + * File header + */ +package org.myPackage; + +// Simple comment1 +import test.importtest.pkg; + +/** + * Coment about import + */ +import static test.importtest.pkg.Hola.proofMethod; + +/** + * @author DiSiD Technologies + */ +public interface SimpleInterface extends Comparable, Iterable { + + /** + * Javadoc of hello method + * + * @param value + * @return + */ + @Deprecated + String hello(String value); +} diff --git a/classpath-javaparser/legal-classpath-javaparser.txt b/classpath-javaparser/legal-classpath-javaparser.txt new file mode 100644 index 000000000..b556dbf0b --- /dev/null +++ b/classpath-javaparser/legal-classpath-javaparser.txt @@ -0,0 +1,19 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: JavaParser +Software Web Site: http://code.google.com/p/javaparser/ +Effective License: GNU Lesser General Public License +License Info Page: http://www.gnu.org/licenses/lgpl.html + +JavaParser is a runtime dependency of this module. It provides an +abstract syntax tree (AST) model for parsing Java source code. + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/classpath-javaparser/pom.xml b/classpath-javaparser/pom.xml new file mode 100644 index 000000000..0ac8a6e12 --- /dev/null +++ b/classpath-javaparser/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.classpath.javaparser + bundle + Spring Roo - Classpath (JavaParser Implementation) + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.classpath + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.javaparser + + + \ No newline at end of file diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/CompilationUnitServices.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/CompilationUnitServices.java new file mode 100644 index 000000000..911f89779 --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/CompilationUnitServices.java @@ -0,0 +1,38 @@ +package org.springframework.roo.classpath.javaparser; + +import japa.parser.ast.ImportDeclaration; +import japa.parser.ast.body.TypeDeclaration; + +import java.util.List; + +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * An interface that enables Java Parser types to query relevant information + * about a compilation unit. + * + * @author Ben Alex + * @author James Tyrrell + * @since 1.0 + */ +public interface CompilationUnitServices { + + JavaPackage getCompilationUnitPackage(); + + /** + * @return the enclosing type (never null) + */ + JavaType getEnclosingTypeName(); + + List getImports(); + + /** + * @return the names of each inner type and the enclosing type (never null + * but may be empty) + */ + List getInnerTypes(); + + PhysicalTypeCategory getPhysicalTypeCategory(); +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserTypeParsingService.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserTypeParsingService.java new file mode 100644 index 000000000..8ee17e12e --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserTypeParsingService.java @@ -0,0 +1,450 @@ +package org.springframework.roo.classpath.javaparser; + +import static org.springframework.roo.model.JavaType.OBJECT; +import japa.parser.ASTHelper; +import japa.parser.JavaParser; +import japa.parser.ParseException; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.ImportDeclaration; +import japa.parser.ast.PackageDeclaration; +import japa.parser.ast.TypeParameter; +import japa.parser.ast.body.BodyDeclaration; +import japa.parser.ast.body.ClassOrInterfaceDeclaration; +import japa.parser.ast.body.EnumConstantDeclaration; +import japa.parser.ast.body.EnumDeclaration; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.expr.NameExpr; +import japa.parser.ast.expr.QualifiedNameExpr; +import japa.parser.ast.type.ClassOrInterfaceType; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeParsingService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ImportMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.javaparser.details.JavaParserAnnotationMetadataBuilder; +import org.springframework.roo.classpath.javaparser.details.JavaParserClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.javaparser.details.JavaParserConstructorMetadataBuilder; +import org.springframework.roo.classpath.javaparser.details.JavaParserFieldMetadataBuilder; +import org.springframework.roo.classpath.javaparser.details.JavaParserMethodMetadataBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +@Component +@Service +public class JavaParserTypeParsingService implements TypeParsingService { + + @Reference MetadataService metadataService; + @Reference TypeLocationService typeLocationService; + + private void addEnumConstant(final List constants, + final JavaSymbolName name) { + // Determine location to insert + for (final EnumConstantDeclaration constant : constants) { + if (constant.getName().equals(name.getSymbolName())) { + throw new IllegalArgumentException("Enum constant '" + + name.getSymbolName() + "' already exists"); + } + } + final EnumConstantDeclaration newEntry = new EnumConstantDeclaration( + name.getSymbolName()); + constants.add(constants.size(), newEntry); + } + + public final String getCompilationUnitContents( + final ClassOrInterfaceTypeDetails cid) { + Validate.notNull(cid, "Class or interface type details are required"); + // Create a compilation unit to store the type to be created + final CompilationUnit compilationUnit = new CompilationUnit(); + + // NB: this import list is replaced at the end of this method by a + // sorted version + compilationUnit.setImports(new ArrayList()); + + if (!cid.getName().isDefaultPackage()) { + compilationUnit.setPackage(new PackageDeclaration(ASTHelper + .createNameExpr(cid.getName().getPackage() + .getFullyQualifiedPackageName()))); + } + + // Add the class of interface declaration to the compilation unit + final List types = new ArrayList(); + compilationUnit.setTypes(types); + + updateOutput(compilationUnit, null, cid, null); + + return compilationUnit.toString(); + } + + public ClassOrInterfaceTypeDetails getTypeAtLocation( + final String fileIdentifier, final String declaredByMetadataId, + final JavaType typeName) { + Validate.notBlank(fileIdentifier, "Compilation unit path required"); + Validate.notBlank(declaredByMetadataId, + "Declaring metadata ID required"); + Validate.notNull(typeName, "Java type to locate required"); + final File file = new File(fileIdentifier); + String typeContents = ""; + try { + typeContents = FileUtils.readFileToString(file); + } + catch (IOException ignored) { + } + if (StringUtils.isBlank(typeContents)) { + return null; + } + return getTypeFromString(typeContents, declaredByMetadataId, typeName); + } + + public ClassOrInterfaceTypeDetails getTypeFromString( + final String fileContents, final String declaredByMetadataId, + final JavaType typeName) { + if (StringUtils.isBlank(fileContents)) { + return null; + } + + Validate.notBlank(declaredByMetadataId, + "Declaring metadata ID required"); + Validate.notNull(typeName, "Java type to locate required"); + try { + final CompilationUnit compilationUnit = JavaParser + .parse(new ByteArrayInputStream(fileContents.getBytes())); + final TypeDeclaration typeDeclaration = JavaParserUtils + .locateTypeDeclaration(compilationUnit, typeName); + if (typeDeclaration == null) { + return null; + } + return JavaParserClassOrInterfaceTypeDetailsBuilder.getInstance( + compilationUnit, null, typeDeclaration, + declaredByMetadataId, typeName, metadataService, + typeLocationService).build(); + } + catch (final ParseException e) { + throw new IllegalStateException("Failed to parse " + typeName + + " : " + e.getMessage()); + } + } + + /** + * Appends the presented class to the end of the presented body + * declarations. The body declarations appear within the presented + * compilation unit. This is used to progressively build inner types. + * + * @param compilationUnit the work-in-progress compilation unit (required) + * @param enclosingCompilationUnitServices + * @param cid the new class to add (required) + * @param parent the class body declarations a subclass should be added to + * (may be null, which denotes a top-level type within the + * compilation unit) + */ + private void updateOutput(final CompilationUnit compilationUnit, + CompilationUnitServices enclosingCompilationUnitServices, + final ClassOrInterfaceTypeDetails cid, + final List parent) { + // Append the new imports this class declares + Validate.notNull(compilationUnit.getImports(), + "Compilation unit imports should be non-null when producing type '" + + cid.getName() + "'"); + for (final ImportMetadata importType : cid.getRegisteredImports()) { + if (!importType.isAsterisk()) { + NameExpr typeToImportExpr; + if (importType.getImportType().getEnclosingType() == null) { + typeToImportExpr = new QualifiedNameExpr(new NameExpr( + importType.getImportType().getPackage() + .getFullyQualifiedPackageName()), + importType.getImportType().getSimpleTypeName()); + } + else { + typeToImportExpr = new QualifiedNameExpr(new NameExpr( + importType.getImportType().getEnclosingType() + .getFullyQualifiedTypeName()), importType + .getImportType().getSimpleTypeName()); + } + compilationUnit.getImports().add( + new ImportDeclaration(typeToImportExpr, importType + .isStatic(), false)); + } + else { + compilationUnit.getImports().add( + new ImportDeclaration(new NameExpr(importType + .getImportPackage() + .getFullyQualifiedPackageName()), importType + .isStatic(), importType.isAsterisk())); + } + } + + // Create a class or interface declaration to represent this actual type + final int javaParserModifier = JavaParserUtils + .getJavaParserModifier(cid.getModifier()); + TypeDeclaration typeDeclaration; + ClassOrInterfaceDeclaration classOrInterfaceDeclaration; + + // Implements handling + final List implementsList = new ArrayList(); + for (final JavaType current : cid.getImplementsTypes()) { + implementsList.add(JavaParserUtils.getResolvedName(cid.getName(), + current, compilationUnit)); + } + + if (cid.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE + || cid.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS) { + final boolean isInterface = cid.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE; + + if (parent == null) { + // Top level type + typeDeclaration = new ClassOrInterfaceDeclaration( + javaParserModifier, isInterface, cid + .getName() + .getNameIncludingTypeParameters() + .replace( + cid.getName().getPackage() + .getFullyQualifiedPackageName() + + ".", "")); + classOrInterfaceDeclaration = (ClassOrInterfaceDeclaration) typeDeclaration; + } + else { + // Inner type + typeDeclaration = new ClassOrInterfaceDeclaration( + javaParserModifier, isInterface, cid.getName() + .getSimpleTypeName()); + classOrInterfaceDeclaration = (ClassOrInterfaceDeclaration) typeDeclaration; + + if (cid.getName().getParameters().size() > 0) { + classOrInterfaceDeclaration + .setTypeParameters(new ArrayList()); + + for (final JavaType param : cid.getName().getParameters()) { + NameExpr pNameExpr = JavaParserUtils + .importTypeIfRequired(cid.getName(), + compilationUnit.getImports(), param); + final String tempName = StringUtils.replace( + pNameExpr.toString(), param.getArgName() + + " extends ", "", 1); + pNameExpr = new NameExpr(tempName); + final ClassOrInterfaceType pResolvedName = JavaParserUtils + .getClassOrInterfaceType(pNameExpr); + classOrInterfaceDeclaration.getTypeParameters().add( + new TypeParameter(param.getArgName() + .getSymbolName(), Collections + .singletonList(pResolvedName))); + } + } + } + + // Superclass handling + final List extendsList = new ArrayList(); + for (final JavaType current : cid.getExtendsTypes()) { + if (!OBJECT.equals(current)) { + extendsList.add(JavaParserUtils.getResolvedName( + cid.getName(), current, compilationUnit)); + } + } + if (extendsList.size() > 0) { + classOrInterfaceDeclaration.setExtends(extendsList); + } + + // Implements handling + if (implementsList.size() > 0) { + classOrInterfaceDeclaration.setImplements(implementsList); + } + } + else { + typeDeclaration = new EnumDeclaration(javaParserModifier, cid + .getName().getSimpleTypeName()); + } + typeDeclaration.setMembers(new ArrayList()); + + Validate.notNull(typeDeclaration.getName(), + "Missing type declaration name for '" + cid.getName() + "'"); + + // If adding a new top-level type, must add it to the compilation unit + // types + Validate.notNull(compilationUnit.getTypes(), + "Compilation unit types must not be null when attempting to add '" + + cid.getName() + "'"); + + if (parent == null) { + // Top-level class + compilationUnit.getTypes().add(typeDeclaration); + } + else { + // Inner class + parent.add(typeDeclaration); + } + + // If the enclosing CompilationUnitServices was not provided a default + // CompilationUnitServices needs to be created + if (enclosingCompilationUnitServices == null) { + // Create a compilation unit so that we can use JavaType*Metadata + // static methods directly + enclosingCompilationUnitServices = new CompilationUnitServices() { + public JavaPackage getCompilationUnitPackage() { + return cid.getName().getPackage(); + } + + public JavaType getEnclosingTypeName() { + return cid.getName(); + } + + public List getImports() { + return compilationUnit.getImports(); + } + + public List getInnerTypes() { + return compilationUnit.getTypes(); + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return cid.getPhysicalTypeCategory(); + } + }; + } + + final CompilationUnitServices finalCompilationUnitServices = enclosingCompilationUnitServices; + // A hybrid CompilationUnitServices must be provided that references the + // enclosing types imports and package + final CompilationUnitServices compilationUnitServices = new CompilationUnitServices() { + public JavaPackage getCompilationUnitPackage() { + return finalCompilationUnitServices.getCompilationUnitPackage(); + } + + public JavaType getEnclosingTypeName() { + return cid.getName(); + } + + public List getImports() { + return finalCompilationUnitServices.getImports(); + } + + public List getInnerTypes() { + return compilationUnit.getTypes(); + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return cid.getPhysicalTypeCategory(); + } + }; + + // Add type annotations + final List annotations = new ArrayList(); + typeDeclaration.setAnnotations(annotations); + for (final AnnotationMetadata candidate : cid.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, candidate); + } + + // Add enum constants and interfaces + if (typeDeclaration instanceof EnumDeclaration + && cid.getEnumConstants().size() > 0) { + final EnumDeclaration enumDeclaration = (EnumDeclaration) typeDeclaration; + + final List constants = new ArrayList(); + enumDeclaration.setEntries(constants); + + for (final JavaSymbolName constant : cid.getEnumConstants()) { + addEnumConstant(constants, constant); + } + + // Implements handling + if (implementsList.size() > 0) { + enumDeclaration.setImplements(implementsList); + } + } + + // Add fields + for (final FieldMetadata candidate : cid.getDeclaredFields()) { + JavaParserFieldMetadataBuilder.addField(compilationUnitServices, + typeDeclaration.getMembers(), candidate); + } + + // Add constructors + for (final ConstructorMetadata candidate : cid + .getDeclaredConstructors()) { + JavaParserConstructorMetadataBuilder.addConstructor( + compilationUnitServices, typeDeclaration.getMembers(), + candidate, null); + } + + // Add methods + for (final MethodMetadata candidate : cid.getDeclaredMethods()) { + JavaParserMethodMetadataBuilder.addMethod(compilationUnitServices, + typeDeclaration.getMembers(), candidate, null); + } + + // Add inner types + for (final ClassOrInterfaceTypeDetails candidate : cid + .getDeclaredInnerTypes()) { + updateOutput(compilationUnit, compilationUnitServices, candidate, + typeDeclaration.getMembers()); + } + + final HashSet imported = new HashSet(); + final ArrayList imports = new ArrayList(); + for (final ImportDeclaration importDeclaration : compilationUnit + .getImports()) { + JavaPackage importPackage = null; + JavaType importType = null; + if (importDeclaration.isAsterisk()) { + importPackage = new JavaPackage(importDeclaration.getName() + .toString()); + } + else { + importType = new JavaType(importDeclaration.getName() + .toString()); + importPackage = importType.getPackage(); + } + + if (importPackage.equals(cid.getName().getPackage()) + && importDeclaration.isAsterisk()) { + continue; + } + + if (importPackage.equals(cid.getName().getPackage()) + && importType != null + && importType.getEnclosingType() == null) { + continue; + } + + if (importType != null && importType.equals(cid.getName())) { + continue; + } + + if (!imported.contains(importDeclaration.getName().toString())) { + imports.add(importDeclaration); + imported.add(importDeclaration.getName().toString()); + } + } + + Collections.sort(imports, new Comparator() { + public int compare(final ImportDeclaration importDeclaration, + final ImportDeclaration importDeclaration1) { + return importDeclaration.getName().toString() + .compareTo(importDeclaration1.getName().toString()); + } + }); + + compilationUnit.setImports(imports); + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserTypeResolutionService.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserTypeResolutionService.java new file mode 100644 index 000000000..6f4b55841 --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserTypeResolutionService.java @@ -0,0 +1,93 @@ +package org.springframework.roo.classpath.javaparser; + +import japa.parser.JavaParser; +import japa.parser.ParseException; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.body.TypeDeclaration; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeResolutionService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +@Component +@Service +public class JavaParserTypeResolutionService implements TypeResolutionService { + + public final JavaType getJavaType(final String fileIdentifier) { + Validate.notBlank(fileIdentifier, "Compilation unit path required"); + Validate.isTrue(new File(fileIdentifier).exists(), + "The file doesn't exist"); + Validate.isTrue(new File(fileIdentifier).isFile(), + "The identifier doesn't represent a file"); + try { + final File file = new File(fileIdentifier); + String typeContents = ""; + try { + typeContents = FileUtils.readFileToString(file); + } + catch (IOException ignored) { + } + if (StringUtils.isBlank(typeContents)) { + return null; + } + final CompilationUnit compilationUnit = JavaParser + .parse(new ByteArrayInputStream(typeContents.getBytes())); + final String typeName = fileIdentifier.substring( + fileIdentifier.lastIndexOf(File.separator) + 1, + fileIdentifier.lastIndexOf(".")); + for (final TypeDeclaration typeDeclaration : compilationUnit + .getTypes()) { + if (typeName.equals(typeDeclaration.getName())) { + return new JavaType(compilationUnit.getPackage().getName() + .getName() + + "." + typeDeclaration.getName()); + } + } + return null; + } + catch (final ParseException e) { + throw new IllegalStateException("Failed to parse " + fileIdentifier + + " : " + e.getMessage()); + } + } + + public final JavaPackage getPackage(final String fileIdentifier) { + Validate.notBlank(fileIdentifier, "Compilation unit path required"); + Validate.isTrue(new File(fileIdentifier).exists(), + "The file doesn't exist"); + Validate.isTrue(new File(fileIdentifier).isFile(), + "The identifier doesn't represent a file"); + try { + final File file = new File(fileIdentifier); + String typeContents = ""; + try { + typeContents = FileUtils.readFileToString(file); + } + catch (final IOException ignored) { + } + if (StringUtils.isBlank(typeContents)) { + return null; + } + final CompilationUnit compilationUnit = JavaParser + .parse(new ByteArrayInputStream(typeContents.getBytes())); + if (compilationUnit == null || compilationUnit.getPackage() == null) { + return null; + } + return new JavaPackage(compilationUnit.getPackage().getName() + .toString()); + } + catch (final ParseException e) { + throw new IllegalStateException("Failed to parse " + fileIdentifier + + " : " + e.getMessage()); + } + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserUtils.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserUtils.java new file mode 100644 index 000000000..265cdb569 --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/JavaParserUtils.java @@ -0,0 +1,1066 @@ +package org.springframework.roo.classpath.javaparser; + +import static org.springframework.roo.model.JavaType.OBJECT; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.ImportDeclaration; +import japa.parser.ast.TypeParameter; +import japa.parser.ast.body.ClassOrInterfaceDeclaration; +import japa.parser.ast.body.ModifierSet; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.expr.ClassExpr; +import japa.parser.ast.expr.Expression; +import japa.parser.ast.expr.FieldAccessExpr; +import japa.parser.ast.expr.MarkerAnnotationExpr; +import japa.parser.ast.expr.NameExpr; +import japa.parser.ast.expr.NormalAnnotationExpr; +import japa.parser.ast.expr.QualifiedNameExpr; +import japa.parser.ast.expr.SingleMemberAnnotationExpr; +import japa.parser.ast.type.ClassOrInterfaceType; +import japa.parser.ast.type.PrimitiveType; +import japa.parser.ast.type.PrimitiveType.Primitive; +import japa.parser.ast.type.ReferenceType; +import japa.parser.ast.type.Type; +import japa.parser.ast.type.VoidType; +import japa.parser.ast.type.WildcardType; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; + +/** + * Assists with the usage of Java Parser. + *

    + * This class is for internal use by the Java Parser module and should NOT be + * used by other code. + * + * @author Ben Alex + * @since 1.0 + */ +public final class JavaParserUtils { + + /** + * Converts the indicated {@link NameExpr} into a + * {@link ClassOrInterfaceType}. + *

    + * Note that no effort is made to manage imports etc. + * + * @param nameExpr to convert (required) + * @return the corresponding {@link ClassOrInterfaceType} (never null) + */ + public static ClassOrInterfaceType getClassOrInterfaceType( + final NameExpr nameExpr) { + Validate.notNull(nameExpr, "Java type required"); + if (nameExpr instanceof QualifiedNameExpr) { + final QualifiedNameExpr qne = (QualifiedNameExpr) nameExpr; + if (StringUtils.isNotBlank(qne.getQualifier().getName())) { + return new ClassOrInterfaceType(qne.getQualifier().getName() + + "." + qne.getName()); + } + return new ClassOrInterfaceType(qne.getName()); + } + return new ClassOrInterfaceType(nameExpr.getName()); + } + + /** + * Looks up the import declaration applicable to the presented name + * expression. + *

    + * If a fully-qualified name is passed to this method, the corresponding + * import will be evaluated for a complete match. If a simple name is passed + * to this method, the corresponding import will be evaluated if its simple + * name matches. This therefore reflects the normal Java semantics for using + * simple type names that have been imported. + * + * @param compilationUnitServices the types in the compilation unit + * (required) + * @param nameExpr the expression to locate an import for (which would + * generally be a {@link NameExpr} and thus not have a package + * identifier; required) + * @return the relevant import, or null if there is no import for the + * expression + */ + private static ImportDeclaration getImportDeclarationFor( + final CompilationUnitServices compilationUnitServices, + final NameExpr nameExpr) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(nameExpr, "Name expression required"); + + final List imports = compilationUnitServices + .getImports(); + + for (final ImportDeclaration candidate : imports) { + final NameExpr candidateNameExpr = candidate.getName(); + if (!candidate.toString().contains("*")) { + Validate.isInstanceOf(QualifiedNameExpr.class, + candidateNameExpr, "Expected import '" + candidate + + "' to use a fully-qualified type name"); + } + if (nameExpr instanceof QualifiedNameExpr) { + // User is asking for a fully-qualified name; let's see if there + // is a full match + if (isEqual(nameExpr, candidateNameExpr)) { + return candidate; + } + } + else { + // User is not asking for a fully-qualified name, so let's do a + // simple name comparison that discards the import's + // qualified-name package + if (candidateNameExpr.getName().equals(nameExpr.getName())) { + return candidate; + } + } + } + return null; + } + + /** + * Converts a JDK {@link Modifier} integer into the equivalent Java Parser + * modifier. + * + * @param modifiers the JDK int + * @return the equivalent Java Parser int + */ + public static int getJavaParserModifier(final int modifiers) { + int result = 0; + if (Modifier.isAbstract(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.ABSTRACT, result); + } + if (Modifier.isFinal(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.FINAL, result); + } + if (Modifier.isInterface(modifiers)) { + // Unsupported by Java Parser ModifierSet + } + if (Modifier.isNative(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.NATIVE, result); + } + if (Modifier.isPrivate(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.PRIVATE, result); + } + if (Modifier.isProtected(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.PROTECTED, result); + } + if (Modifier.isPublic(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.PUBLIC, result); + } + if (Modifier.isStatic(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.STATIC, result); + } + if (Modifier.isStrict(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.STRICTFP, result); + } + if (Modifier.isSynchronized(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.SYNCHRONIZED, result); + } + if (Modifier.isTransient(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.TRANSIENT, result); + } + if (Modifier.isVolatile(modifiers)) { + result = ModifierSet.addModifier(ModifierSet.VOLATILE, result); + } + return result; + } + + /** + * Resolves the effective {@link JavaType} a {@link NameExpr} represents. + *

    + * You should use {@link #getJavaType(CompilationUnitServices, Type, Set)} + * where possible so that type arguments are preserved (a {@link NameExpr} + * does not contain type arguments). + *

    + * A name expression can be either qualified or unqualified. + *

    + * If a name expression is qualified and the qualification starts with a + * lowercase letter, that represents the fully-qualified name. If the + * qualification starts with an uppercase letter, the package name is + * prepended to the qualifier. + *

    + * If a name expression is unqualified, the imports are scanned. If the + * unqualified name expression is found in the imports, that import + * declaration represents the fully-qualified name. If the unqualified name + * expression is not found in the imports, it indicates the name to find is + * either in the same package as the qualified name expression, or the type + * relates to a member of java.lang. If part of java.lang, the fully + * qualified name is treated as part of java.lang. Otherwise the compilation + * unit package plus unqualified name expression represents the fully + * qualified name expression. + * + * @param compilationUnitServices for package management (required) + * @param nameToFind to locate (required) + * @param typeParameters names to consider type parameters (can be null if + * there are none) + * @return the effective Java type (never null) + */ + public static JavaType getJavaType( + final CompilationUnitServices compilationUnitServices, + final NameExpr nameToFind, final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(nameToFind, "Name to find is required"); + + final JavaPackage compilationUnitPackage = compilationUnitServices + .getCompilationUnitPackage(); + + if (nameToFind instanceof QualifiedNameExpr) { + final QualifiedNameExpr qne = (QualifiedNameExpr) nameToFind; + + // Handle qualified name expressions that are related to inner types + // (eg Foo.Bar) + final NameExpr qneQualifier = qne.getQualifier(); + final NameExpr enclosedBy = getNameExpr(compilationUnitServices + .getEnclosingTypeName().getSimpleTypeName()); + if (isEqual(qneQualifier, enclosedBy)) { + // This qualified name expression is simply an inner type + // reference + final String name = compilationUnitServices + .getEnclosingTypeName().getFullyQualifiedTypeName() + + "." + nameToFind.getName(); + return new JavaType(name, + compilationUnitServices.getEnclosingTypeName()); + } + + // Refers to a different enclosing type, so calculate the package + // name based on convention of an uppercase letter denotes same + // package (ROO-1210) + if (qne.toString().length() > 1 + && Character.isUpperCase(qne.toString().charAt(0))) { + // First letter is uppercase, so this likely requires prepending + // of some package name + final ImportDeclaration importDeclaration = getImportDeclarationFor( + compilationUnitServices, qne.getQualifier()); + if (importDeclaration == null) { + if (!compilationUnitPackage.getFullyQualifiedPackageName() + .equals("")) { + // It was not imported, so let's assume it's in the same + // package + return new JavaType(compilationUnitServices + .getCompilationUnitPackage() + .getFullyQualifiedPackageName() + + "." + qne.toString()); + } + } + else { + return new JavaType(importDeclaration.getName() + "." + + qne.getName()); + } + + // This name expression (which contains a dot) had its qualifier + // imported, so let's use the import + } + else { + // First letter is lowercase, so the reference already includes + // a package + return new JavaType(qne.toString()); + } + } + + if ("?".equals(nameToFind.getName())) { + return new JavaType(OBJECT.getFullyQualifiedTypeName(), 0, + DataType.TYPE, JavaType.WILDCARD_NEITHER, null); + } + + // Unqualified name detected, so check if it's in the type parameter + // list + if (typeParameters != null + && typeParameters.contains(new JavaSymbolName(nameToFind + .getName()))) { + return new JavaType(nameToFind.getName(), 0, DataType.VARIABLE, + null, null); + } + + // Check if we are looking for the enclosingType itself + final NameExpr enclosingTypeName = getNameExpr(compilationUnitServices + .getEnclosingTypeName().getSimpleTypeName()); + if (isEqual(enclosingTypeName, nameToFind)) { + return compilationUnitServices.getEnclosingTypeName(); + } + + // We are searching for a non-qualified name expression (nameToFind), so + // check if the compilation unit itself declares that type + for (final TypeDeclaration internalType : compilationUnitServices + .getInnerTypes()) { + final NameExpr nameExpr = getNameExpr(internalType.getName()); + if (isEqual(nameExpr, nameToFind)) { + // Found, so now we need to convert the internalType to a proper + // JavaType + final String name = compilationUnitServices + .getEnclosingTypeName().getFullyQualifiedTypeName() + + "." + nameToFind.getName(); + return new JavaType(name); + } + } + + final ImportDeclaration importDeclaration = getImportDeclarationFor( + compilationUnitServices, nameToFind); + if (importDeclaration == null) { + if (JdkJavaType.isPartOfJavaLang(nameToFind.getName())) { + return new JavaType("java.lang." + nameToFind.getName()); + } + final String name = compilationUnitPackage + .getFullyQualifiedPackageName().equals("") ? nameToFind + .getName() : compilationUnitPackage + .getFullyQualifiedPackageName() + + "." + + nameToFind.getName(); + return new JavaType(name); + } + + return new JavaType(importDeclaration.getName().toString()); + } + + /** + * Resolves the effective {@link JavaType} a {@link Type} represents. A + * {@link Type} includes low-level types such as void, arrays and + * primitives. + * + * @param compilationUnitServices to use for package resolution (required) + * @param type to locate (required) + * @param typeParameters names to consider type parameters (can be null if + * there are none) + * @return the {@link JavaType}, with proper indication of primitive and + * array status (never null) + */ + public static JavaType getJavaType( + final CompilationUnitServices compilationUnitServices, + final Type type, final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(type, "The reference type must be provided"); + + if (type instanceof VoidType) { + return JavaType.VOID_PRIMITIVE; + } + + int array = 0; + + Type internalType = type; + if (internalType instanceof ReferenceType) { + array = ((ReferenceType) internalType).getArrayCount(); + if (array > 0) { + internalType = ((ReferenceType) internalType).getType(); + } + } + + if (internalType instanceof PrimitiveType) { + final PrimitiveType pt = (PrimitiveType) internalType; + if (pt.getType().equals(Primitive.Boolean)) { + return new JavaType(Boolean.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Char)) { + return new JavaType(Character.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Byte)) { + return new JavaType(Byte.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Short)) { + return new JavaType(Short.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Int)) { + return new JavaType(Integer.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Long)) { + return new JavaType(Long.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Float)) { + return new JavaType(Float.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + if (pt.getType().equals(Primitive.Double)) { + return new JavaType(Double.class.getName(), array, + DataType.PRIMITIVE, null, null); + } + throw new IllegalStateException("Unsupported primitive '" + + pt.getType() + "'"); + } + + if (internalType instanceof WildcardType) { + // We only provide very primitive support for wildcard types; Roo + // only needs metadata at the end of the day, + // not complete binding support from an AST + final WildcardType wt = (WildcardType) internalType; + if (wt.getSuper() != null) { + final ReferenceType rt = wt.getSuper(); + final ClassOrInterfaceType cit = (ClassOrInterfaceType) rt + .getType(); + final JavaType effectiveType = getJavaTypeNow( + compilationUnitServices, cit, typeParameters); + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + rt.getArrayCount(), effectiveType.getDataType(), + JavaType.WILDCARD_SUPER, effectiveType.getParameters()); + } + else if (wt.getExtends() != null) { + final ReferenceType rt = wt.getExtends(); + final ClassOrInterfaceType cit = (ClassOrInterfaceType) rt + .getType(); + final JavaType effectiveType = getJavaTypeNow( + compilationUnitServices, cit, typeParameters); + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + rt.getArrayCount(), effectiveType.getDataType(), + JavaType.WILDCARD_EXTENDS, + effectiveType.getParameters()); + } + else { + return new JavaType(OBJECT.getFullyQualifiedTypeName(), 0, + DataType.TYPE, JavaType.WILDCARD_NEITHER, null); + } + } + + ClassOrInterfaceType cit; + if (internalType instanceof ClassOrInterfaceType) { + cit = (ClassOrInterfaceType) internalType; + } + else if (internalType instanceof ReferenceType) { + cit = (ClassOrInterfaceType) ((ReferenceType) type).getType(); + } + else { + throw new IllegalStateException("The presented type '" + + internalType.getClass() + "' with value '" + internalType + + "' is unsupported by JavaParserUtils"); + } + + final JavaType effectiveType = getJavaTypeNow(compilationUnitServices, + cit, typeParameters); + if (array > 0) { + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + array, effectiveType.getDataType(), + effectiveType.getArgName(), effectiveType.getParameters()); + } + + return effectiveType; + } + + /** + * Resolves the effective {@link JavaType} a + * {@link ClassOrInterfaceDeclaration} represents, including any type + * parameters. + * + * @param compilationUnitServices for package management (required) + * @param typeDeclaration the type declaration to resolve (required) + * @return the effective Java type (never null) + */ + public static JavaType getJavaType( + final CompilationUnitServices compilationUnitServices, + final TypeDeclaration typeDeclaration) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(typeDeclaration, "Type declaration required"); + + // Convert the ClassOrInterfaceDeclaration name into a JavaType + final NameExpr nameExpr = getNameExpr(typeDeclaration.getName()); + final JavaType effectiveType = getJavaType(compilationUnitServices, + nameExpr, null); + + final List parameterTypes = new ArrayList(); + if (typeDeclaration instanceof ClassOrInterfaceDeclaration) { + final ClassOrInterfaceDeclaration clazz = (ClassOrInterfaceDeclaration) typeDeclaration; + // Populate JavaType with type parameters + final List typeParameters = clazz + .getTypeParameters(); + if (typeParameters != null) { + final Set locatedTypeParameters = new HashSet(); + for (final TypeParameter candidate : typeParameters) { + final JavaSymbolName currentTypeParam = new JavaSymbolName( + candidate.getName()); + locatedTypeParameters.add(currentTypeParam); + JavaType javaType = null; + if (candidate.getTypeBound() == null) { + javaType = new JavaType( + OBJECT.getFullyQualifiedTypeName(), 0, + DataType.TYPE, currentTypeParam, null); + } + else { + final ClassOrInterfaceType cit = candidate + .getTypeBound().get(0); + javaType = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, cit, + locatedTypeParameters); + javaType = new JavaType( + javaType.getFullyQualifiedTypeName(), + javaType.getArray(), javaType.getDataType(), + currentTypeParam, javaType.getParameters()); + } + parameterTypes.add(javaType); + } + } + } + + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + effectiveType.getArray(), effectiveType.getDataType(), null, + parameterTypes); + } + + /** + * Resolves the effective {@link JavaType} a {@link ClassOrInterfaceType} + * represents, including any type arguments. + * + * @param compilationUnitServices for package management (required) + * @param cit the class or interface type to resolve (required) + * @return the effective Java type (never null) + */ + public static JavaType getJavaTypeNow( + final CompilationUnitServices compilationUnitServices, + final ClassOrInterfaceType cit, + final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(cit, "ClassOrInterfaceType required"); + + final JavaPackage compilationUnitPackage = compilationUnitServices + .getCompilationUnitPackage(); + Validate.notNull(compilationUnitPackage, + "Compilation unit package required"); + + String typeName = cit.getName(); + ClassOrInterfaceType scope = cit.getScope(); + while (scope != null) { + typeName = scope.getName() + "." + typeName; + scope = scope.getScope(); + } + final NameExpr nameExpr = getNameExpr(typeName); + + final JavaType effectiveType = getJavaType(compilationUnitServices, + nameExpr, typeParameters); + + // Handle any type arguments + final List parameterTypes = new ArrayList(); + if (cit.getTypeArgs() != null) { + for (final Type ta : cit.getTypeArgs()) { + parameterTypes.add(getJavaType(compilationUnitServices, ta, + typeParameters)); + } + } + + return new JavaType(effectiveType.getFullyQualifiedTypeName(), + effectiveType.getArray(), effectiveType.getDataType(), null, + parameterTypes); + } + + /** + * Converts a Java Parser modifier integer into a JDK {@link Modifier} + * integer. + * + * @param modifiers the Java Parser int + * @return the equivalent JDK int + */ + public static int getJdkModifier(final int modifiers) { + int result = 0; + if (ModifierSet.isAbstract(modifiers)) { + result |= Modifier.ABSTRACT; + } + if (ModifierSet.isFinal(modifiers)) { + result |= Modifier.FINAL; + } + if (ModifierSet.isNative(modifiers)) { + result |= Modifier.NATIVE; + } + if (ModifierSet.isPrivate(modifiers)) { + result |= Modifier.PRIVATE; + } + if (ModifierSet.isProtected(modifiers)) { + result |= Modifier.PROTECTED; + } + if (ModifierSet.isPublic(modifiers)) { + result |= Modifier.PUBLIC; + } + if (ModifierSet.isStatic(modifiers)) { + result |= Modifier.STATIC; + } + if (ModifierSet.isStrictfp(modifiers)) { + result |= Modifier.STRICT; + } + if (ModifierSet.isSynchronized(modifiers)) { + result |= Modifier.SYNCHRONIZED; + } + if (ModifierSet.isTransient(modifiers)) { + result |= Modifier.TRANSIENT; + } + if (ModifierSet.isVolatile(modifiers)) { + result |= Modifier.VOLATILE; + } + return result; + } + + /** + * Obtains the name expression ({@link NameExpr}) for the passed + * {@link AnnotationExpr}, which is the annotation's type. + * + * @param annotationExpr to retrieve the type name from (required) + * @return the name (never null) + */ + public static NameExpr getNameExpr(final AnnotationExpr annotationExpr) { + Validate.notNull(annotationExpr, "Annotation expression required"); + if (annotationExpr instanceof MarkerAnnotationExpr) { + final MarkerAnnotationExpr a = (MarkerAnnotationExpr) annotationExpr; + final NameExpr nameToFind = a.getName(); + Validate.notNull(nameToFind, + "Unable to determine annotation name from '" + + annotationExpr + "'"); + return nameToFind; + } + else if (annotationExpr instanceof SingleMemberAnnotationExpr) { + final SingleMemberAnnotationExpr a = (SingleMemberAnnotationExpr) annotationExpr; + final NameExpr nameToFind = a.getName(); + Validate.notNull(nameToFind, + "Unable to determine annotation name from '" + + annotationExpr + "'"); + return nameToFind; + } + else if (annotationExpr instanceof NormalAnnotationExpr) { + final NormalAnnotationExpr a = (NormalAnnotationExpr) annotationExpr; + final NameExpr nameToFind = a.getName(); + Validate.notNull(nameToFind, + "Unable to determine annotation name from '" + + annotationExpr + "'"); + return nameToFind; + } + throw new UnsupportedOperationException( + "Unknown annotation expression type '" + + annotationExpr.getClass().getName() + "'"); + } + + /** + * Converts the presented class name into a name expression (either a + * {@link NameExpr} or {@link QualifiedNameExpr} depending on whether a + * package was presented). + * + * @param className to convert (required; can be fully qualified or simple + * name only) + * @return a compatible expression (never returns null) + */ + public static NameExpr getNameExpr(final String className) { + Validate.notBlank(className, "Class name required"); + if (className.contains(".")) { + final int offset = className.lastIndexOf("."); + final String packageName = className.substring(0, offset); + final String typeName = className.substring(offset + 1); + return new QualifiedNameExpr(new NameExpr(packageName), typeName); + } + return new NameExpr(className); + } + + /** + * Converts the indicated {@link JavaType} into a {@link ReferenceType}. + *

    + * Note that no effort is made to manage imports etc. + * + * @param nameExpr to convert (required) + * @return the corresponding {@link ReferenceType} (never null) + */ + public static ReferenceType getReferenceType(final NameExpr nameExpr) { + Validate.notNull(nameExpr, "Java type required"); + return new ReferenceType(getClassOrInterfaceType(nameExpr)); + } + + public static ClassOrInterfaceType getResolvedName(final JavaType target, + final JavaType current, final CompilationUnit compilationUnit) { + final NameExpr nameExpr = JavaParserUtils.importTypeIfRequired(target, + compilationUnit.getImports(), current); + final ClassOrInterfaceType resolvedName = JavaParserUtils + .getClassOrInterfaceType(nameExpr); + if (current.getParameters() != null + && current.getParameters().size() > 0) { + resolvedName.setTypeArgs(new ArrayList()); + for (final JavaType param : current.getParameters()) { + resolvedName.getTypeArgs().add( + getResolvedName(target, param, compilationUnit)); + } + } + + return resolvedName; + } + + public static Type getResolvedName(final JavaType target, + final JavaType current, + final CompilationUnitServices compilationUnit) { + final NameExpr nameExpr = JavaParserUtils.importTypeIfRequired(target, + compilationUnit.getImports(), current); + final ClassOrInterfaceType resolvedName = JavaParserUtils + .getClassOrInterfaceType(nameExpr); + if (current.getParameters() != null + && current.getParameters().size() > 0) { + resolvedName.setTypeArgs(new ArrayList()); + for (final JavaType param : current.getParameters()) { + resolvedName.getTypeArgs().add( + getResolvedName(target, param, compilationUnit)); + } + } + + if (current.getArray() > 0) { + // Primitives includes array declaration in resolvedName + if (!current.isPrimitive()){ + return new ReferenceType(resolvedName, current.getArray()); + } + } + + return resolvedName; + } + + /** + * Given a primitive type, computes the corresponding Java Parser type. + *

    + * Presenting a non-primitive type to this method will throw an exception. + * If you have a non-primitive type, use + * {@link #importTypeIfRequired(JavaType, List, JavaType)} and then present + * the {@link NameExpr} it returns to + * {@link #getClassOrInterfaceType(NameExpr)}. + * + * @param javaType a primitive type (required, and must be primitive) + * @return the equivalent Java Parser {@link Type} + */ + public static Type getType(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + Validate.isTrue(javaType.isPrimitive(), + "Java type must be primitive to be presented to this method"); + if (javaType.equals(JavaType.VOID_PRIMITIVE)) { + return new VoidType(); + } + else if (javaType.equals(JavaType.BOOLEAN_PRIMITIVE)) { + return new PrimitiveType(Primitive.Boolean); + } + else if (javaType.equals(JavaType.BYTE_PRIMITIVE)) { + return new PrimitiveType(Primitive.Byte); + } + else if (javaType.equals(JavaType.CHAR_PRIMITIVE)) { + return new PrimitiveType(Primitive.Char); + } + else if (javaType.equals(JavaType.DOUBLE_PRIMITIVE)) { + return new PrimitiveType(Primitive.Double); + } + else if (javaType.equals(JavaType.FLOAT_PRIMITIVE)) { + return new PrimitiveType(Primitive.Float); + } + else if (javaType.equals(JavaType.INT_PRIMITIVE)) { + return new PrimitiveType(Primitive.Int); + } + else if (javaType.equals(JavaType.LONG_PRIMITIVE)) { + return new PrimitiveType(Primitive.Long); + } + else if (javaType.equals(JavaType.SHORT_PRIMITIVE)) { + return new PrimitiveType(Primitive.Short); + } + throw new IllegalStateException("Unknown primitive " + javaType); + } + + /** + * Recognises {@link Expression}s of type {@link FieldAccessExpr} and + * {@link ClassExpr} and automatically imports them if required, returning + * the correct {@link Expression} that should subsequently be used. + *

    + * Even if an {@link Expression} is not resolved by this method into a type + * and/or imported, the method guarantees to always return an + * {@link Expression} that the caller can subsequently use in place of the + * passed {@link Expression}. In practical terms, the {@link Expression} + * passed to this method will be returned unless the type was already + * imported, just imported, or represented a java.lang type. + * + * @param targetType the compilation unit target type (required) + * @param imports the existing imports (required) + * @param value that expression, which need not necessarily be resolvable to + * a type (required) + * @return the expression to now use, as appropriately resolved (never + * returns null) + */ + public static Expression importExpressionIfRequired( + final JavaType targetType, final List imports, + final Expression value) { + Validate.notNull(targetType, "Target type required"); + Validate.notNull(imports, "Imports required"); + Validate.notNull(value, "Expression value required"); + + if (value instanceof FieldAccessExpr) { + final Expression scope = ((FieldAccessExpr) value).getScope(); + final String field = ((FieldAccessExpr) value).getField(); + if (scope instanceof QualifiedNameExpr) { + final String packageName = ((QualifiedNameExpr) scope) + .getQualifier().getName(); + final String simpleName = ((QualifiedNameExpr) scope).getName(); + final String fullyQualifiedName = packageName + "." + + simpleName; + final JavaType javaType = new JavaType(fullyQualifiedName); + final NameExpr nameToUse = importTypeIfRequired(targetType, + imports, javaType); + if (!(nameToUse instanceof QualifiedNameExpr)) { + return new FieldAccessExpr(nameToUse, field); + } + } + } + else if (value instanceof ClassExpr) { + final Type type = ((ClassExpr) value).getType(); + if (type instanceof ClassOrInterfaceType) { + final JavaType javaType = new JavaType( + ((ClassOrInterfaceType) type).getName()); + final NameExpr nameToUse = importTypeIfRequired(targetType, + imports, javaType); + if (!(nameToUse instanceof QualifiedNameExpr)) { + return new ClassExpr(new ClassOrInterfaceType( + javaType.getSimpleTypeName())); + } + } + else if (type instanceof ReferenceType + && ((ReferenceType) type).getType() instanceof ClassOrInterfaceType) { + final ClassOrInterfaceType cit = (ClassOrInterfaceType) ((ReferenceType) type) + .getType(); + final JavaType javaType = new JavaType(cit.getName()); + final NameExpr nameToUse = importTypeIfRequired(targetType, + imports, javaType); + if (!(nameToUse instanceof QualifiedNameExpr)) { + return new ClassExpr(new ClassOrInterfaceType( + javaType.getSimpleTypeName())); + } + } + } + + // Make no changes + return value; + } + + public static ReferenceType importParametersForType( + final JavaType targetType, final List imports, + final JavaType typeToImport) { + Validate.notNull(targetType, "Target type is required"); + Validate.notNull(imports, "Compilation unit imports required"); + Validate.notNull(typeToImport, "Java type to import is required"); + + final ClassOrInterfaceType cit = getClassOrInterfaceType(importTypeIfRequired( + targetType, imports, typeToImport)); + + // Add any type arguments presented for the return type + if (typeToImport.getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : typeToImport + .getParameters()) { + typeArgs.add(JavaParserUtils.importParametersForType( + targetType, + imports, parameter)); + } + } + return new ReferenceType(cit); + } + + /** + * Attempts to import the presented {@link JavaType}. + *

    + * Whether imported or not, the method returns a {@link NameExpr} suitable + * for subsequent use when referring to that type. + *

    + * If an attempt is made to import a java.lang type, it is ignored. + *

    + * If an attempt is made to import a type without a package, it is ignored. + *

    + * We import every type usage even if the type usage is within the same + * package and would theoretically not require an import. This is undertaken + * so that there is no requirement to separately parse every unqualified + * type usage within the compilation unit so as to refrain from importing + * subsequently conflicting types. + * + * @param targetType the compilation unit target type (required) + * @param imports the compilation unit's imports (required) + * @param typeToImport the type to be imported (required) + * @return the name expression to be used when referring to that type (never + * null) + */ + public static NameExpr importTypeIfRequired(final JavaType targetType, + final List imports, final JavaType typeToImport) { + Validate.notNull(targetType, "Target type is required"); + final JavaPackage compilationUnitPackage = targetType.getPackage(); + Validate.notNull(imports, "Compilation unit imports required"); + Validate.notNull(typeToImport, "Java type to import is required"); + + // If it's a primitive, it's really easy + if (typeToImport.isPrimitive()) { + return new NameExpr(typeToImport.getNameIncludingTypeParameters()); + } + + // Handle if the type doesn't have a package at all + if (typeToImport.isDefaultPackage()) { + return new NameExpr(typeToImport.getSimpleTypeName()); + } + + final JavaPackage typeToImportPackage = typeToImport.getPackage(); + if (typeToImportPackage.equals(compilationUnitPackage)) { + return new NameExpr(typeToImport.getSimpleTypeName()); + } + + NameExpr typeToImportExpr; + if (typeToImport.getEnclosingType() == null) { + typeToImportExpr = new QualifiedNameExpr(new NameExpr(typeToImport + .getPackage().getFullyQualifiedPackageName()), + typeToImport.getSimpleTypeName()); + } + else { + typeToImportExpr = new QualifiedNameExpr(new NameExpr(typeToImport + .getEnclosingType().getFullyQualifiedTypeName()), + typeToImport.getSimpleTypeName()); + } + + final ImportDeclaration newImport = new ImportDeclaration( + typeToImportExpr, false, false); + + boolean addImport = true; + boolean useSimpleTypeName = false; + for (final ImportDeclaration existingImport : imports) { + if (existingImport.getName().getName() + .equals(newImport.getName().getName())) { + // Do not import, as there is already an import with the simple + // type name + addImport = false; + + // If this is a complete match, it indicates we can use the + // simple type name + if (isEqual(existingImport.getName(), newImport.getName())) { + useSimpleTypeName = true; + break; + } + } + } + + if (addImport + && JdkJavaType.isPartOfJavaLang(typeToImport + .getSimpleTypeName())) { + // This simple type name would be part of java.lang if left as the + // simple name. We want a fully-qualified name. + addImport = false; + useSimpleTypeName = false; + } + + if (JdkJavaType.isPartOfJavaLang(typeToImport)) { + // So we would have imported, but we don't need to + addImport = false; + + // The fact we could have imported means there was no other + // conflicting simple type names + useSimpleTypeName = true; + } + + if (addImport + && typeToImport.getPackage().equals(compilationUnitPackage)) { + // It is not theoretically necessary to add an import for something + // in the same package, + // but we elect to explicitly perform an import so future + // conflicting types are not imported + // addImport = true; + // useSimpleTypeName = false; + } + + if (addImport + && targetType.getSimpleTypeName().equals( + typeToImport.getSimpleTypeName())) { + // So we would have imported it, but then it would conflict with the + // simple name of the type + addImport = false; + useSimpleTypeName = false; + } + + if (addImport) { + imports.add(newImport); + useSimpleTypeName = true; + } + + // This is pretty crude, but at least it emits source code for people + // (forget imports, though!) + if (typeToImport.getArgName() != null) { + return new NameExpr(typeToImport.toString()); + } + + if (useSimpleTypeName) { + return new NameExpr(typeToImport.getSimpleTypeName()); + } + return new QualifiedNameExpr(new NameExpr(typeToImport.getPackage() + .getFullyQualifiedPackageName()), + typeToImport.getSimpleTypeName()); + } + + /** + * Indicates whether two {@link NameExpr} expressions are equal. + *

    + * This method is necessary given {@link NameExpr} does not offer an equals + * method. + * + * @param o1 the first entry to compare (null is acceptable) + * @param o2 the second entry to compare (null is acceptable) + * @return true if and only if both entries are identical + */ + private static boolean isEqual(final NameExpr o1, final NameExpr o2) { + if (o1 == null && o2 == null) { + return true; + } + if (o1 == null && o2 != null) { + return false; + } + if (o1 != null && o2 == null) { + return false; + } + if (o1 != null && !o1.getName().equals(o2.getName())) { + return false; + } + return o1 != null && o1.toString().equals(o2.toString()); + } + + /** + * Searches a compilation unit and locates the declaration with the given + * type's simple name. + * + * @param compilationUnit to scan (required) + * @param javaType the target to locate (required) + * @return the located type declaration or null if it could not be found + */ + public static TypeDeclaration locateTypeDeclaration( + final CompilationUnit compilationUnit, final JavaType javaType) { + Validate.notNull(compilationUnit, "Compilation unit required"); + Validate.notNull(javaType, "Java type to search for required"); + if (compilationUnit.getTypes() == null) { + return null; + } + for (final TypeDeclaration candidate : compilationUnit.getTypes()) { + if (javaType.getSimpleTypeName().equals(candidate.getName())) { + // We have the required type declaration + return candidate; + } + } + return null; + } + + /** + * Constructor is private to prevent instantiation + */ + private JavaParserUtils() { + } + + /** + * Returns the final {@link ClassOrInterfaceType} from a {@link Type} + * + * @param initType + * @return the final {@link ClassOrInterfaceType} or null if no {@link ClassOrInterfaceType} found + * + */ + public static ClassOrInterfaceType getClassOrInterfaceType(Type type) { + Type tmp = type; + while (tmp instanceof ReferenceType) { + tmp = ((ReferenceType) tmp).getType(); + }; + if (tmp instanceof ClassOrInterfaceType){ + return (ClassOrInterfaceType) tmp; + } + return null; + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserAnnotationMetadataBuilder.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserAnnotationMetadataBuilder.java new file mode 100644 index 000000000..6508c6bf3 --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserAnnotationMetadataBuilder.java @@ -0,0 +1,615 @@ +package org.springframework.roo.classpath.javaparser.details; + +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.expr.ArrayInitializerExpr; +import japa.parser.ast.expr.BinaryExpr; +import japa.parser.ast.expr.BooleanLiteralExpr; +import japa.parser.ast.expr.CharLiteralExpr; +import japa.parser.ast.expr.ClassExpr; +import japa.parser.ast.expr.DoubleLiteralExpr; +import japa.parser.ast.expr.Expression; +import japa.parser.ast.expr.FieldAccessExpr; +import japa.parser.ast.expr.IntegerLiteralExpr; +import japa.parser.ast.expr.LongLiteralExpr; +import japa.parser.ast.expr.MarkerAnnotationExpr; +import japa.parser.ast.expr.MemberValuePair; +import japa.parser.ast.expr.NameExpr; +import japa.parser.ast.expr.NormalAnnotationExpr; +import japa.parser.ast.expr.SingleMemberAnnotationExpr; +import japa.parser.ast.expr.StringLiteralExpr; +import japa.parser.ast.expr.UnaryExpr; +import japa.parser.ast.expr.UnaryExpr.Operator; +import japa.parser.ast.type.Type; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.CharAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.DoubleAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.classpath.details.annotations.LongAttributeValue; +import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.javaparser.CompilationUnitServices; +import org.springframework.roo.classpath.javaparser.JavaParserUtils; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Java Parser implementation of {@link AnnotationMetadata}. + * + * @author Ben Alex + * @author Andrew Swan + * @since 1.0 + */ +public class JavaParserAnnotationMetadataBuilder implements + Builder { + + /** + * Facilitates the addition of the annotation to the presented type. + * + * @param compilationUnitServices to use (required) + * @param annotations to add to the end of (required) + * @param annotation to add (required) + */ + public static void addAnnotationToList( + final CompilationUnitServices compilationUnitServices, + final List annotations, + final AnnotationMetadata annotation) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(annotations, "Annotations required"); + Validate.notNull(annotation, "Annotation metadata required"); + + // Create a holder for the annotation we're going to create + boolean foundExisting = false; + + // Search for an existing annotation of this type + for (final AnnotationExpr candidate : annotations) { + NameExpr existingName = null; + if (candidate instanceof NormalAnnotationExpr) { + existingName = ((NormalAnnotationExpr) candidate).getName(); + } + else if (candidate instanceof MarkerAnnotationExpr) { + existingName = ((MarkerAnnotationExpr) candidate).getName(); + } + else if (candidate instanceof SingleMemberAnnotationExpr) { + existingName = ((SingleMemberAnnotationExpr) candidate) + .getName(); + } + + // Convert the candidate annotation type's into a JavaType + final JavaType javaType = JavaParserUtils.getJavaType( + compilationUnitServices, existingName, null); + if (annotation.getAnnotationType().equals(javaType)) { + foundExisting = true; + break; + } + } + Validate.isTrue( + !foundExisting, + "Found an existing annotation for type '" + + annotation.getAnnotationType() + "'"); + + // Import the annotation type, if needed + final NameExpr nameToUse = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + annotation.getAnnotationType()); + + // Create member-value pairs in accordance with Java Parser requirements + final List memberValuePairs = new ArrayList(); + for (final JavaSymbolName attributeName : annotation + .getAttributeNames()) { + final AnnotationAttributeValue value = annotation + .getAttribute(attributeName); + Validate.notNull(value, "Unable to acquire value '" + attributeName + + "' from annotation"); + final MemberValuePair memberValuePair = convert(value); + // Validate.notNull(memberValuePair, + // "Member value pair should have been set"); + if (memberValuePair != null) { + memberValuePairs.add(memberValuePair); + } + } + + // Create the AnnotationExpr; it varies depending on how many + // member-value pairs we need to present + AnnotationExpr annotationExpression = null; + if (memberValuePairs.isEmpty()) { + annotationExpression = new MarkerAnnotationExpr(nameToUse); + } + else if (memberValuePairs.size() == 1 + && (memberValuePairs.get(0).getName() == null || "value" + .equals(memberValuePairs.get(0).getName()))) { + final Expression toUse = JavaParserUtils + .importExpressionIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + memberValuePairs.get(0).getValue()); + annotationExpression = new SingleMemberAnnotationExpr(nameToUse, + toUse); + } + else { + // We have a number of pairs being presented + annotationExpression = new NormalAnnotationExpr(nameToUse, + new ArrayList()); + } + + // Add our AnnotationExpr to the actual annotations that will eventually + // be flushed through to the compilation unit + annotations.add(annotationExpression); + + // Add member-value pairs to our AnnotationExpr + if (!memberValuePairs.isEmpty()) { + // Have to check here for cases where we need to change an existing + // MarkerAnnotationExpr to a NormalAnnotationExpr or + // SingleMemberAnnotationExpr + if (annotationExpression instanceof MarkerAnnotationExpr) { + final MarkerAnnotationExpr mae = (MarkerAnnotationExpr) annotationExpression; + + annotations.remove(mae); + + if (memberValuePairs.size() == 1 + && (memberValuePairs.get(0).getName() == null || "value" + .equals(memberValuePairs.get(0).getName()))) { + final Expression toUse = JavaParserUtils + .importExpressionIfRequired(compilationUnitServices + .getEnclosingTypeName(), + compilationUnitServices.getImports(), + memberValuePairs.get(0).getValue()); + annotationExpression = new SingleMemberAnnotationExpr( + nameToUse, toUse); + annotations.add(annotationExpression); + } + else { + // We have a number of pairs being presented + annotationExpression = new NormalAnnotationExpr(nameToUse, + new ArrayList()); + annotations.add(annotationExpression); + } + } + if (annotationExpression instanceof SingleMemberAnnotationExpr) { + // Potentially upgrade this expression to a NormalAnnotationExpr + final SingleMemberAnnotationExpr smae = (SingleMemberAnnotationExpr) annotationExpression; + if (memberValuePairs.size() == 1 + && memberValuePairs.get(0).getName() == null + || memberValuePairs.get(0).getName().equals("value") + || memberValuePairs.get(0).getName().equals("")) { + // They specified only a single member-value pair, and it is + // the default anyway, so we need not do anything except + // update the value + final Expression toUse = JavaParserUtils + .importExpressionIfRequired(compilationUnitServices + .getEnclosingTypeName(), + compilationUnitServices.getImports(), + memberValuePairs.get(0).getValue()); + smae.setMemberValue(toUse); + return; + } + + // There is > 1 expression, or they have provided some sort of + // non-default value, so it's time to upgrade the expression + // (whilst retaining any potentially existing expression values) + final Expression existingValue = smae.getMemberValue(); + annotationExpression = new NormalAnnotationExpr(smae.getName(), + new ArrayList()); + ((NormalAnnotationExpr) annotationExpression).getPairs().add( + new MemberValuePair("value", existingValue)); + } + Validate.isInstanceOf( + NormalAnnotationExpr.class, + annotationExpression, + "Attempting to add >1 annotation member-value pair requires an existing normal annotation expression"); + final List annotationPairs = ((NormalAnnotationExpr) annotationExpression) + .getPairs(); + annotationPairs.clear(); + for (final MemberValuePair pair : memberValuePairs) { + final Expression toUse = JavaParserUtils + .importExpressionIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + pair.getValue()); + pair.setValue(toUse); + annotationPairs.add(pair); + } + } + } + + @SuppressWarnings("unchecked") + private static MemberValuePair convert( + final AnnotationAttributeValue value) { + if (value instanceof NestedAnnotationAttributeValue) { + final NestedAnnotationAttributeValue castValue = (NestedAnnotationAttributeValue) value; + AnnotationExpr annotationExpr; + final AnnotationMetadata nestedAnnotation = castValue.getValue(); + if (castValue.getValue().getAttributeNames().size() == 0) { + annotationExpr = new MarkerAnnotationExpr( + JavaParserUtils.getNameExpr(nestedAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName())); + } + else if (castValue.getValue().getAttributeNames().size() == 1) { + annotationExpr = new SingleMemberAnnotationExpr( + JavaParserUtils.getNameExpr(nestedAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName()), convert( + nestedAnnotation.getAttribute(nestedAnnotation + .getAttributeNames().get(0))) + .getValue()); + } + else { + final List memberValuePairs = new ArrayList(); + for (final JavaSymbolName attributeName : nestedAnnotation + .getAttributeNames()) { + memberValuePairs.add(convert(nestedAnnotation + .getAttribute(attributeName))); + } + annotationExpr = new NormalAnnotationExpr( + JavaParserUtils.getNameExpr(nestedAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName()), memberValuePairs); + } + // Rely on the nested instance to know its member value pairs + return new MemberValuePair(value.getName().getSymbolName(), + annotationExpr); + } + + if (value instanceof BooleanAttributeValue) { + final boolean castValue = ((BooleanAttributeValue) value) + .getValue(); + final BooleanLiteralExpr convertedValue = new BooleanLiteralExpr( + castValue); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof CharAttributeValue) { + final char castValue = ((CharAttributeValue) value).getValue(); + final CharLiteralExpr convertedValue = new CharLiteralExpr( + new String(new char[] { castValue })); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof LongAttributeValue) { + final Long castValue = ((LongAttributeValue) value).getValue(); + final LongLiteralExpr convertedValue = new LongLiteralExpr( + castValue.toString() + "L"); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof IntegerAttributeValue) { + final Integer castValue = ((IntegerAttributeValue) value) + .getValue(); + final IntegerLiteralExpr convertedValue = new IntegerLiteralExpr( + castValue.toString()); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof DoubleAttributeValue) { + final DoubleAttributeValue doubleAttributeValue = (DoubleAttributeValue) value; + final Double castValue = doubleAttributeValue.getValue(); + DoubleLiteralExpr convertedValue; + if (doubleAttributeValue.isFloatingPrecisionOnly()) { + convertedValue = new DoubleLiteralExpr(castValue.toString() + + "F"); + } + else { + convertedValue = new DoubleLiteralExpr(castValue.toString() + + "D"); + } + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof StringAttributeValue) { + final String castValue = ((StringAttributeValue) value).getValue(); + final StringLiteralExpr convertedValue = new StringLiteralExpr( + castValue.toString()); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof EnumAttributeValue) { + final EnumDetails castValue = ((EnumAttributeValue) value) + .getValue(); + // This isn't as elegant as it could be (ie loss of type + // parameters), but it will do for now + final FieldAccessExpr convertedValue = new FieldAccessExpr( + JavaParserUtils.getNameExpr(castValue.getType() + .getFullyQualifiedTypeName()), castValue.getField() + .getSymbolName()); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof ClassAttributeValue) { + final JavaType castValue = ((ClassAttributeValue) value).getValue(); + // This doesn't preserve type parameters + final NameExpr nameExpr = JavaParserUtils.getNameExpr(castValue + .getFullyQualifiedTypeName()); + final ClassExpr convertedValue = new ClassExpr( + JavaParserUtils.getReferenceType(nameExpr)); + return new MemberValuePair(value.getName().getSymbolName(), + convertedValue); + } + + if (value instanceof ArrayAttributeValue) { + final ArrayAttributeValue> castValue = (ArrayAttributeValue>) value; + + final List arrayElements = new ArrayList(); + for (final AnnotationAttributeValue v : castValue.getValue()) { + MemberValuePair converted = convert(v); + if (converted != null) { + arrayElements.add(converted.getValue()); + } + } + return new MemberValuePair(value.getName().getSymbolName(), + new ArrayInitializerExpr(arrayElements)); + } + + throw new UnsupportedOperationException("Unsupported attribute value '" + + value.getName() + "' of type '" + value.getClass().getName() + + "'"); + } + + public static JavaParserAnnotationMetadataBuilder getInstance( + final AnnotationExpr annotationExpr, + final CompilationUnitServices compilationUnitServices) { + return new JavaParserAnnotationMetadataBuilder(annotationExpr, + compilationUnitServices); + } + + private final JavaType annotationType; + + private final List> attributeValues; + + /** + * Factory method + * + * @param annotationExpr + * @param compilationUnitServices + * @return a non-null instance + * @since 1.2.0 + */ + private JavaParserAnnotationMetadataBuilder( + final AnnotationExpr annotationExpr, + final CompilationUnitServices compilationUnitServices) { + Validate.notNull(annotationExpr, "Annotation expression required"); + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + + // Obtain the annotation type name from the assorted types of + // annotations we might have received (ie marker annotations, single + // member annotations, normal annotations etc) + final NameExpr nameToFind = JavaParserUtils.getNameExpr(annotationExpr); + + // Compute the actual annotation type, having regard to the compilation + // unit package and imports + annotationType = JavaParserUtils.getJavaType(compilationUnitServices, + nameToFind, null); + + // Generate some member-value pairs for subsequent parsing + List annotationPairs = new ArrayList(); + if (annotationExpr instanceof MarkerAnnotationExpr) { + // A marker annotation has no values, so we can have no pairs to add + } + else if (annotationExpr instanceof SingleMemberAnnotationExpr) { + final SingleMemberAnnotationExpr a = (SingleMemberAnnotationExpr) annotationExpr; + // Add the "value=" member-value pair. + if (a.getMemberValue() != null) { + annotationPairs.add(new MemberValuePair("value", a + .getMemberValue())); + } + } + else if (annotationExpr instanceof NormalAnnotationExpr) { + final NormalAnnotationExpr a = (NormalAnnotationExpr) annotationExpr; + // Must iterate over the expressions + if (a.getPairs() != null) { + annotationPairs = a.getPairs(); + } + } + + // Iterate over the annotation attributes, creating our parsed + // attributes map + final List> attributeValues = new ArrayList>(); + for (final MemberValuePair p : annotationPairs) { + final JavaSymbolName annotationName = new JavaSymbolName( + p.getName()); + final AnnotationAttributeValue value = convert(annotationName, + p.getValue(), compilationUnitServices); + attributeValues.add(value); + } + this.attributeValues = attributeValues; + } + + public AnnotationMetadata build() { + final AnnotationMetadataBuilder annotationMetadataBuilder = new AnnotationMetadataBuilder( + annotationType, attributeValues); + return annotationMetadataBuilder.build(); + } + + private AnnotationAttributeValue convert(JavaSymbolName annotationName, + final Expression expression, + final CompilationUnitServices compilationUnitServices) { + if (annotationName == null) { + annotationName = new JavaSymbolName("__ARRAY_ELEMENT__"); + } + + if (expression instanceof AnnotationExpr) { + final AnnotationExpr annotationExpr = (AnnotationExpr) expression; + final AnnotationMetadata value = getInstance(annotationExpr, + compilationUnitServices).build(); + return new NestedAnnotationAttributeValue(annotationName, value); + } + + if (expression instanceof BooleanLiteralExpr) { + final boolean value = ((BooleanLiteralExpr) expression).getValue(); + return new BooleanAttributeValue(annotationName, value); + } + + if (expression instanceof CharLiteralExpr) { + final String value = ((CharLiteralExpr) expression).getValue(); + Validate.isTrue(value.length() == 1, + "Expected a char expression, but instead received '" + + value + "' for attribute '" + annotationName + + "'"); + final char c = value.charAt(0); + return new CharAttributeValue(annotationName, c); + } + + if (expression instanceof LongLiteralExpr) { + String value = ((LongLiteralExpr) expression).getValue(); + Validate.isTrue(value.toUpperCase().endsWith("L"), + "Expected long literal expression '" + value + + "' to end in 'l' or 'L'"); + value = value.substring(0, value.length() - 1); + final long l = new Long(value); + return new LongAttributeValue(annotationName, l); + } + + if (expression instanceof IntegerLiteralExpr) { + final String value = ((IntegerLiteralExpr) expression).getValue(); + final int i = new Integer(value); + return new IntegerAttributeValue(annotationName, i); + } + + if (expression instanceof DoubleLiteralExpr) { + String value = ((DoubleLiteralExpr) expression).getValue(); + boolean floatingPrecisionOnly = false; + if (value.toUpperCase().endsWith("F")) { + value = value.substring(0, value.length() - 1); + floatingPrecisionOnly = true; + } + if (value.toUpperCase().endsWith("D")) { + value = value.substring(0, value.length() - 1); + } + final double d = new Double(value); + return new DoubleAttributeValue(annotationName, d, + floatingPrecisionOnly); + } + + if (expression instanceof BinaryExpr) { + String result = ""; + BinaryExpr current = (BinaryExpr) expression; + while (current != null) { + String right = ""; + if (current.getRight() instanceof StringLiteralExpr) { + right = ((StringLiteralExpr) current.getRight()).getValue(); + } + else if (current.getRight() instanceof NameExpr) { + right = ((NameExpr) current.getRight()).getName(); + } + + result = right + result; + if (current.getLeft() instanceof StringLiteralExpr) { + final String left = ((StringLiteralExpr) current.getLeft()) + .getValue(); + result = left + result; + } + if (current.getLeft() instanceof BinaryExpr) { + current = (BinaryExpr) current.getLeft(); + } + else { + current = null; + } + } + return new StringAttributeValue(annotationName, result); + } + + if (expression instanceof StringLiteralExpr) { + final String value = ((StringLiteralExpr) expression).getValue(); + return new StringAttributeValue(annotationName, value); + } + + if (expression instanceof FieldAccessExpr) { + final FieldAccessExpr field = (FieldAccessExpr) expression; + final String fieldName = field.getField(); + + // Determine the type + final Expression scope = field.getScope(); + NameExpr nameToFind = null; + if (scope instanceof FieldAccessExpr) { + final FieldAccessExpr fScope = (FieldAccessExpr) scope; + nameToFind = JavaParserUtils.getNameExpr(fScope.toString()); + } + else if (scope instanceof NameExpr) { + nameToFind = (NameExpr) scope; + } + else { + throw new UnsupportedOperationException( + "A FieldAccessExpr for '" + + field.getScope() + + "' should return a NameExpr or FieldAccessExpr (was " + + field.getScope().getClass().getName() + ")"); + } + final JavaType fieldType = JavaParserUtils.getJavaType( + compilationUnitServices, nameToFind, null); + + final EnumDetails enumDetails = new EnumDetails(fieldType, + new JavaSymbolName(fieldName)); + return new EnumAttributeValue(annotationName, enumDetails); + } + + if (expression instanceof NameExpr) { + final NameExpr field = (NameExpr) expression; + final String name = field.getName(); + // As we have no way of finding out the real type + final JavaType fieldType = new JavaType("unknown.Object"); + final EnumDetails enumDetails = new EnumDetails(fieldType, + new JavaSymbolName(name)); + return new EnumAttributeValue(annotationName, enumDetails); + } + + if (expression instanceof ClassExpr) { + final ClassExpr clazz = (ClassExpr) expression; + final Type nameToFind = clazz.getType(); + final JavaType javaType = JavaParserUtils.getJavaType( + compilationUnitServices, nameToFind, null); + return new ClassAttributeValue(annotationName, javaType); + } + + if (expression instanceof ArrayInitializerExpr) { + final ArrayInitializerExpr castExp = (ArrayInitializerExpr) expression; + final List> arrayElements = new ArrayList>(); + for (final Expression e : castExp.getValues()) { + arrayElements.add(convert(null, e, compilationUnitServices)); + } + return new ArrayAttributeValue>( + annotationName, arrayElements); + } + + if (expression instanceof UnaryExpr) { + final UnaryExpr castExp = (UnaryExpr) expression; + if (castExp.getOperator() == Operator.negative) { + String value = castExp.toString(); + value = value.toUpperCase().endsWith("L") ? value.substring(0, + value.length() - 1) : value; + final long l = new Long(value); + return new LongAttributeValue(annotationName, l); + } + else { + throw new UnsupportedOperationException( + "Only negative operator in UnaryExpr is supported"); + } + } + + throw new UnsupportedOperationException( + "Unable to parse annotation attribute '" + annotationName + + "' due to unsupported annotation expression '" + + expression.getClass().getName() + "'"); + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserClassOrInterfaceTypeDetailsBuilder.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserClassOrInterfaceTypeDetailsBuilder.java new file mode 100644 index 000000000..21c6ea130 --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserClassOrInterfaceTypeDetailsBuilder.java @@ -0,0 +1,372 @@ +package org.springframework.roo.classpath.javaparser.details; + +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.ImportDeclaration; +import japa.parser.ast.body.BodyDeclaration; +import japa.parser.ast.body.ClassOrInterfaceDeclaration; +import japa.parser.ast.body.ConstructorDeclaration; +import japa.parser.ast.body.EnumConstantDeclaration; +import japa.parser.ast.body.EnumDeclaration; +import japa.parser.ast.body.FieldDeclaration; +import japa.parser.ast.body.MethodDeclaration; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.body.VariableDeclarator; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.expr.QualifiedNameExpr; +import japa.parser.ast.type.ClassOrInterfaceType; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ImportMetadataBuilder; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.javaparser.CompilationUnitServices; +import org.springframework.roo.classpath.javaparser.JavaParserUtils; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public class JavaParserClassOrInterfaceTypeDetailsBuilder implements + Builder { + + static final String UNSUPPORTED_MESSAGE_PREFIX = "Only enum, class and interface files are supported"; + + /** + * Factory method for this builder class + * + * @param compilationUnit + * @param enclosingCompilationUnitServices + * @param typeDeclaration + * @param declaredByMetadataId + * @param typeName + * @param metadataService + * @param typeLocationService + * @return a non-null builder + */ + public static JavaParserClassOrInterfaceTypeDetailsBuilder getInstance( + final CompilationUnit compilationUnit, + final CompilationUnitServices enclosingCompilationUnitServices, + final TypeDeclaration typeDeclaration, + final String declaredByMetadataId, final JavaType typeName, + final MetadataService metadataService, + final TypeLocationService typeLocationService) { + return new JavaParserClassOrInterfaceTypeDetailsBuilder( + compilationUnit, enclosingCompilationUnitServices, + typeDeclaration, declaredByMetadataId, typeName, + metadataService, typeLocationService); + } + + private final CompilationUnit compilationUnit; + private JavaPackage compilationUnitPackage; + private final CompilationUnitServices compilationUnitServices; + private final String declaredByMetadataId; + private List imports = new ArrayList(); + private final List innerTypes = new ArrayList(); + private final MetadataService metadataService; + + private JavaType name; + private PhysicalTypeCategory physicalTypeCategory; + private final TypeDeclaration typeDeclaration; + private final TypeLocationService typeLocationService; + + /** + * Constructor + * + * @param compilationUnit + * @param enclosingCompilationUnitServices + * @param typeDeclaration + * @param declaredByMetadataId + * @param typeName + * @param metadataService + * @param typeLocationService + */ + private JavaParserClassOrInterfaceTypeDetailsBuilder( + final CompilationUnit compilationUnit, + final CompilationUnitServices enclosingCompilationUnitServices, + final TypeDeclaration typeDeclaration, + final String declaredByMetadataId, final JavaType typeName, + final MetadataService metadataService, + final TypeLocationService typeLocationService) { + // Check + Validate.notNull(compilationUnit, "Compilation unit required"); + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(typeDeclaration, + "Unable to locate the class or interface declaration"); + Validate.notNull(typeName, "Name required"); + + // Assign + this.compilationUnit = compilationUnit; + compilationUnitServices = enclosingCompilationUnitServices == null ? getDefaultCompilationUnitServices() + : enclosingCompilationUnitServices; + this.declaredByMetadataId = declaredByMetadataId; + this.metadataService = metadataService; + name = typeName; + this.typeDeclaration = typeDeclaration; + this.typeLocationService = typeLocationService; + } + + public ClassOrInterfaceTypeDetails build() { + Validate.notEmpty(compilationUnit.getTypes(), + "No types in compilation unit, so unable to continue parsing"); + + ClassOrInterfaceDeclaration clazz = null; + EnumDeclaration enumClazz = null; + + final StringBuilder sb = new StringBuilder(compilationUnit.getPackage() + .getName().toString()); + if (name.getEnclosingType() != null) { + sb.append(".").append(name.getEnclosingType().getSimpleTypeName()); + } + compilationUnitPackage = new JavaPackage(sb.toString()); + + // Determine the type name, adding type parameters if possible + final JavaType newName = JavaParserUtils.getJavaType( + compilationUnitServices, typeDeclaration); + + // Revert back to the original type name (thus avoiding unnecessary + // inferences about java.lang types; see ROO-244) + name = new JavaType(newName.getFullyQualifiedTypeName(), + newName.getEnclosingType(), newName.getArray(), + newName.getDataType(), newName.getArgName(), + newName.getParameters()); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId); + + physicalTypeCategory = PhysicalTypeCategory.CLASS; + if (typeDeclaration instanceof ClassOrInterfaceDeclaration) { + clazz = (ClassOrInterfaceDeclaration) typeDeclaration; + if (clazz.isInterface()) { + physicalTypeCategory = PhysicalTypeCategory.INTERFACE; + } + + } + else if (typeDeclaration instanceof EnumDeclaration) { + enumClazz = (EnumDeclaration) typeDeclaration; + physicalTypeCategory = PhysicalTypeCategory.ENUMERATION; + } + + Validate.notNull(physicalTypeCategory, UNSUPPORTED_MESSAGE_PREFIX + + " (" + typeDeclaration.getClass().getSimpleName() + " for " + + name + ")"); + + cidBuilder.setName(name); + cidBuilder.setPhysicalTypeCategory(physicalTypeCategory); + + imports = compilationUnit.getImports(); + if (imports == null) { + imports = new ArrayList(); + compilationUnit.setImports(imports); + } + + // Verify the package declaration appears to be correct + Validate.isTrue(compilationUnitPackage.equals(name.getPackage()), + "Compilation unit package '" + compilationUnitPackage + + "' unexpected for type '" + name.getPackage() + "'"); + + for (final ImportDeclaration importDeclaration : imports) { + if (importDeclaration.getName() instanceof QualifiedNameExpr) { + final String qualifier = ((QualifiedNameExpr) importDeclaration + .getName()).getQualifier().toString(); + final String simpleName = importDeclaration.getName().getName(); + final String fullName = qualifier + "." + simpleName; + // We want to calculate these... + + final JavaType type = new JavaType(fullName); + final JavaPackage typePackage = importDeclaration.isAsterisk() + ? new JavaPackage(fullName) : type.getPackage(); + final ImportMetadataBuilder newImport = new ImportMetadataBuilder( + declaredByMetadataId, 0, typePackage, type, + importDeclaration.isStatic(), + importDeclaration.isAsterisk()); + cidBuilder.add(newImport.build()); + } + } + + // Convert Java Parser modifier into JDK modifier + cidBuilder.setModifier(JavaParserUtils.getJdkModifier(typeDeclaration + .getModifiers())); + + // Type parameters + final Set typeParameterNames = new HashSet(); + for (final JavaType param : name.getParameters()) { + final JavaSymbolName arg = param.getArgName(); + // Fortunately type names can only appear at the top-level + if (arg != null && !JavaType.WILDCARD_NEITHER.equals(arg) + && !JavaType.WILDCARD_EXTENDS.equals(arg) + && !JavaType.WILDCARD_SUPER.equals(arg)) { + typeParameterNames.add(arg); + } + } + + List implementsList; + List annotationsList = null; + List members = null; + + if (clazz != null) { + final List extendsList = clazz.getExtends(); + if (extendsList != null) { + for (final ClassOrInterfaceType candidate : extendsList) { + final JavaType javaType = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, candidate, + typeParameterNames); + cidBuilder.addExtendsTypes(javaType); + } + } + + final List extendsTypes = cidBuilder.getExtendsTypes(); + // Obtain the superclass, if this is a class and one is available + if (physicalTypeCategory == PhysicalTypeCategory.CLASS + && extendsTypes.size() == 1) { + final JavaType superclass = extendsTypes.get(0); + final String superclassId = typeLocationService + .getPhysicalTypeIdentifier(superclass); + PhysicalTypeMetadata superPtm = null; + if (superclassId != null) { + superPtm = (PhysicalTypeMetadata) metadataService + .get(superclassId); + } + if (superPtm != null + && superPtm.getMemberHoldingTypeDetails() != null) { + cidBuilder.setSuperclass(superPtm + .getMemberHoldingTypeDetails()); + } + } + + implementsList = clazz.getImplements(); + if (implementsList != null) { + for (final ClassOrInterfaceType candidate : implementsList) { + final JavaType javaType = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, candidate, + typeParameterNames); + cidBuilder.addImplementsType(javaType); + } + } + + annotationsList = typeDeclaration.getAnnotations(); + members = clazz.getMembers(); + } + + if (enumClazz != null) { + final List constants = enumClazz + .getEntries(); + if (constants != null) { + for (final EnumConstantDeclaration enumConstants : constants) { + cidBuilder.addEnumConstant(new JavaSymbolName(enumConstants + .getName())); + } + } + + implementsList = enumClazz.getImplements(); + annotationsList = enumClazz.getAnnotations(); + members = enumClazz.getMembers(); + } + + if (annotationsList != null) { + for (final AnnotationExpr candidate : annotationsList) { + final AnnotationMetadata md = JavaParserAnnotationMetadataBuilder + .getInstance(candidate, compilationUnitServices) + .build(); + cidBuilder.addAnnotation(md); + } + } + + if (members != null) { + // Now we've finished declaring the type, we should introspect for + // any inner types that can thus be referred to in other body + // members + // We defer this until now because it's illegal to refer to an inner + // type in the signature of the enclosing type + for (final BodyDeclaration bodyDeclaration : members) { + if (bodyDeclaration instanceof TypeDeclaration) { + // Found a type + innerTypes.add((TypeDeclaration) bodyDeclaration); + } + } + + for (final BodyDeclaration member : members) { + if (member instanceof FieldDeclaration) { + final FieldDeclaration castMember = (FieldDeclaration) member; + for (final VariableDeclarator var : castMember + .getVariables()) { + final FieldMetadata field = JavaParserFieldMetadataBuilder + .getInstance(declaredByMetadataId, castMember, + var, compilationUnitServices, + typeParameterNames).build(); + cidBuilder.addField(field); + } + } + if (member instanceof MethodDeclaration) { + final MethodDeclaration castMember = (MethodDeclaration) member; + final MethodMetadata method = JavaParserMethodMetadataBuilder + .getInstance(declaredByMetadataId, castMember, + compilationUnitServices, typeParameterNames) + .build(); + cidBuilder.addMethod(method); + } + if (member instanceof ConstructorDeclaration) { + final ConstructorDeclaration castMember = (ConstructorDeclaration) member; + final ConstructorMetadata constructor = JavaParserConstructorMetadataBuilder + .getInstance(declaredByMetadataId, castMember, + compilationUnitServices, typeParameterNames) + .build(); + cidBuilder.addConstructor(constructor); + } + if (member instanceof TypeDeclaration) { + final TypeDeclaration castMember = (TypeDeclaration) member; + final JavaType innerType = new JavaType( + castMember.getName(), name); + final String innerTypeMetadataId = PhysicalTypeIdentifier + .createIdentifier(innerType, PhysicalTypeIdentifier + .getPath(declaredByMetadataId)); + final ClassOrInterfaceTypeDetails cid = new JavaParserClassOrInterfaceTypeDetailsBuilder( + compilationUnit, compilationUnitServices, + castMember, innerTypeMetadataId, innerType, + metadataService, typeLocationService).build(); + cidBuilder.addInnerType(cid); + } + } + } + + return cidBuilder.build(); + } + + private CompilationUnitServices getDefaultCompilationUnitServices() { + return new CompilationUnitServices() { + public JavaPackage getCompilationUnitPackage() { + return compilationUnitPackage; + } + + public JavaType getEnclosingTypeName() { + return name; + } + + public List getImports() { + return imports; + } + + public List getInnerTypes() { + return innerTypes; + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return physicalTypeCategory; + } + }; + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserConstructorMetadataBuilder.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserConstructorMetadataBuilder.java new file mode 100644 index 000000000..d70799edc --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserConstructorMetadataBuilder.java @@ -0,0 +1,324 @@ +package org.springframework.roo.classpath.javaparser.details; + +import japa.parser.JavaParser; +import japa.parser.ParseException; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.TypeParameter; +import japa.parser.ast.body.BodyDeclaration; +import japa.parser.ast.body.ConstructorDeclaration; +import japa.parser.ast.body.Parameter; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.body.VariableDeclaratorId; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.stmt.BlockStmt; +import japa.parser.ast.type.ClassOrInterfaceType; +import japa.parser.ast.type.Type; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.javaparser.CompilationUnitServices; +import org.springframework.roo.classpath.javaparser.JavaParserUtils; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Java Parser implementation of {@link ConstructorMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaParserConstructorMetadataBuilder implements + Builder { + + // TODO: Should parse the throws types from JavaParser source + + public static void addConstructor( + final CompilationUnitServices compilationUnitServices, + final List members, + final ConstructorMetadata constructor, + final Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(constructor, "Method required"); + + // Start with the basic constructor + final ConstructorDeclaration d = new ConstructorDeclaration(); + d.setModifiers(JavaParserUtils.getJavaParserModifier(constructor + .getModifier())); + d.setName(PhysicalTypeIdentifier.getJavaType( + constructor.getDeclaredByMetadataId()).getSimpleTypeName()); + + // Add any constructor-level annotations (not parameter annotations) + final List annotations = new ArrayList(); + d.setAnnotations(annotations); + for (final AnnotationMetadata annotation : constructor.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, annotation); + } + + // Add any constructor parameters, including their individual + // annotations and type parameters + final List parameters = new ArrayList(); + d.setParameters(parameters); + int index = -1; + for (final AnnotatedJavaType constructorParameter : constructor + .getParameterTypes()) { + index++; + + // Add the parameter annotations applicable for this parameter type + final List parameterAnnotations = new ArrayList(); + + for (final AnnotationMetadata parameterAnnotation : constructorParameter + .getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, parameterAnnotations, + parameterAnnotation); + } + + // Compute the parameter name + final String parameterName = constructor.getParameterNames() + .get(index).getSymbolName(); + + // Compute the parameter type + Type parameterType = null; + if (constructorParameter.getJavaType().isPrimitive()) { + parameterType = JavaParserUtils.getType(constructorParameter + .getJavaType()); + } + else { + final Type finalType = JavaParserUtils.getResolvedName( + constructorParameter.getJavaType(), + constructorParameter.getJavaType(), + compilationUnitServices); + final ClassOrInterfaceType cit = JavaParserUtils + .getClassOrInterfaceType(finalType); + + // Add any type arguments presented for the return type + if (constructorParameter.getJavaType().getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : constructorParameter + .getJavaType().getParameters()) { + // NameExpr importedParameterType = + // JavaParserUtils.importTypeIfRequired(compilationUnitServices.getEnclosingTypeName(), + // compilationUnitServices.getImports(), parameter); + // typeArgs.add(JavaParserUtils.getReferenceType(importedParameterType)); + typeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + + } + parameterType = finalType; + } + + // Create a Java Parser constructor parameter and add it to the list + // of parameters + final Parameter p = new Parameter(parameterType, + new VariableDeclaratorId(parameterName)); + p.setAnnotations(parameterAnnotations); + parameters.add(p); + } + + // Set the body + if (constructor.getBody() == null + || constructor.getBody().length() == 0) { + d.setBlock(new BlockStmt()); + } + else { + // There is a body. + // We need to make a fake constructor that we can have JavaParser + // parse. + // Easiest way to do that is to build a simple source class + // containing the required method and re-parse it. + final StringBuilder sb = new StringBuilder(); + sb.append("class TemporaryClass {\n"); + sb.append(" TemporaryClass() {\n"); + sb.append(constructor.getBody()); + sb.append("\n"); + sb.append(" }\n"); + sb.append("}\n"); + final ByteArrayInputStream bais = new ByteArrayInputStream(sb + .toString().getBytes()); + CompilationUnit ci; + try { + ci = JavaParser.parse(bais); + } + catch (final ParseException pe) { + throw new IllegalStateException( + "Illegal state: JavaParser did not parse correctly", pe); + } + final List types = ci.getTypes(); + if (types == null || types.size() != 1) { + throw new IllegalArgumentException("Method body invalid"); + } + final TypeDeclaration td = types.get(0); + final List bodyDeclarations = td.getMembers(); + if (bodyDeclarations == null || bodyDeclarations.size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return body declarations correctly"); + } + final BodyDeclaration bd = bodyDeclarations.get(0); + if (!(bd instanceof ConstructorDeclaration)) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a method declaration correctly"); + } + final ConstructorDeclaration cd = (ConstructorDeclaration) bd; + d.setBlock(cd.getBlock()); + } + + // Locate where to add this constructor; also verify if this method + // already exists + for (final BodyDeclaration bd : members) { + if (bd instanceof ConstructorDeclaration) { + // Next constructor should appear after this current constructor + final ConstructorDeclaration cd = (ConstructorDeclaration) bd; + if (cd.getParameters().size() == d.getParameters().size()) { + // Possible match, we need to consider parameter types as + // well now + final ConstructorMetadata constructorMetadata = new JavaParserConstructorMetadataBuilder( + constructor.getDeclaredByMetadataId(), cd, + compilationUnitServices, typeParameters).build(); + boolean matchesFully = true; + for (final AnnotatedJavaType existingParameter : constructorMetadata + .getParameterTypes()) { + if (!existingParameter.getJavaType().equals( + constructor.getParameterTypes().get(index))) { + matchesFully = false; + break; + } + } + if (matchesFully) { + throw new IllegalStateException("Constructor '" + + constructor.getParameterNames() + + "' already exists with identical parameters"); + } + } + } + } + + // Add the constructor to the end of the compilation unit + members.add(d); + } + + public static JavaParserConstructorMetadataBuilder getInstance( + final String declaredByMetadataId, + final ConstructorDeclaration constructorDeclaration, + final CompilationUnitServices compilationUnitServices, + final Set typeParameterNames) { + return new JavaParserConstructorMetadataBuilder(declaredByMetadataId, + constructorDeclaration, compilationUnitServices, + typeParameterNames); + } + + private final List annotations = new ArrayList(); + private String body; + private final String declaredByMetadataId; + private final int modifier; + private final List parameterNames = new ArrayList(); + + private final List parameterTypes = new ArrayList(); + + private final List throwsTypes = new ArrayList(); + + private JavaParserConstructorMetadataBuilder( + final String declaredByMetadataId, + final ConstructorDeclaration constructorDeclaration, + final CompilationUnitServices compilationUnitServices, + Set typeParameterNames) { + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(constructorDeclaration, + "Constructor declaration is mandatory"); + Validate.notNull(compilationUnitServices, + "Compilation unit services are required"); + + // Convert Java Parser modifier into JDK modifier + modifier = JavaParserUtils.getJdkModifier(constructorDeclaration + .getModifiers()); + + this.declaredByMetadataId = declaredByMetadataId; + + if (typeParameterNames == null) { + typeParameterNames = new HashSet(); + } + + // Add method-declared type parameters (if any) to the list of type + // parameters + final Set fullTypeParameters = new HashSet(); + fullTypeParameters.addAll(typeParameterNames); + final List params = constructorDeclaration + .getTypeParameters(); + if (params != null) { + for (final TypeParameter candidate : params) { + final JavaSymbolName currentTypeParam = new JavaSymbolName( + candidate.getName()); + fullTypeParameters.add(currentTypeParam); + } + } + + // Get the body + body = constructorDeclaration.getBlock().toString(); + body = StringUtils.replace(body, "{", "", 1); + body = body.substring(0, body.lastIndexOf("}")); + + // Lookup the parameters and their names + if (constructorDeclaration.getParameters() != null) { + for (final Parameter p : constructorDeclaration.getParameters()) { + final Type pt = p.getType(); + final JavaType parameterType = JavaParserUtils.getJavaType( + compilationUnitServices, pt, fullTypeParameters); + + final List annotationsList = p.getAnnotations(); + final List annotations = new ArrayList(); + if (annotationsList != null) { + for (final AnnotationExpr candidate : annotationsList) { + final JavaParserAnnotationMetadataBuilder md = JavaParserAnnotationMetadataBuilder + .getInstance(candidate, compilationUnitServices); + annotations.add(md.build()); + } + } + + parameterTypes.add(new AnnotatedJavaType(parameterType, + annotations)); + parameterNames.add(new JavaSymbolName(p.getId().getName())); + } + } + + if (constructorDeclaration.getAnnotations() != null) { + for (final AnnotationExpr annotation : constructorDeclaration + .getAnnotations()) { + annotations.add(JavaParserAnnotationMetadataBuilder + .getInstance(annotation, compilationUnitServices) + .build()); + } + } + } + + public ConstructorMetadata build() { + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + declaredByMetadataId); + constructorBuilder.setAnnotations(annotations); + constructorBuilder.setBodyBuilder(InvocableMemberBodyBuilder + .getInstance().append(body)); + constructorBuilder.setModifier(modifier); + constructorBuilder.setParameterNames(parameterNames); + constructorBuilder.setParameterTypes(parameterTypes); + constructorBuilder.setThrowsTypes(throwsTypes); + return constructorBuilder.build(); + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserFieldMetadataBuilder.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserFieldMetadataBuilder.java new file mode 100644 index 000000000..2dd5f642d --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserFieldMetadataBuilder.java @@ -0,0 +1,299 @@ +package org.springframework.roo.classpath.javaparser.details; + +import japa.parser.ASTHelper; +import japa.parser.JavaParser; +import japa.parser.ParseException; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.body.BodyDeclaration; +import japa.parser.ast.body.FieldDeclaration; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.body.VariableDeclarator; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.expr.Expression; +import japa.parser.ast.expr.NameExpr; +import japa.parser.ast.expr.ObjectCreationExpr; +import japa.parser.ast.type.ClassOrInterfaceType; +import japa.parser.ast.type.Type; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.javaparser.CompilationUnitServices; +import org.springframework.roo.classpath.javaparser.JavaParserUtils; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Java Parser implementation of {@link FieldMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaParserFieldMetadataBuilder implements Builder { + + public static void addField( + final CompilationUnitServices compilationUnitServices, + final List members, final FieldMetadata field) { + Validate.notNull(compilationUnitServices, + "Flushable compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(field, "Field required"); + + JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), field.getFieldType()); + final Type initType = JavaParserUtils.getResolvedName( + compilationUnitServices.getEnclosingTypeName(), + field.getFieldType(), compilationUnitServices); + ClassOrInterfaceType finalType = JavaParserUtils + .getClassOrInterfaceType(initType); + + final FieldDeclaration newField = ASTHelper.createFieldDeclaration( + JavaParserUtils.getJavaParserModifier(field.getModifier()), + initType, field.getFieldName().getSymbolName()); + + // Add parameterized types for the field type (not initializer) + if (field.getFieldType().getParameters().size() > 0) { + final List fieldTypeArgs = new ArrayList(); + finalType.setTypeArgs(fieldTypeArgs); + for (final JavaType parameter : field.getFieldType() + .getParameters()) { + fieldTypeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + } + + final List vars = newField.getVariables(); + Validate.notEmpty(vars, + "Expected ASTHelper to have provided a single VariableDeclarator"); + Validate.isTrue(vars.size() == 1, + "Expected ASTHelper to have provided a single VariableDeclarator"); + final VariableDeclarator vd = vars.iterator().next(); + + if (StringUtils.isNotBlank(field.getFieldInitializer())) { + // There is an initializer. + // We need to make a fake field that we can have JavaParser parse. + // Easiest way to do that is to build a simple source class + // containing the required field and re-parse it. + final StringBuilder sb = new StringBuilder(); + sb.append("class TemporaryClass {\n"); + sb.append(" private " + field.getFieldType() + " " + + field.getFieldName() + " = " + + field.getFieldInitializer() + ";\n"); + sb.append("}\n"); + final ByteArrayInputStream bais = new ByteArrayInputStream(sb + .toString().getBytes()); + CompilationUnit ci; + try { + ci = JavaParser.parse(bais); + } + catch (final ParseException pe) { + throw new IllegalStateException( + "Illegal state: JavaParser did not parse correctly", pe); + } + final List types = ci.getTypes(); + if (types == null || types.size() != 1) { + throw new IllegalArgumentException("Field member invalid"); + } + final TypeDeclaration td = types.get(0); + final List bodyDeclarations = td.getMembers(); + if (bodyDeclarations == null || bodyDeclarations.size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return body declarations correctly"); + } + final BodyDeclaration bd = bodyDeclarations.get(0); + if (!(bd instanceof FieldDeclaration)) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a field declaration correctly"); + } + final FieldDeclaration fd = (FieldDeclaration) bd; + if (fd.getVariables() == null || fd.getVariables().size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a field declaration correctly"); + } + + final Expression init = fd.getVariables().get(0).getInit(); + + // Resolve imports (ROO-1505) + if (init instanceof ObjectCreationExpr) { + final ObjectCreationExpr ocr = (ObjectCreationExpr) init; + final JavaType typeToImport = JavaParserUtils.getJavaTypeNow( + compilationUnitServices, ocr.getType(), null); + final NameExpr nameExpr = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), typeToImport); + final ClassOrInterfaceType classOrInterfaceType = JavaParserUtils + .getClassOrInterfaceType(nameExpr); + ocr.setType(classOrInterfaceType); + + if (typeToImport.getParameters().size() > 0) { + final List initTypeArgs = new ArrayList(); + finalType.setTypeArgs(initTypeArgs); + for (final JavaType parameter : typeToImport + .getParameters()) { + initTypeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + classOrInterfaceType.setTypeArgs(initTypeArgs); + } + } + + vd.setInit(init); + } + + // Add annotations + final List annotations = new ArrayList(); + newField.setAnnotations(annotations); + for (final AnnotationMetadata annotation : field.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, annotation); + } + + // Locate where to add this field; also verify if this field already + // exists + int nextFieldIndex = 0; + int i = -1; + for (final BodyDeclaration bd : members) { + i++; + if (bd instanceof FieldDeclaration) { + // Next field should appear after this current field + nextFieldIndex = i + 1; + final FieldDeclaration bdf = (FieldDeclaration) bd; + for (final VariableDeclarator v : bdf.getVariables()) { + Validate.isTrue(!field.getFieldName().getSymbolName() + .equals(v.getId().getName()), "A field with name '" + + field.getFieldName().getSymbolName() + + "' already exists"); + } + } + } + + // Add the field to the compilation unit + members.add(nextFieldIndex, newField); + } + + public static JavaParserFieldMetadataBuilder getInstance( + final String declaredByMetadataId, + final FieldDeclaration fieldDeclaration, + final VariableDeclarator var, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + return new JavaParserFieldMetadataBuilder(declaredByMetadataId, + fieldDeclaration, var, compilationUnitServices, typeParameters); + } + + public static void removeField( + final CompilationUnitServices compilationUnitServices, + final List members, final JavaSymbolName fieldName) { + Validate.notNull(compilationUnitServices, + "Flushable compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(fieldName, "Field name to remove is required"); + + // Locate the field + int i = -1; + int toDelete = -1; + for (final BodyDeclaration bd : members) { + i++; + if (bd instanceof FieldDeclaration) { + final FieldDeclaration fieldDeclaration = (FieldDeclaration) bd; + for (final VariableDeclarator var : fieldDeclaration + .getVariables()) { + if (var.getId().getName().equals(fieldName.getSymbolName())) { + toDelete = i; + break; + } + } + } + } + + Validate.isTrue(toDelete > -1, "Could not locate field '%s' to delete", + fieldName); + + // Do removal outside iteration of body declaration members, to avoid + // concurrent modification exceptions + members.remove(toDelete); + } + + private final List annotations = new ArrayList(); + private final String declaredByMetadataId; + private String fieldInitializer; + + private final JavaSymbolName fieldName; + + private JavaType fieldType; + + private final int modifier; + + private JavaParserFieldMetadataBuilder(final String declaredByMetadataId, + final FieldDeclaration fieldDeclaration, + final VariableDeclarator var, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + Validate.notNull(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(fieldDeclaration, "Field declaration is mandatory"); + Validate.notNull(var, "Variable declarator required"); + Validate.isTrue(fieldDeclaration.getVariables().contains(var), + "Cannot request a variable not already in the field declaration"); + Validate.notNull(compilationUnitServices, + "Compilation unit services are required"); + + // Convert Java Parser modifier into JDK modifier + modifier = JavaParserUtils.getJdkModifier(fieldDeclaration + .getModifiers()); + + this.declaredByMetadataId = declaredByMetadataId; + + final Type type = fieldDeclaration.getType(); + fieldType = JavaParserUtils.getJavaType(compilationUnitServices, type, + typeParameters); + + // Convert into an array if this variable ID uses array notation + if (var.getId().getArrayCount() > 0) { + fieldType = new JavaType(fieldType.getFullyQualifiedTypeName(), var + .getId().getArrayCount() + fieldType.getArray(), + fieldType.getDataType(), fieldType.getArgName(), + fieldType.getParameters()); + } + + fieldName = new JavaSymbolName(var.getId().getName()); + + // Lookup initializer, if one was requested and easily determinable + final Expression e = var.getInit(); + if (e != null) { + fieldInitializer = e.toString(); + } + + final List annotations = fieldDeclaration + .getAnnotations(); + if (annotations != null) { + for (final AnnotationExpr annotation : annotations) { + this.annotations.add(JavaParserAnnotationMetadataBuilder + .getInstance(annotation, compilationUnitServices) + .build()); + } + } + } + + public FieldMetadata build() { + final FieldMetadataBuilder fieldMetadataBuilder = new FieldMetadataBuilder( + declaredByMetadataId); + fieldMetadataBuilder.setAnnotations(annotations); + fieldMetadataBuilder.setFieldInitializer(fieldInitializer); + fieldMetadataBuilder.setFieldName(fieldName); + fieldMetadataBuilder.setFieldType(fieldType); + fieldMetadataBuilder.setModifier(modifier); + return fieldMetadataBuilder.build(); + } +} diff --git a/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserMethodMetadataBuilder.java b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserMethodMetadataBuilder.java new file mode 100644 index 000000000..d68bb14b9 --- /dev/null +++ b/classpath-javaparser/src/main/java/org/springframework/roo/classpath/javaparser/details/JavaParserMethodMetadataBuilder.java @@ -0,0 +1,425 @@ +package org.springframework.roo.classpath.javaparser.details; + +import japa.parser.JavaParser; +import japa.parser.ParseException; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.TypeParameter; +import japa.parser.ast.body.BodyDeclaration; +import japa.parser.ast.body.MethodDeclaration; +import japa.parser.ast.body.Parameter; +import japa.parser.ast.body.TypeDeclaration; +import japa.parser.ast.body.VariableDeclaratorId; +import japa.parser.ast.expr.AnnotationExpr; +import japa.parser.ast.expr.NameExpr; +import japa.parser.ast.stmt.BlockStmt; +import japa.parser.ast.type.ClassOrInterfaceType; +import japa.parser.ast.type.ReferenceType; +import japa.parser.ast.type.Type; + +import java.io.ByteArrayInputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.classpath.javaparser.CompilationUnitServices; +import org.springframework.roo.classpath.javaparser.JavaParserUtils; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Java Parser implementation of {@link MethodMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaParserMethodMetadataBuilder implements Builder { + + public static void addMethod( + final CompilationUnitServices compilationUnitServices, + final List members, final MethodMetadata method, + Set typeParameters) { + Validate.notNull(compilationUnitServices, + "Flushable compilation unit services required"); + Validate.notNull(members, "Members required"); + Validate.notNull(method, "Method required"); + + if (typeParameters == null) { + typeParameters = new HashSet(); + } + + // Create the return type we should use + Type returnType = null; + if (method.getReturnType().isPrimitive()) { + returnType = JavaParserUtils.getType(method.getReturnType()); + } + else { + final NameExpr importedType = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + method.getReturnType()); + final ClassOrInterfaceType cit = JavaParserUtils + .getClassOrInterfaceType(importedType); + + // Add any type arguments presented for the return type + if (method.getReturnType().getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : method.getReturnType() + .getParameters()) { + typeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + } + + // Handle arrays + if (method.getReturnType().isArray()) { + final ReferenceType rt = new ReferenceType(); + rt.setArrayCount(method.getReturnType().getArray()); + rt.setType(cit); + returnType = rt; + } + else { + returnType = cit; + } + } + + // Start with the basic method + final MethodDeclaration d = new MethodDeclaration(); + d.setModifiers(JavaParserUtils.getJavaParserModifier(method + .getModifier())); + d.setName(method.getMethodName().getSymbolName()); + d.setType(returnType); + + // Add any method-level annotations (not parameter annotations) + final List annotations = new ArrayList(); + d.setAnnotations(annotations); + for (final AnnotationMetadata annotation : method.getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, annotations, annotation); + } + + // Add any method parameters, including their individual annotations and + // type parameters + final List parameters = new ArrayList(); + d.setParameters(parameters); + + int index = -1; + for (final AnnotatedJavaType methodParameter : method + .getParameterTypes()) { + index++; + + // Add the parameter annotations applicable for this parameter type + final List parameterAnnotations = new ArrayList(); + + for (final AnnotationMetadata parameterAnnotation : methodParameter + .getAnnotations()) { + JavaParserAnnotationMetadataBuilder.addAnnotationToList( + compilationUnitServices, parameterAnnotations, + parameterAnnotation); + } + + // Compute the parameter name + final String parameterName = method.getParameterNames().get(index) + .getSymbolName(); + + // Compute the parameter type + Type parameterType = null; + if (methodParameter.getJavaType().isPrimitive()) { + parameterType = JavaParserUtils.getType(methodParameter + .getJavaType()); + } + else { + final NameExpr type = JavaParserUtils.importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), + methodParameter.getJavaType()); + final ClassOrInterfaceType cit = JavaParserUtils + .getClassOrInterfaceType(type); + + // Add any type arguments presented for the return type + if (methodParameter.getJavaType().getParameters().size() > 0) { + final List typeArgs = new ArrayList(); + cit.setTypeArgs(typeArgs); + for (final JavaType parameter : methodParameter + .getJavaType().getParameters()) { + typeArgs.add(JavaParserUtils.importParametersForType( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), parameter)); + } + } + + // Handle arrays + if (methodParameter.getJavaType().isArray()) { + final ReferenceType rt = new ReferenceType(); + rt.setArrayCount(methodParameter.getJavaType().getArray()); + rt.setType(cit); + parameterType = rt; + } + else { + parameterType = cit; + } + } + + // Create a Java Parser method parameter and add it to the list of + // parameters + final Parameter p = new Parameter(parameterType, + new VariableDeclaratorId(parameterName)); + p.setVarArgs(methodParameter.isVarArgs()); + p.setAnnotations(parameterAnnotations); + parameters.add(p); + } + + // Add exceptions which the method my throw + if (method.getThrowsTypes().size() > 0) { + final List throwsTypes = new ArrayList(); + for (final JavaType javaType : method.getThrowsTypes()) { + final NameExpr importedType = JavaParserUtils + .importTypeIfRequired( + compilationUnitServices.getEnclosingTypeName(), + compilationUnitServices.getImports(), javaType); + throwsTypes.add(importedType); + } + d.setThrows(throwsTypes); + } + + // Set the body + if (StringUtils.isBlank(method.getBody())) { + // Never set the body if an abstract method + if (!Modifier.isAbstract(method.getModifier()) + && !PhysicalTypeCategory.INTERFACE + .equals(compilationUnitServices + .getPhysicalTypeCategory())) { + d.setBody(new BlockStmt()); + } + } + else { + // There is a body. + // We need to make a fake method that we can have JavaParser parse. + // Easiest way to do that is to build a simple source class + // containing the required method and re-parse it. + final StringBuilder sb = new StringBuilder(); + sb.append("class TemporaryClass {\n"); + sb.append(" public void temporaryMethod() {\n"); + sb.append(method.getBody()); + sb.append("\n"); + sb.append(" }\n"); + sb.append("}\n"); + final ByteArrayInputStream bais = new ByteArrayInputStream(sb + .toString().getBytes()); + CompilationUnit ci; + try { + ci = JavaParser.parse(bais); + } + catch (final ParseException pe) { + throw new IllegalStateException( + "Illegal state: JavaParser did not parse correctly", pe); + } + final List types = ci.getTypes(); + if (types == null || types.size() != 1) { + throw new IllegalArgumentException("Method body invalid"); + } + final TypeDeclaration td = types.get(0); + final List bodyDeclarations = td.getMembers(); + if (bodyDeclarations == null || bodyDeclarations.size() != 1) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return body declarations correctly"); + } + final BodyDeclaration bd = bodyDeclarations.get(0); + if (!(bd instanceof MethodDeclaration)) { + throw new IllegalStateException( + "Illegal state: JavaParser did not return a method declaration correctly"); + } + final MethodDeclaration md = (MethodDeclaration) bd; + d.setBody(md.getBody()); + } + + // Locate where to add this method; also verify if this method already + // exists + for (final BodyDeclaration bd : members) { + if (bd instanceof MethodDeclaration) { + // Next method should appear after this current method + final MethodDeclaration md = (MethodDeclaration) bd; + if (md.getName().equals(d.getName())) { + if ((md.getParameters() == null || md.getParameters() + .isEmpty()) + && (d.getParameters() == null || d.getParameters() + .isEmpty())) { + throw new IllegalStateException("Method '" + + method.getMethodName().getSymbolName() + + "' already exists"); + } + else if (md.getParameters() != null + && md.getParameters().size() == d.getParameters() + .size()) { + // Possible match, we need to consider parameter types + // as well now + final MethodMetadata methodMetadata = JavaParserMethodMetadataBuilder + .getInstance(method.getDeclaredByMetadataId(), + md, compilationUnitServices, + typeParameters).build(); + boolean matchesFully = true; + index = -1; + for (final AnnotatedJavaType existingParameter : methodMetadata + .getParameterTypes()) { + index++; + final AnnotatedJavaType parameterType = method + .getParameterTypes().get(index); + if (!existingParameter.getJavaType().equals( + parameterType.getJavaType())) { + matchesFully = false; + break; + } + } + if (matchesFully) { + throw new IllegalStateException( + "Method '" + + method.getMethodName() + .getSymbolName() + + "' already exists with identical parameters"); + } + } + } + } + } + + // Add the method to the end of the compilation unit + members.add(d); + } + + public static JavaParserMethodMetadataBuilder getInstance( + final String declaredByMetadataId, + final MethodDeclaration methodDeclaration, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + return new JavaParserMethodMetadataBuilder(declaredByMetadataId, + methodDeclaration, compilationUnitServices, typeParameters); + } + + private final List annotations = new ArrayList(); + private String body; + private final String declaredByMetadataId; + private final JavaSymbolName methodName; + private final int modifier; + private final List parameterNames = new ArrayList(); + private final List parameterTypes = new ArrayList(); + + private final JavaType returnType; + + private final List throwsTypes = new ArrayList(); + + private JavaParserMethodMetadataBuilder(final String declaredByMetadataId, + final MethodDeclaration methodDeclaration, + final CompilationUnitServices compilationUnitServices, + final Set typeParameters) { + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(methodDeclaration, "Method declaration is mandatory"); + Validate.notNull(compilationUnitServices, + "Compilation unit services are required"); + + this.declaredByMetadataId = declaredByMetadataId; + + // Convert Java Parser modifier into JDK modifier + modifier = JavaParserUtils.getJdkModifier(methodDeclaration + .getModifiers()); + + // Add method-declared type parameters (if any) to the list of type + // parameters + final Set fullTypeParameters = new HashSet(); + fullTypeParameters.addAll(typeParameters); + final List params = methodDeclaration + .getTypeParameters(); + if (params != null) { + for (final TypeParameter candidate : params) { + final JavaSymbolName currentTypeParam = new JavaSymbolName( + candidate.getName()); + fullTypeParameters.add(currentTypeParam); + } + } + + // Compute the return type + final Type rt = methodDeclaration.getType(); + returnType = JavaParserUtils.getJavaType(compilationUnitServices, rt, + fullTypeParameters); + + // Compute the method name + methodName = new JavaSymbolName(methodDeclaration.getName()); + + // Get the body + body = methodDeclaration.getBody() == null ? null : methodDeclaration + .getBody().toString(); + if (body != null) { + body = StringUtils.replace(body, "{", "", 1); + body = body.substring(0, body.lastIndexOf("}")); + } + + // Lookup the parameters and their names + if (methodDeclaration.getParameters() != null) { + for (final Parameter p : methodDeclaration.getParameters()) { + final Type pt = p.getType(); + final JavaType parameterType = JavaParserUtils.getJavaType( + compilationUnitServices, pt, fullTypeParameters); + final List annotationsList = p.getAnnotations(); + final List annotations = new ArrayList(); + if (annotationsList != null) { + for (final AnnotationExpr candidate : annotationsList) { + final AnnotationMetadata annotationMetadata = JavaParserAnnotationMetadataBuilder + .getInstance(candidate, compilationUnitServices) + .build(); + annotations.add(annotationMetadata); + } + } + final AnnotatedJavaType param = new AnnotatedJavaType( + parameterType, annotations); + param.setVarArgs(p.isVarArgs()); + parameterTypes.add(param); + parameterNames.add(new JavaSymbolName(p.getId().getName())); + } + } + + if (methodDeclaration.getThrows() != null) { + for (final NameExpr throwsType : methodDeclaration.getThrows()) { + final JavaType throwing = JavaParserUtils + .getJavaType(compilationUnitServices, throwsType, + fullTypeParameters); + throwsTypes.add(throwing); + } + } + + if (methodDeclaration.getAnnotations() != null) { + for (final AnnotationExpr annotation : methodDeclaration + .getAnnotations()) { + annotations.add(JavaParserAnnotationMetadataBuilder + .getInstance(annotation, compilationUnitServices) + .build()); + } + } + } + + public MethodMetadata build() { + final MethodMetadataBuilder methodMetadataBuilder = new MethodMetadataBuilder( + declaredByMetadataId); + methodMetadataBuilder.setMethodName(methodName); + methodMetadataBuilder.setReturnType(returnType); + methodMetadataBuilder.setAnnotations(annotations); + methodMetadataBuilder.setBodyBuilder(InvocableMemberBodyBuilder + .getInstance().append(body)); + methodMetadataBuilder.setModifier(modifier); + methodMetadataBuilder.setParameterNames(parameterNames); + methodMetadataBuilder.setParameterTypes(parameterTypes); + methodMetadataBuilder.setThrowsTypes(throwsTypes); + return methodMetadataBuilder.build(); + } +} diff --git a/classpath-javaparser/src/test/java/org/springframework/roo/classpath/javaparser/JavaParserTypeParsingServiceTest.java b/classpath-javaparser/src/test/java/org/springframework/roo/classpath/javaparser/JavaParserTypeParsingServiceTest.java new file mode 100644 index 000000000..6259d6479 --- /dev/null +++ b/classpath-javaparser/src/test/java/org/springframework/roo/classpath/javaparser/JavaParserTypeParsingServiceTest.java @@ -0,0 +1,117 @@ +package org.springframework.roo.classpath.javaparser; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import japa.parser.JavaParser; +import japa.parser.ast.CompilationUnit; +import japa.parser.ast.body.TypeDeclaration; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.javaparser.details.JavaParserClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link JavaParserTypeParsingService} + * + * @author Andrew Swan + * @since 1.2.0 + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ JavaParserClassOrInterfaceTypeDetailsBuilder.class, + JavaParser.class, JavaParserUtils.class }) +public class JavaParserTypeParsingServiceTest { + + private static final String DECLARED_BY_MID = "MID:foo#bar"; + private static final String EMPTY_FILE = "package com.example;"; + + private static final String SOURCE_FILE = "package com.example;" + "" + + "public class MyClass {}" + "" + "class TargetClass {}" + "" + + "class OtherClass {}"; + @Mock private MetadataService mockMetadataService; + @Mock private TypeLocationService mockTypeLocationService; + + // Fixture + private JavaParserTypeParsingService typeParsingService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + typeParsingService = new JavaParserTypeParsingService(); + typeParsingService.metadataService = mockMetadataService; + typeParsingService.typeLocationService = mockTypeLocationService; + } + + @Test + public void testGetTypeFromStringWhenFileContainsNoSuchType() { + // Set up + final JavaType mockTargetType = mock(JavaType.class); + when(mockTargetType.getSimpleTypeName()).thenReturn("NoSuchType"); + + // Invoke + final ClassOrInterfaceTypeDetails locatedType = typeParsingService + .getTypeFromString(SOURCE_FILE, DECLARED_BY_MID, mockTargetType); + + // Check + assertNull(locatedType); + } + + @Test + public void testGetTypeFromStringWhenFileContainsNoTypes() { + // Set up + final JavaType mockTargetType = mock(JavaType.class); + + // Invoke + final ClassOrInterfaceTypeDetails locatedType = typeParsingService + .getTypeFromString(EMPTY_FILE, DECLARED_BY_MID, mockTargetType); + + // Check + assertNull(locatedType); + } + + @Test + public void testGetTypeFromStringWhenFileContainsThatType() + throws Exception { + // Set up + final JavaType mockTargetType = mock(JavaType.class); + final TypeDeclaration mockTypeDeclaration = mock(TypeDeclaration.class); + final ClassOrInterfaceTypeDetails mockClassOrInterfaceTypeDetails = mock(ClassOrInterfaceTypeDetails.class); + final JavaParserClassOrInterfaceTypeDetailsBuilder mockBuilder = mock(JavaParserClassOrInterfaceTypeDetailsBuilder.class); + when(mockBuilder.build()).thenReturn(mockClassOrInterfaceTypeDetails); + + mockStatic(JavaParserUtils.class); + when( + JavaParserUtils.locateTypeDeclaration( + any(CompilationUnit.class), eq(mockTargetType))) + .thenReturn(mockTypeDeclaration); + + mockStatic(JavaParserClassOrInterfaceTypeDetailsBuilder.class); + when( + JavaParserClassOrInterfaceTypeDetailsBuilder.getInstance( + any(CompilationUnit.class), + (CompilationUnitServices) eq(null), + eq(mockTypeDeclaration), eq(DECLARED_BY_MID), + eq(mockTargetType), eq(mockMetadataService), + eq(mockTypeLocationService))).thenReturn(mockBuilder); + + // Invoke + final ClassOrInterfaceTypeDetails locatedType = typeParsingService + .getTypeFromString(SOURCE_FILE, DECLARED_BY_MID, mockTargetType); + + // Check + assertSame(mockClassOrInterfaceTypeDetails, locatedType); + } +} diff --git a/classpath-javaparser/src/test/java/org/springframework/roo/classpath/javaparser/UpdateCompilationUnitTest.java b/classpath-javaparser/src/test/java/org/springframework/roo/classpath/javaparser/UpdateCompilationUnitTest.java new file mode 100644 index 000000000..06f959d1b --- /dev/null +++ b/classpath-javaparser/src/test/java/org/springframework/roo/classpath/javaparser/UpdateCompilationUnitTest.java @@ -0,0 +1,620 @@ +package org.springframework.roo.classpath.javaparser; + +import static org.junit.Assert.assertTrue; +import static org.springframework.roo.model.JdkJavaType.SET; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.operations.Cardinality; +import org.springframework.roo.classpath.operations.jsr303.ReferenceField; +import org.springframework.roo.classpath.operations.jsr303.SetField; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Functional test of + * {@link JavaParserTypeParsingService#getCompilationUnitContents(org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails)} + * + * @author DiSiD Technologies + * @since 1.2.4 + */ +@RunWith(PowerMockRunner.class) +public class UpdateCompilationUnitTest { + + private static final String SIMPLE_INTERFACE_FILE_PATH = "SimpleInterface.java.test"; + private static final String SIMPLE_CLASS_FILE_PATH = "SimpleClass.java.test"; + private static final String SIMPLE_CLASS2_FILE_PATH = "SimpleClass2.java.test"; + private static final String SIMPLE_CLASS3_FILE_PATH = "SimpleClass3.java.test"; + private static final String ROO1505_CLASS_FILE_PATH = "Roo_1505.java.test"; + + private static final JavaType SIMPLE_INTERFACE_TYPE = new JavaType( + "org.myPackage.SimpleInterface"); + private static final JavaType SIMPLE_CLASS_TYPE = new JavaType( + "org.myPackage.SimpleClass"); + private static final JavaType SIMPLE_CLASS2_TYPE = new JavaType( + "org.myPackage.SimpleClass2"); + private static final JavaType SIMPLE_CLASS3_TYPE = new JavaType( + "org.myPackage.SimpleClass3"); + private static final JavaType ROO1505_CLASS_TYPE = new JavaType( + "com.pet.Roo_1505"); + + private static final String SIMPLE_INTERFACE_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#bar?SimpleInterface"; + private static final String SIMPLE_CLASS_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass"; + private static final String SIMPLE_CLASS2_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass2"; + private static final String SIMPLE_CLASS3_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?SimpleClass3"; + private static final String ROO1505_CLASS_DECLARED_BY_MID = "MID:org.springframework.roo.classpath.PhysicalTypeIdentifier#SRC_MAIN_JAVA?Roo_1505"; + + @Mock private MetadataService mockMetadataService; + @Mock private TypeLocationService mockTypeLocationService; + + // Fixture + private JavaParserTypeParsingService typeParsingService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + typeParsingService = new JavaParserTypeParsingService(); + typeParsingService.metadataService = mockMetadataService; + typeParsingService.typeLocationService = mockTypeLocationService; + } + + @Test + public void testSimpleInterfaceNoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_INTERFACE_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, + SIMPLE_INTERFACE_DECLARED_BY_MID, SIMPLE_INTERFACE_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimpleInterface(result); + } + + @Test + public void testSimpleClassNoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimpleClass(result); + } + + @Test + public void testSimpleClass2NoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS2_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS2_DECLARED_BY_MID, + SIMPLE_CLASS2_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimple2Class(result); + } + + @Test + public void testSimpleClass3NoChanges() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS3_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + checkSimple3Class(result); + } + + @Test + public void testSimpleClass3AddField() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS3_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + final SetField fieldDetails = new SetField( + SIMPLE_CLASS3_DECLARED_BY_MID, new JavaType( + SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(SIMPLE_CLASS3_TYPE)), + new JavaSymbolName("children"), SIMPLE_CLASS3_TYPE, + Cardinality.ONE_TO_MANY); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + fieldDetails.getPhysicalTypeIdentifier(), Modifier.PRIVATE, + new ArrayList(), + fieldDetails.getFieldName(), fieldDetails.getFieldType()); + fieldBuilder.setFieldInitializer("new HashSet()"); + + ClassOrInterfaceTypeDetails newClassDetails = addField( + simpleInterfaceDetails, fieldBuilder.build()); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(newClassDetails); + + saveResult(file, result, "-addField"); + + checkSimple3Class(result); + + assertTrue(result + .contains("private Set children = new HashSet();")); + + // Add another + final ClassOrInterfaceTypeDetails simpleInterfaceDetails2 = typeParsingService + .getTypeFromString(result, SIMPLE_CLASS3_DECLARED_BY_MID, + SIMPLE_CLASS3_TYPE); + + final ReferenceField fieldDetails2 = new ReferenceField( + SIMPLE_CLASS3_DECLARED_BY_MID, SIMPLE_CLASS2_TYPE, + new JavaSymbolName("referenceField"), Cardinality.MANY_TO_ONE); + + final FieldMetadataBuilder fieldBuilder2 = new FieldMetadataBuilder( + fieldDetails2.getPhysicalTypeIdentifier(), Modifier.PRIVATE, + new ArrayList(), + fieldDetails2.getFieldName(), fieldDetails2.getFieldType()); + + ClassOrInterfaceTypeDetails newClassDetails2 = addField( + simpleInterfaceDetails2, fieldBuilder2.build()); + + // Invoke + final String result2 = typeParsingService + .getCompilationUnitContents(newClassDetails2); + + // Save to file for debug + saveResult(file, result2, "-addField2"); + + checkSimple3Class(result2); + + assertTrue(result + .contains("private Set children = new HashSet();")); + assertTrue(result2.contains("private SimpleClass2 referenceField;")); + + } + + @Test + public void testRegresion_ROO_1505() throws Exception { + // Set up + final File file = getResource(ROO1505_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, ROO1505_CLASS_DECLARED_BY_MID, + ROO1505_CLASS_TYPE); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result); + + check_ROO_1505_Class(result); + } + + @Test + public void testSimpleClassAddField() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + SIMPLE_CLASS_DECLARED_BY_MID, Modifier.PRIVATE, + new JavaSymbolName("newFieldAddedByCode"), new JavaType( + String.class), "\"Create by code\""); + final ClassOrInterfaceTypeDetails newSimpleInterfaceDetails = addField( + simpleInterfaceDetails, fieldBuilder.build()); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(newSimpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result, "-addedField"); + + checkSimpleClass(result); + + assertTrue(result + .contains("private String newFieldAddedByCode = \"Create by code\";")); + } + + @Test + public void testSimpleClassAddAnnotation() throws Exception { + // Set up + final File file = getResource(SIMPLE_CLASS_FILE_PATH); + final String fileContents = getResourceContents(file); + + final ClassOrInterfaceTypeDetails simpleInterfaceDetails = typeParsingService + .getTypeFromString(fileContents, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( + new JavaType( + "org.springframework.roo.addon.tostring.RooToString")); + final ClassOrInterfaceTypeDetails newSimpleInterfaceDetails = addAnnotation( + simpleInterfaceDetails, annotationBuilder.build()); + + // Invoke + final String result = typeParsingService + .getCompilationUnitContents(newSimpleInterfaceDetails); + + // Save to file for debug + saveResult(file, result, "-addedAnnotation"); + + checkSimpleClass(result); + + assertTrue(result + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result.contains("@RooToString")); + + // Invoke again + final ClassOrInterfaceTypeDetails simpleInterfaceDetails2 = typeParsingService + .getTypeFromString(result, SIMPLE_CLASS_DECLARED_BY_MID, + SIMPLE_CLASS_TYPE); + + final String result2 = typeParsingService + .getCompilationUnitContents(simpleInterfaceDetails2); + + // Save to file for debug + saveResult(file, result2, "-addedAnnotation2"); + + checkSimpleClass(result2); + + assertTrue(result2 + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result2.contains("@RooToString")); + + } + + public static ClassOrInterfaceTypeDetails addField( + final ClassOrInterfaceTypeDetails ptd, final FieldMetadata field) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + ptd); + cidBuilder.addField(field); + return cidBuilder.build(); + } + + public static ClassOrInterfaceTypeDetails addAnnotation( + final ClassOrInterfaceTypeDetails ptd, + final AnnotationMetadata annotation) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + ptd); + cidBuilder.addAnnotation(annotation); + return cidBuilder.build(); + } + + public static void checkSimpleClass(final String result) { + // check headers and import + // assertTrue(result.contains("* File header")); + assertTrue(result.contains("package org.myPackage;")); + // assertTrue(result.contains("// Simple comment1")); + assertTrue(result.contains("import test.importtest.pkg;")); + // assertTrue(result.contains("Comment about import")); + assertTrue(result + .contains("import static test.importtest.pkg.Hola.proofMethod;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public class SimpleClass")); + assertTrue(result.contains("extends OtheClass")); + assertTrue(result.contains("implements SimpleInterface")); + + // assertTrue(result.contains("== Comment in code ==")); + + // Check fields + // assertTrue(result.contains("* Javadoc for field")); + assertTrue(result.contains("private final String[] params;")); + assertTrue(result.contains("protected Double param1 = new Double(12);")); + assertTrue(result + .contains("private List[] listArray = new List[3];")); + assertTrue(result + .contains("Set[] setArray = new Set[] { null, null, null };")); + // assertTrue(result.contains("* Enum javaDoc")); + assertTrue(result.contains("public enum theNumbers")); + assertTrue(result.contains("uno, dos, tres")); + + // Check constructors + assertTrue(result.contains("@Documented(\"aaadbbbdd\")")); + // XXX Not supported Variable Args in Spring Rooo 'public + // SimpleClass(Double param1, String + // params)' + // assertTrue(result.contains("public SimpleClass(Double param1, String... params)")); + assertTrue(result + .contains("public SimpleClass(Double param1, String[] params, Set[] setArrayParam)")); + // assertTrue(result.contains("* Public constructor")); + // assertTrue(result.contains("// Comment in public constructor")); + assertTrue(result.contains("this.param1 = param1;")); + assertTrue(result.contains("this.params = params;")); + // assertTrue(result.contains("* Private constructor")); + assertTrue(result.contains("private SimpleClass()")); + // assertTrue(result.contains("* Comment in private constructor")); + assertTrue(result.contains("this.param1 = null;")); + // assertTrue(result.contains("// Comment inline1")); + // assertTrue(result.contains("// other comment between inline")); + assertTrue(result.contains("this.params = null;")); + // XXX JavaParser Bug + // assertTrue(result.contains("// Comment inline2")); + + // Check method hello declaration + // assertTrue(result.contains("* Javadoc of hello method")); + assertTrue(result + .contains("@Deprecated(message = \"Do not use\", more = \"Nothing\")")); + assertTrue(result.contains("@Override")); + assertTrue(result.contains("public Sting hello(String value)")); + + // Check method methodVoid + // assertTrue(result.contains("* methodVoid JavaDoc")); + assertTrue(result + .contains("void methodVoid(Double param1, String[] params, Map[] mapArrayParam)")); + // assertTrue(result.contains("// Comment before for")); + assertTrue(result + .contains("for (Map map : mapArrayParam)")); + // assertTrue(result.contains("// comment inside for")); + assertTrue(result.contains("map.isEmpty()")); + + // Check content method hello + // assertTrue(result.contains("Comment block inside method")); + // assertTrue(result.contains("Simple comment inside method")); + // assertTrue(result.contains("Simple comment inside method (2nd line)")); + assertTrue(result.contains("return \"Hello\";")); + + // Check private method declaration + // XXX Roo metadata doesn't support this + // assertTrue(result.contains(" Map")); + assertTrue(result.contains("Map")); + assertTrue(result.contains("privateMethod")); + assertTrue(result.contains("@ParameterAnnotation(\"xXX\")")); + // assertTrue(result.contains("Second method comment")); + + // Check subclass + // assertTrue(result.contains("* SubClass JavaDoc")); + assertTrue(result.contains("private static class SubClass")); + assertTrue(result.contains("String string1;")); + assertTrue(result.contains("final theNumbers enumValue = dos;")); + assertTrue(result.contains("int aInteger = 2;")); + assertTrue(result.contains("long aLong = 10l;")); + assertTrue(result.contains("SubClass(theNumbers enumValue)")); + assertTrue(result.contains("super();")); + // assertTrue(result.contains("// if comment")); + assertTrue(result.contains("if (enumValue.equals(this.enumValue))")); + // assertTrue(result.contains("// comment 'if' true")); + assertTrue(result.contains("string1 = \"equals\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("// comment inline 'if' true")); + assertTrue(result + .contains("else if (theNumbers.tres.equals(enumValue))")); + // assertTrue(result.contains("// comment 'elseif' true")); + assertTrue(result.contains("string1 = \"elseif\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("// comment inline elseif true")); + // assertTrue(result.contains("* comment in 'else'")); + assertTrue(result.contains("string1 = \"else\";")); + // XXX JavaParser Bug + // assertTrue(result.contains("* comment inline else")); + + // Check newList method + assertTrue(result + .contains("List>>> newList(List>> theList)")); + assertTrue(result + .contains("List>>> newListResult = new ArrayList>>>();")); + assertTrue(result.contains("newListResult.add(theList);")); + assertTrue(result.contains("return newListResult;")); + + } + + public static void checkSimple2Class(final String result) { + // check headers and import + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public class SimpleClass2")); + assertTrue(result.contains("extends OtheClass")); + assertTrue(result.contains("implements SimpleInterface")); + + // Check newList method + assertTrue(result + .contains("List>>> newList(List>> theList)")); + assertTrue(result + .contains("List>>> newListResult = new ArrayList>>>();")); + assertTrue(result.contains("newListResult.add(theList);")); + assertTrue(result.contains("return newListResult;")); + } + + public static void checkSimple3Class(final String result) { + // check headers and import + assertTrue(result.contains("package org.myPackage;")); + assertTrue(result.contains("import java.util.*;")); + + // check class declaration + assertTrue(result.contains("public class SimpleClass3")); + + // Check int + assertTrue(result.contains("int mInteger = 0;")); + assertTrue(result.contains("int[] mIntegerArray;")); + assertTrue(result.contains("int[][] mIntegerArray2;")); + assertTrue(result.contains("int[][][] mIntegerArray3;")); + + // Check byte + assertTrue(result.contains("byte mByte;")); + assertTrue(result.contains("byte[] mByteArray;")); + assertTrue(result.contains("byte[][] mByteArray2;")); + assertTrue(result.contains("byte[][][] mByteArray3;")); + + // Check Long + assertTrue(result.contains("Long mLongObject;")); + assertTrue(result.contains("Long[] mLongObjectArray;")); + assertTrue(result.contains("Long[][] mLongObjectArray2;")); + assertTrue(result.contains("Long[][][] mLongObjectArray3;")); + + // Check Set of Strings + assertTrue(result.contains("Set mSetString;")); + assertTrue(result.contains("Set[] mSetStringArray;")); + assertTrue(result.contains("Set[][] mSetStringArray2;")); + assertTrue(result.contains("Set[][][] mSetStringArray3;")); + + // Check Map of Strings and Doubles + assertTrue(result.contains("Map mMapStringDouble;")); + assertTrue(result + .contains("Map[] mMapStringDoubleArray;")); + assertTrue(result + .contains("Map[][] mMapStringDoubleArray2;")); + assertTrue(result + .contains("Map[][][] mMapStringDoubleArray3;")); + + // Check Map of Strings and Doubles + assertTrue(result + .contains("List>> mListMapStringIteratorDouble;")); + assertTrue(result + .contains("List>>[] mListMapStringIteratorDoubleArray;")); + assertTrue(result + .contains("List>>[][] mListMapStringIteratorDoubleArray2;")); + assertTrue(result + .contains("List>>[][][] mListMapStringIteratorDoubleArray3;")); + } + + public static void check_ROO_1505_Class(final String result) { + // Check package + assertTrue(result.contains("package com.pet;")); + + // Check imports + assertTrue(result.contains("import javax.persistence.Entity;")); + assertTrue(result + .contains("import org.springframework.roo.addon.javabean.RooJavaBean;")); + assertTrue(result + .contains("import org.springframework.roo.addon.tostring.RooToString;")); + assertTrue(result + .contains("import org.springframework.roo.addon.entity.RooEntity;")); + assertTrue(result + .contains("import javax.validation.constraints.NotNull;")); + assertTrue(result.contains("import java.util.Set;")); + assertTrue(result.contains("import javax.persistence.OneToMany;")); + assertTrue(result.contains("import javax.persistence.CascadeType;")); + + assertTrue(result.contains("@Entity")); + assertTrue(result.contains("@RooJavaBean")); + assertTrue(result.contains("@RooToString")); + assertTrue(result.contains("@RooEntity")); + assertTrue(result.contains("public class Roo_1505 {")); + assertTrue(result.contains("@NotNull")); + assertTrue(result.contains("private String name;")); + + assertTrue(result + .contains("@OneToMany(cascade = CascadeType.ALL, mappedBy = \"owner\")")); + assertTrue(result.contains("private Set pets = new HashSet();")); + + } + + public static void checkSimpleInterface(String result) { + // assertTrue(result.contains("* File header")); + assertTrue(result.contains("package org.myPackage;")); + // assertTrue(result.contains("// Simple comment1")); + assertTrue(result.contains("import test.importtest.pkg;")); + // assertTrue(result.contains("Comment about import")); + assertTrue(result + .contains("import static test.importtest.pkg.Hola.proofMethod;")); + // assertTrue(result.contains("* @author DiSiD Technologies")); + assertTrue(result.contains("public interface SimpleInterface")); + assertTrue(result + .contains("extends Comparable, Iterable")); + + // assertTrue(result.contains("* Javadoc of hello method")); + assertTrue(result.contains("@Deprecated")); + assertTrue(result.contains("String hello(String value);")); + } + + private File getResource(String pathname) { + URL res = this.getClass().getClassLoader().getResource(pathname); + return new File(res.getPath()); + } + + private String getResourceContents(String pathName) throws IOException { + return getResourceContents(getResource(pathName)); + } + + private String getResourceContents(File file) throws IOException { + return FileUtils.readFileToString(file); + } + + private void saveResult(File orgininalFile, String result, String suffix) + throws IOException { + if (suffix == null) { + suffix = ".update.result"; + } + else { + suffix = ".update" + suffix + ".result"; + } + final File resultFile = new File(orgininalFile.getParentFile(), + FilenameUtils.getName(orgininalFile.getName()) + suffix); + FileUtils.write(resultFile, result); + } + + private void saveResult(File orgininalFile, String result) + throws IOException { + saveResult(orgininalFile, result, null); + } + +} diff --git a/classpath-javaparser/src/test/resources/Roo_1505.java.test b/classpath-javaparser/src/test/resources/Roo_1505.java.test new file mode 100644 index 000000000..506543ac6 --- /dev/null +++ b/classpath-javaparser/src/test/resources/Roo_1505.java.test @@ -0,0 +1,23 @@ +package com.pet; + +import javax.persistence.Entity; +import org.springframework.roo.addon.javabean.RooJavaBean; +import org.springframework.roo.addon.tostring.RooToString; +import org.springframework.roo.addon.entity.RooEntity; +import javax.validation.constraints.NotNull; +import java.util.Set; +import javax.persistence.OneToMany; +import javax.persistence.CascadeType; + +@Entity +@RooJavaBean +@RooToString +@RooEntity +public class Roo_1505 { + + @NotNull + private String name; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") + private Set pets = new HashSet(); +} \ No newline at end of file diff --git a/classpath-javaparser/src/test/resources/SimpleClass.java.test b/classpath-javaparser/src/test/resources/SimpleClass.java.test new file mode 100644 index 000000000..04e3d73dc --- /dev/null +++ b/classpath-javaparser/src/test/resources/SimpleClass.java.test @@ -0,0 +1,130 @@ +/** + * File header + */ +package org.myPackage; + +// Simple comment1 + +import java.lang.annotation.Documented; + +import test.importtest.pkg; + +/** + * Comment about import + */ +import static test.importtest.pkg.Hola.proofMethod; +import java.util.*; + +/** + * @author DiSiD Technologies + */ +public class SimpleClass extends OtheClass implements SimpleInterface{ + + /* + * =========== Comment in code ================ + */ + + /** + * Javadoc for field + */ + private final String[] params; + protected Double param1 = new Double(12); + private List[] listArray = new List[3]; + Set[] setArray = new Set[] {null, null, null}; + + /** + * Enum javaDoc + */ + public enum theNumbers {uno, dos, tres}; + + /** + * Public constructor + */ + @Documented("aaadbbbdd") + // Not supported varing arguments public SimpleClass(Double param1, String...params) { + public SimpleClass(Double param1, String[] params, Set[] setArrayParam) { + // Comment in public constructor + this.param1 = param1; + this.params = params; + } + + /** + * methodVoid JavaDoc + */ + void methodVoid(Double param1, String[] params, Map[] mapArrayParam){ + // Comment before for + for (Map map : mapArrayParam){ + // comment inside for + map.isEmpty(); + } + } + + /** + * Private constructor + */ + private SimpleClass() { + /* + * Comment in private constructor + */ + this.param1 = null; // Comment inline1 + // other comment between inline + this.params = null; // Comment inline2 + } + + /** + * Javadoc of hello method + * + * @param value + * @return + */ + @Deprecated(message="Do not use",more="Nothing") + @Override + public Sting hello(String value){ + /* + * Comment block inside method + */ + + // Simple comment inside method + // Simple comment inside method (2nd line) + + + return "Hello"; + } + + private Map privateMethod(@ParameterAnnotation("xXX")T tValue, X xValue){ + // Second method comment + return null; + } + + /** + * SubClass JavaDoc + */ + private static class SubClass { + String string1; + final theNumbers enumValue = dos; + int aInteger = 2; + long aLong = 10l; + + SubClass(theNumbers enumValue) { + super(); + // if comment + if (enumValue.equals(this.enumValue)) { + // comment 'if' true + string1 = "equals"; // comment inline 'if' true + } else if (theNumbers.tres.equals(enumValue)){ + // comment 'elseif' true + string1 = "elseif"; // comment inline elseif true + } else { + /* comment in 'else' */ + string1 = "else"; /* comment inline else*/ + } + } + + } + + List>>> newList(List>> theList){ + List>>> newListResult = new ArrayList>>>(); + newListResult.add(theList); + return newListResult; + } +} diff --git a/classpath-javaparser/src/test/resources/SimpleClass2.java.test b/classpath-javaparser/src/test/resources/SimpleClass2.java.test new file mode 100644 index 000000000..8acb42d7a --- /dev/null +++ b/classpath-javaparser/src/test/resources/SimpleClass2.java.test @@ -0,0 +1,12 @@ +package org.myPackage; + +import java.util.*; + +public class SimpleClass2 extends OtheClass implements SimpleInterface{ + + List>>> newList(List>> theList){ + List>>> newListResult = new ArrayList>>>(); + newListResult.add(theList); + return newListResult; + } +} diff --git a/classpath-javaparser/src/test/resources/SimpleClass3.java.test b/classpath-javaparser/src/test/resources/SimpleClass3.java.test new file mode 100644 index 000000000..48905b2cc --- /dev/null +++ b/classpath-javaparser/src/test/resources/SimpleClass3.java.test @@ -0,0 +1,36 @@ +package org.myPackage; + +import java.util.*; + +public class SimpleClass3{ + + int mInteger = 0; + int[] mIntegerArray; + int[][] mIntegerArray2; + int[][][] mIntegerArray3; + + byte mByte; + byte mByteArray[]; + byte mByteArray2[][]; + byte mByteArray3[][][]; + + Long mLongObject; + Long[] mLongObjectArray; + Long[][] mLongObjectArray2; + Long[][][] mLongObjectArray3; + + Set mSetString; + Set[] mSetStringArray; + Set[][] mSetStringArray2; + Set[][][] mSetStringArray3; + + Map mMapStringDouble; + Map[] mMapStringDoubleArray; + Map[][] mMapStringDoubleArray2; + Map[][][] mMapStringDoubleArray3; + + List>> mListMapStringIteratorDouble; + List>>[] mListMapStringIteratorDoubleArray; + List>>[][] mListMapStringIteratorDoubleArray2; + List>>[][][] mListMapStringIteratorDoubleArray3; +} diff --git a/classpath-javaparser/src/test/resources/SimpleInterface.java.test b/classpath-javaparser/src/test/resources/SimpleInterface.java.test new file mode 100644 index 000000000..e25b1b250 --- /dev/null +++ b/classpath-javaparser/src/test/resources/SimpleInterface.java.test @@ -0,0 +1,27 @@ +/** + * File header + */ +package org.myPackage; + +// Simple comment1 +import test.importtest.pkg; + +/** + * Coment about import + */ +import static test.importtest.pkg.Hola.proofMethod; + +/** + * @author DiSiD Technologies + */ +public interface SimpleInterface extends Comparable, Iterable { + + /** + * Javadoc of hello method + * + * @param value + * @return + */ + @Deprecated + String hello(String value); +} diff --git a/classpath/pom.xml b/classpath/pom.xml new file mode 100644 index 000000000..0171fcabe --- /dev/null +++ b/classpath/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.classpath + bundle + Spring Roo - Classpath + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.project + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.inflector + + + junit + junit + + compile + + true + + + \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/DefaultPhysicalTypeMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/DefaultPhysicalTypeMetadataProvider.java new file mode 100644 index 000000000..69860e63d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/DefaultPhysicalTypeMetadataProvider.java @@ -0,0 +1,370 @@ +package org.springframework.roo.classpath; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.References; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.DefaultPhysicalTypeMetadata; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsBuilder; +import org.springframework.roo.classpath.scanner.MemberDetailsDecorator; +import org.springframework.roo.file.monitor.event.FileEvent; +import org.springframework.roo.file.monitor.event.FileEventListener; +import org.springframework.roo.file.monitor.event.FileOperation; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Monitors for *.java files and produces a {@link PhysicalTypeMetadata} for + * each, also providing type creation and deleting methods. Prior to 1.2.0, the + * default implementation of PhysicalTypeMetadataProvider was + * JavaParserMetadataProvider. + * + * @author Ben Alex + * @author James Tyrrell + * @since 1.2.0 + */ +@Component +@Service +@References(value = { @Reference(name = "memberHoldingDecorator", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = MemberDetailsDecorator.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) }) +public class DefaultPhysicalTypeMetadataProvider implements + PhysicalTypeMetadataProvider, FileEventListener { + + protected final static Logger LOGGER = HandlerUtils.getLogger(DefaultPhysicalTypeMetadataProvider.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + private final SortedSet decorators = new TreeSet( + new Comparator() { + public int compare(final MemberDetailsDecorator o1, + final MemberDetailsDecorator o2) { + return o1.getClass().getName() + .compareTo(o2.getClass().getName()); + } + }); + + private FileManager fileManager; + private MetadataDependencyRegistry metadataDependencyRegistry; + private MetadataService metadataService; + private ProjectOperations projectOperations; + private TypeLocationService typeLocationService; + private TypeParsingService typeParsingService; + + // Mutex + private final Object lock = new Object(); + + protected void bindMemberHoldingDecorator( + final MemberDetailsDecorator decorator) { + synchronized (lock) { + decorators.add(decorator); + } + } + + public MetadataItem get(final String metadataIdentificationString) { + + if(fileManager == null){ + fileManager = getFileManager(); + } + + Validate.notNull(fileManager, "FileManager is required"); + + if (metadataDependencyRegistry == null){ + metadataDependencyRegistry = getMetadataDependencyRegistry(); + } + + Validate.notNull(metadataDependencyRegistry, "MetadataDependencyRegistry is required"); + + if(projectOperations == null){ + projectOperations = getProjectOperations(); + } + + Validate.notNull(projectOperations, "ProjectOperations is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + if(typeParsingService == null){ + typeParsingService = getTypeParsingService(); + } + + Validate.notNull(typeParsingService, "TypeParsingService is required"); + + + Validate.isTrue( + PhysicalTypeIdentifier.isValid(metadataIdentificationString), + "Metadata id '%s' is not valid for this metadata provider", + metadataIdentificationString); + final String canonicalPath = typeLocationService + .getPhysicalTypeCanonicalPath(metadataIdentificationString); + if (StringUtils.isBlank(canonicalPath)) { + return null; + } + metadataDependencyRegistry + .deregisterDependencies(metadataIdentificationString); + if (!fileManager.exists(canonicalPath)) { + // Couldn't find the file, so return null to distinguish from a file + // that was found but could not be parsed + return null; + } + final JavaType javaType = PhysicalTypeIdentifier + .getJavaType(metadataIdentificationString); + final ClassOrInterfaceTypeDetails typeDetails = typeParsingService + .getTypeAtLocation(canonicalPath, metadataIdentificationString, + javaType); + if (typeDetails == null) { + return null; + } + final PhysicalTypeMetadata result = new DefaultPhysicalTypeMetadata( + metadataIdentificationString, canonicalPath, typeDetails); + final ClassOrInterfaceTypeDetails details = result + .getMemberHoldingTypeDetails(); + if (details != null + && details.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS + && details.getExtendsTypes().size() == 1) { + // This is a class, and it extends another class + if (details.getSuperclass() != null) { + // We have a dependency on the superclass, and there is metadata + // available for the superclass + // We won't implement the full MetadataNotificationListener + // here, but rely on MetadataService's fallback + // (which is to evict from cache and call get again given + // JavaParserMetadataProvider doesn't implement + // MetadataNotificationListener, then notify everyone we've + // changed) + final String superclassId = details.getSuperclass() + .getDeclaredByMetadataId(); + metadataDependencyRegistry.registerDependency(superclassId, + result.getId()); + } + else { + // We have a dependency on the superclass, but no metadata is + // available + // We're left with no choice but to register for every physical + // type change, in the hope we discover our parent someday + for (final LogicalPath sourcePath : projectOperations + .getPathResolver().getSourcePaths()) { + final String possibleSuperclass = PhysicalTypeIdentifier + .createIdentifier(details.getExtendsTypes().get(0), + sourcePath); + metadataDependencyRegistry.registerDependency( + possibleSuperclass, result.getId()); + } + } + } + MemberDetails memberDetails = new MemberDetailsBuilder( + Arrays.asList(details)).build(); + // Loop until such time as we complete a full loop where no changes are + // made to the result + boolean additionalLoopRequired = true; + while (additionalLoopRequired) { + additionalLoopRequired = false; + for (final MemberDetailsDecorator decorator : decorators) { + final MemberDetails newResult = decorator.decorateTypes( + DefaultPhysicalTypeMetadataProvider.class.getName(), + memberDetails); + Validate.isTrue(newResult != null, + "Decorator '%s' returned an illegal result", decorator + .getClass().getName()); + if (!newResult.equals(memberDetails)) { + additionalLoopRequired = true; + memberDetails = newResult; + } + } + } + + return new DefaultPhysicalTypeMetadata(metadataIdentificationString, + canonicalPath, (ClassOrInterfaceTypeDetails) memberDetails + .getDetails().get(0)); + } + + public String getProvidesType() { + return PhysicalTypeIdentifier.getMetadataIdentiferType(); + } + + public void onFileEvent(final FileEvent fileEvent) { + + if (metadataDependencyRegistry == null){ + metadataDependencyRegistry = getMetadataDependencyRegistry(); + } + + Validate.notNull(metadataDependencyRegistry, "MetadataDependencyRegistry is required"); + + if(metadataService == null){ + metadataService = getMetadataService(); + } + + Validate.notNull(metadataService, "MetadataService is required"); + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + final String fileIdentifier = fileEvent.getFileDetails() + .getCanonicalPath(); + + // Check to see if file is of interest + if (fileIdentifier.endsWith(".java") + && fileEvent.getOperation() != FileOperation.MONITORING_FINISH + && !fileIdentifier.endsWith("package-info.java")) { + // Figure out the PhysicalTypeIdentifier + final String id = typeLocationService + .getPhysicalTypeIdentifier(fileIdentifier); + if (id == null) { + return; + } + // Now we've worked out the id, we can publish the event in case + // others were interested + metadataService.evictAndGet(id); + metadataDependencyRegistry.notifyDownstream(id); + } + } + + protected void unbindMemberHoldingDecorator( + final MemberDetailsDecorator decorator) { + synchronized (lock) { + decorators.remove(decorator); + } + } + + public FileManager getFileManager(){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) this.context.getService(ref); + } + + LOGGER.warning("Cannot load FileManager on DefaultPhysicalTypeMetadataProvider."); + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on DefaultPhysicalTypeMetadataProvider."); + return null; + } + } + + public MetadataDependencyRegistry getMetadataDependencyRegistry(){ + // Get all Services implement MetadataDependencyRegistry interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataDependencyRegistry.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataDependencyRegistry) this.context.getService(ref); + } + + LOGGER.warning("Cannot load MetadataDependencyRegistry on DefaultPhysicalTypeMetadataProvider."); + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataDependencyRegistry on DefaultPhysicalTypeMetadataProvider."); + return null; + } + } + + public MetadataService getMetadataService(){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + LOGGER.warning("Cannot load MetadataService on DefaultPhysicalTypeMetadataProvider."); + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on DefaultPhysicalTypeMetadataProvider."); + return null; + } + } + + public ProjectOperations getProjectOperations(){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) this.context.getService(ref); + } + + LOGGER.warning("Cannot load ProjectOperations on DefaultPhysicalTypeMetadataProvider."); + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on DefaultPhysicalTypeMetadataProvider."); + return null; + } + } + + public TypeLocationService getTypeLocationService(){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) this.context.getService(ref); + } + + LOGGER.warning("Cannot load TypeLocationService on DefaultPhysicalTypeMetadataProvider."); + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on DefaultPhysicalTypeMetadataProvider."); + return null; + } + } + + public TypeParsingService getTypeParsingService(){ + // Get all Services implement TypeParsingService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(TypeParsingService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeParsingService) this.context.getService(ref); + } + + LOGGER.warning("Cannot load TypeParsingService on DefaultPhysicalTypeMetadataProvider."); + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeParsingService on DefaultPhysicalTypeMetadataProvider."); + return null; + } + } + + +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/ItdDiscoveryService.java b/classpath/src/main/java/org/springframework/roo/classpath/ItdDiscoveryService.java new file mode 100644 index 000000000..66c4c7048 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/ItdDiscoveryService.java @@ -0,0 +1,38 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.model.JavaType; + +/** + * An ITD store which can be inspected to see if ITDs associated with a type + * have changed. + * + * @author James Tyrrell + * @since 1.2.0 + */ +public interface ItdDiscoveryService { + + /** + * Adds the presented {@link ItdTypeDetails} to the management service. + * + * @param itdTypeDetails to be added (required) + */ + void addItdTypeDetails(ItdTypeDetails itdTypeDetails); + + /** + * Indicates whether ITDs associate with the passed in type has changed + * since last invocation by the requesting class. + * + * @param requestingClass the class requesting the changed types + * @param javaType the type to lookup to see if a change has occurred + * @return a collection of MIDs which represent changed types + */ + boolean haveItdsChanged(String requestingClass, JavaType javaType); + + /** + * Removes the {@link ItdTypeDetails} associated with the presented String. + * + * @param mid the ID of the {@link ItdTypeDetails} be removed (required) + */ + void removeItdTypeDetails(String mid); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/ItdDiscoveryServiceImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/ItdDiscoveryServiceImpl.java new file mode 100644 index 000000000..99a45a0b6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/ItdDiscoveryServiceImpl.java @@ -0,0 +1,93 @@ +package org.springframework.roo.classpath; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.model.JavaType; + +/** + * Implementation of {@link ItdDiscoveryService}. + * + * @author James Tyrrell + * @since 1.2.0 + */ +@Component +@Service +public class ItdDiscoveryServiceImpl implements ItdDiscoveryService { + + private final Map> changeMap = new HashMap>(); + private final Map itdIdToTypeMap = new HashMap(); + private final Map> typeMap = new HashMap>(); + + public void addItdTypeDetails(final ItdTypeDetails itdTypeDetails) { + if (itdTypeDetails == null || itdTypeDetails.getGovernor() == null) { + return; + } + if (typeMap.get(itdTypeDetails.getGovernor().getName() + .getFullyQualifiedTypeName()) == null) { + typeMap.put(itdTypeDetails.getGovernor().getName() + .getFullyQualifiedTypeName(), + new HashMap()); + } + itdIdToTypeMap.put(itdTypeDetails.getDeclaredByMetadataId(), + itdTypeDetails.getGovernor().getName() + .getFullyQualifiedTypeName()); + typeMap.get( + itdTypeDetails.getGovernor().getName() + .getFullyQualifiedTypeName()).put( + itdTypeDetails.getDeclaredByMetadataId(), itdTypeDetails); + updateChanges(itdTypeDetails.getGovernor().getName(), false); + } + + public boolean haveItdsChanged(final String requestingClass, + final JavaType javaType) { + Set changesSinceLastRequest = changeMap.get(requestingClass); + if (changesSinceLastRequest == null) { + changesSinceLastRequest = new LinkedHashSet( + typeMap.keySet()); + changeMap.put(requestingClass, changesSinceLastRequest); + } + for (final String changedId : changesSinceLastRequest) { + if (changedId.equals(javaType.getFullyQualifiedTypeName())) { + changesSinceLastRequest.remove(changedId); + return true; + } + } + return false; + } + + public void removeItdTypeDetails(final String itdTypeDetailsId) { + if (StringUtils.isBlank(itdTypeDetailsId)) { + return; + } + final String type = itdIdToTypeMap.get(itdTypeDetailsId); + if (type != null) { + final Map typeDetailsHashMap = typeMap + .get(type); + if (typeDetailsHashMap != null) { + typeDetailsHashMap.remove(itdTypeDetailsId); + } + updateChanges(new JavaType(type), true); + } + } + + private void updateChanges(final JavaType javaType, final boolean remove) { + for (final String requestingClass : changeMap.keySet()) { + if (remove) { + changeMap.get(requestingClass).remove( + javaType.getFullyQualifiedTypeName()); + } + else { + changeMap.get(requestingClass).add( + javaType.getFullyQualifiedTypeName()); + } + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/LocatedTypeCallback.java b/classpath/src/main/java/org/springframework/roo/classpath/LocatedTypeCallback.java new file mode 100644 index 000000000..f84f56cf6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/LocatedTypeCallback.java @@ -0,0 +1,20 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; + +/** + * Callback interface to process a {@link ClassOrInterfaceTypeDetails} type. + * + * @author Alan Stewart + * @since 1.1 + */ +public interface LocatedTypeCallback { + + /** + * Callback method to process the located + * {@link ClassOrInterfaceTypeDetails} type. + * + * @param located the {@link ClassOrInterfaceTypeDetails} type. + */ + void process(ClassOrInterfaceTypeDetails located); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/MetadataCommands.java b/classpath/src/main/java/org/springframework/roo/classpath/MetadataCommands.java new file mode 100644 index 000000000..c61a147be --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/MetadataCommands.java @@ -0,0 +1,169 @@ +package org.springframework.roo.classpath; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataLogger; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.metadata.MetadataTimingStatistic; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.ProjectMetadata; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.converter.PomConverter; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +@Component +@Service +public class MetadataCommands implements CommandMarker { + + private static final String METADATA_FOR_MODULE_COMMAND = "metadata for module"; + + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private MetadataDependencyRegistry metadataDependencyRegistry; + @Reference private MetadataLogger metadataLogger; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + + @CliAvailabilityIndicator(METADATA_FOR_MODULE_COMMAND) + public boolean isModuleMetadataAvailable() { + return projectOperations.getFocusedModule() != null; + } + + @CliCommand(value = "metadata cache", help = "Shows detailed metadata for the indicated type") + public String metadataCacheMaximum( + @CliOption(key = { "maximumCapacity" }, mandatory = true, help = "The maximum number of metadata items to cache") final int maxCapacity) { + Validate.isTrue(maxCapacity >= 100, + "Maximum capacity must be 100 or greater"); + metadataService.setMaxCapacity(maxCapacity); + // Show them that the change has taken place + return metadataTimings(); + } + + @CliCommand(value = "metadata for id", help = "Shows detailed information about the metadata item") + public String metadataForId( + @CliOption(key = { "", "metadataId" }, mandatory = true, help = "The metadata ID (should start with MID:)") final String metadataId) { + final StringBuilder sb = new StringBuilder(); + sb.append("Identifier : ").append(metadataId) + .append(IOUtils.LINE_SEPARATOR); + + for (final String upstreamId : metadataDependencyRegistry + .getUpstream(metadataId)) { + sb.append("Upstream : ").append(upstreamId) + .append(LINE_SEPARATOR); + } + + // Include any "class level" notifications that this instance would + // receive (useful for debugging) + // Only necessary if the ID doesn't already represent a class (as such + // dependencies would have been listed earlier) + if (!MetadataIdentificationUtils.isIdentifyingClass(metadataId)) { + final String mdClassId = MetadataIdentificationUtils + .getMetadataClassId(metadataId); + for (final String upstreamId : metadataDependencyRegistry + .getUpstream(mdClassId)) { + sb.append("Upstream : ").append(upstreamId) + .append(" (via MD class)").append(LINE_SEPARATOR); + } + } + + for (final String downstreamId : metadataDependencyRegistry + .getDownstream(metadataId)) { + sb.append("Downstream : ").append(downstreamId) + .append(LINE_SEPARATOR); + } + + // Include any notifications that this class of metadata would trigger + // (useful for debugging) + // Only necessary if the ID doesn't already represent a class (as such + // dependencies would have been listed earlier) + if (!MetadataIdentificationUtils.isIdentifyingClass(metadataId)) { + final String mdClassId = MetadataIdentificationUtils + .getMetadataClassId(metadataId); + for (final String downstreamId : metadataDependencyRegistry + .getDownstream(mdClassId)) { + sb.append("Downstream : ").append(downstreamId) + .append(" (via MD class)").append(LINE_SEPARATOR); + } + } + + if (MetadataIdentificationUtils.isIdentifyingInstance(metadataId)) { + sb.append("Metadata : ").append(metadataService.get(metadataId)); + } + return sb.toString(); + } + + @CliCommand(value = METADATA_FOR_MODULE_COMMAND, help = "Shows the ProjectMetadata for the indicated project module") + public String metadataForModule( + @CliOption(key = { "", "module" }, mandatory = false, optionContext = PomConverter.INCLUDE_CURRENT_MODULE, help = "The module for which to retrieve the metadata (defaults to the focused module)") final Pom pom) { + final Pom targetPom = ObjectUtils.defaultIfNull(pom, + projectOperations.getFocusedModule()); + if (targetPom == null) { + return "This project has no modules"; + } + final String projectMID = ProjectMetadata + .getProjectIdentifier(targetPom.getModuleName()); + return metadataService.get(projectMID).toString(); + } + + @CliCommand(value = "metadata for type", help = "Shows detailed metadata for the indicated type") + public String metadataForType( + @CliOption(key = { "", "type" }, mandatory = true, help = "The Java type for which to display metadata") final JavaType javaType) { + final String id = typeLocationService + .getPhysicalTypeIdentifier(javaType); + if (id == null) { + return "Cannot locate source for " + + javaType.getFullyQualifiedTypeName(); + } + final StringBuilder sb = new StringBuilder(); + sb.append("Java Type : ").append(javaType.getFullyQualifiedTypeName()) + .append(System.getProperty("line.separator")); + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(javaType); + if (javaTypeDetails == null) { + sb.append("Java type details unavailable").append( + System.getProperty("line.separator")); + } + else { + for (final MemberHoldingTypeDetails holder : memberDetailsScanner + .getMemberDetails(getClass().getName(), javaTypeDetails) + .getDetails()) { + sb.append("Member scan: ") + .append(holder.getDeclaredByMetadataId()) + .append(System.getProperty("line.separator")); + } + } + sb.append(metadataForId(id)); + return sb.toString(); + } + + @CliCommand(value = "metadata status", help = "Shows metadata statistics") + public String metadataTimings() { + final StringBuilder sb = new StringBuilder(); + for (final MetadataTimingStatistic stat : metadataLogger.getTimings()) { + sb.append(stat.toString()).append(LINE_SEPARATOR); + } + sb.append(metadataService.toString()); + return sb.toString(); + } + + @CliCommand(value = "metadata trace", help = "Traces metadata event delivery notifications") + public void metadataTrace( + @CliOption(key = { "", "level" }, mandatory = true, help = "The verbosity of notifications (0=none, 1=some, 2=all)") final int level) { + metadataLogger.setTraceLevel(level); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeCategory.java b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeCategory.java new file mode 100644 index 000000000..e4dc418d1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeCategory.java @@ -0,0 +1,12 @@ +package org.springframework.roo.classpath; + +/** + * Indicates the type of {@link PhysicalTypeDetails}. + * + * @author Ben Alex + * @since 1.0 + */ +public enum PhysicalTypeCategory { + + ANNOTATION, CLASS, ENUMERATION, INTERFACE, ITD, OTHER; +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeDetails.java new file mode 100644 index 000000000..83ba114a4 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeDetails.java @@ -0,0 +1,40 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.model.CustomDataAccessor; +import org.springframework.roo.model.JavaType; + +/** + * Provides details of the actual type presented by a + * {@link PhysicalTypeMetadata} instance. + *

    + * Sub-interfaces are created for different major Java types, such as those + * specific to classes, enums, and annotations. This allows sub-interfaces to + * provide accessors applicable to the specific category of Java type. + * + * @author Ben Alex + * @since 1.0 + */ +public interface PhysicalTypeDetails extends CustomDataAccessor { + + /** + * @see #getType(), which returns the same thing but is better named + */ + JavaType getName(); + + /** + * @return the category of Java type being provided by this + * {@link PhysicalTypeDetails} instance (never null) + */ + PhysicalTypeCategory getPhysicalTypeCategory(); + + /** + * Returns the {@link JavaType} provided by this physical type. If possible, + * indicates any type parameters. + * + * @return the full name of the type that members will eventually be + * available from when compiled, including any available type + * parameters (may be null if unable to parse) + * @since 1.2.0 + */ + JavaType getType(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeIdentifier.java b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeIdentifier.java new file mode 100644 index 000000000..1934930f2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeIdentifier.java @@ -0,0 +1,106 @@ +package org.springframework.roo.classpath; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Provides string manipulation functions for {@link PhysicalTypeMetadata} IDs. + * + * @author Ben Alex + * @since 1.0 + */ +public final class PhysicalTypeIdentifier { + + private static final String PHYSICAL_METADATA_TYPE = PhysicalTypeIdentifier.class + .getName(); + + /** + * The class-level ID for physical type metadata. + * + * @since 1.2.0 + */ + public static final String PHYSICAL_METADATA_TYPE_ID = MetadataIdentificationUtils + .create(PHYSICAL_METADATA_TYPE); + + /** + * Creates a physical type metadata ID for the given user project type, + * which need not exist. If you know the {@link JavaType} exists but don't + * know its {@link LogicalPath}, you can use + * {@link TypeLocationService#getPhysicalTypeIdentifier(JavaType)} instead. + * + * @param javaType the type for which to create the identifier (required) + * @param path the path in which it's located (required) + * @return a non-blank ID + */ + public static String createIdentifier(final JavaType javaType, + final LogicalPath path) { + return PhysicalTypeIdentifierNamingUtils.createIdentifier( + PHYSICAL_METADATA_TYPE, javaType, path); + } + + public static String getFriendlyName(final String metadataId) { + Validate.isTrue(isValid(metadataId), "Invalid metadata id '%s'", + metadataId); + return getPath(metadataId) + "/" + getJavaType(metadataId); + } + + /** + * Parses the given metadata ID for the user project type to which it + * relates. + * + * @param physicalTypeId the metadata ID to parse (must identify an instance + * of {@link PhysicalTypeIdentifier#PHYSICAL_METADATA_TYPE}) + * @return a non-null type + */ + public static JavaType getJavaType(final String physicalTypeId) { + Validate.isTrue(PhysicalTypeIdentifier.isValid(physicalTypeId), + "Physical type identifier is invalid"); + return PhysicalTypeIdentifierNamingUtils.getJavaType( + PHYSICAL_METADATA_TYPE, physicalTypeId); + } + + /** + * Returns the class-level ID for physical type metadata. Equivalent to + * accessing {@link #PHYSICAL_METADATA_TYPE_ID} directly. + * + * @return {@value #PHYSICAL_METADATA_TYPE_ID} + */ + public static String getMetadataIdentiferType() { + return PHYSICAL_METADATA_TYPE_ID; + } + + /** + * Parses the given metadata ID for the path of the user project type to + * which it relates. + * + * @param metadataId the metadata ID to parse (must identify an instance of + * {@link PhysicalTypeIdentifier#PHYSICAL_METADATA_TYPE}) + * @return a non-null path + */ + public static LogicalPath getPath(final String metadataId) { + return PhysicalTypeIdentifierNamingUtils.getPath( + PHYSICAL_METADATA_TYPE, metadataId); + } + + /** + * Indicates whether the given metadata ID identifies a physical Java type, + * in other words an interface, class, annotation, or enum. + * + * @param metadataIdentificationString the metadata ID to check + * @return see above + */ + public static boolean isValid(final String metadataIdentificationString) { + return PhysicalTypeIdentifierNamingUtils.isValid( + PHYSICAL_METADATA_TYPE, metadataIdentificationString); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private PhysicalTypeIdentifier() { + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeIdentifierNamingUtils.java b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeIdentifierNamingUtils.java new file mode 100644 index 000000000..4596c7ece --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeIdentifierNamingUtils.java @@ -0,0 +1,167 @@ +package org.springframework.roo.classpath; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; + +/** + * Produces metadata identification strings that represent a {@link JavaType} + * located in a particular {@link LogicalPath}. + *

    + * The metadata identification strings separate the path name from the fully + * qualified type name via the presence of a question mark character ("?"). A + * question mark is used given it is reserved by {@link Path}. TODO these + * methods are not specific to physical types; either rename this class, move + * them somewhere more generic, and/or make them more specific, e.g. hardcode + * the "metadata class" arguments to that of physical types. + * + * @author Ben Alex + * @since 1.0 + */ +public final class PhysicalTypeIdentifierNamingUtils { + + private static final String PATH_SUFFIX = "?"; + + /** + * Creates a metadata ID from the given inputs + * + * @param metadataClass the fully-qualified name of the metadata class + * (required) + * @param projectType the fully-qualified name of the user project type to + * which the metadata relates (required) + * @param path the path to that type within the project (required) + * @return a non-blank ID + */ + public static String createIdentifier(final String metadataClass, + final JavaType projectType, final LogicalPath path) { + Validate.notNull(projectType, "Java type required"); + Validate.notNull(path, "Path required"); + return MetadataIdentificationUtils.create(metadataClass, path.getName() + + PATH_SUFFIX + projectType.getFullyQualifiedTypeName()); + } + + /** + * Parses the instance key from the given metadata ID. + * + * @param metadataClass the fully-qualified name of the metadata type + * (required) + * @param metadataId the ID of the metadata instance (must identify an + * instance of the given metadata class) + * @return a non-blank key, as per + * {@link MetadataIdentificationUtils#getMetadataInstance(String)} + */ + private static String getInstanceKey(final String metadataClass, + final String metadataId) { + Validate.isTrue(isValid(metadataClass, metadataId), + "Metadata id '%s' is not a valid %s identifier", metadataId, + metadataClass); + return MetadataIdentificationUtils.getMetadataInstance(metadataId); + } + + public static JavaType getJavaType(final String metadataIdentificationString) { + Validate.isTrue( + metadataIdentificationString.contains("#"), + "Metadata identification string '%s' does not appear to be a valid identifier", + metadataIdentificationString); + final String instance = MetadataIdentificationUtils + .getMetadataInstance(metadataIdentificationString); + final int index = instance.indexOf("?"); + return new JavaType(instance.substring(index + 1)); + } + + /** + * Returns the user project type with which the given metadata ID is + * associated. + * + * @param metadataClass the fully-qualified name of the metadata type + * (required) + * @param metadataId the ID of the metadata instance (must identify an + * instance of the given metadata class) + * @return a non-null type + */ + public static JavaType getJavaType(final String metadataClass, + final String metadataId) { + final String instanceKey = getInstanceKey(metadataClass, metadataId); + return new JavaType(instanceKey.substring(instanceKey + .indexOf(PATH_SUFFIX) + 1)); + } + + /** + * Returns the name of the project module that contains the metadata item + * with the given id. + * + * @param metadataId must be a valid metadata instance id + * @return a non-null module name (blank means the root or only + * module) + * @since 1.2.0 + */ + public static String getModule(final String metadataId) { + return getPath(metadataId).getModule(); + } + + /** + * Returns the {@link LogicalPath} of the metadata item with the given id. + * + * @param metadataId must be a valid metadata instance id + * @return a non-null path + */ + public static LogicalPath getPath(final String metadataId) { + Validate.isTrue( + MetadataIdentificationUtils.isIdentifyingInstance(metadataId), + "Metadata id '%s' does not appear to be a valid identifier", + metadataId); + final String instanceKey = MetadataIdentificationUtils + .getMetadataInstance(metadataId); + final int index = instanceKey.indexOf("?"); + return LogicalPath.getInstance(instanceKey.substring(0, index)); + } + + /** + * Parses the user project path from the given metadata ID. + * + * @param metadataClass the fully-qualified name of the metadata type + * (required) + * @param metadataId the ID of the metadata instance (must identify an + * instance of the given metadata class) + * @return a non-null path + */ + public static LogicalPath getPath(final String providesType, + final String metadataIdentificationString) { + Validate.isTrue( + isValid(providesType, metadataIdentificationString), + "Metadata identification string '%s' does not appear to be a valid physical type identifier", + metadataIdentificationString); + final String instance = MetadataIdentificationUtils + .getMetadataInstance(metadataIdentificationString); + final int index = instance.indexOf("?"); + return LogicalPath.getInstance(instance.substring(0, index)); + } + + /** + * Indicates whether the given metadata id appears to identify an instance + * of the given metadata class. + * + * @param metadataClass the fully-qualified name of the expected metadata + * type (can be blank) + * @param metadataId the ID to evaluate (can be blank) + * @return true only if the metadata ID appears to be valid + */ + public static boolean isValid(final String metadataClass, + final String metadataId) { + return MetadataIdentificationUtils.isIdentifyingInstance(metadataId) + && MetadataIdentificationUtils.getMetadataClass(metadataId) + .equals(metadataClass) + && MetadataIdentificationUtils.getMetadataInstance(metadataId) + .contains(PATH_SUFFIX); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private PhysicalTypeIdentifierNamingUtils() { + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeMetadata.java new file mode 100644 index 000000000..992356cf4 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeMetadata.java @@ -0,0 +1,72 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.itd.ItdMetadataProvider; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.model.JavaType; + +/** + * The metadata for a Java type in the user's project. Excludes any members + * introduced via an inter-type declaration (ITD) or other bytecode modification + * technique. + * + * @author Ben Alex + * @since 1.0 + * @see PhysicalTypeMetadataProvider + * @see MemberDetailsScanner + */ +public interface PhysicalTypeMetadata extends + MemberHoldingTypeDetailsMetadataItem { + + /** + * Obtains the canonical file path to where an ITD can be emitted for this + * physical Java type. + * + * @param metadataProvider so the + * {@link ItdMetadataProvider#getItdUniquenessFilenameSuffix()} + * can be queried (never null) + * @return a full file path that can be used to produce an ITD (never null) + * @deprecated use {@link #getItdCanonicalPath(ItdMetadataProvider)} instead + * (fixes typo) + */ + @Deprecated + String getItdCanoncialPath(ItdMetadataProvider metadataProvider); + + /** + * Obtains the canonical file path to where an ITD can be emitted for this + * physical Java type. + * + * @param metadataProvider the {@link MetadataProvider} that produces the + * ITD in question (never null) + * @return a full file path that can be used to produce an ITD (never null) + * @since 1.2.0 + */ + String getItdCanonicalPath(ItdMetadataProvider metadataProvider); + + /** + * Obtains the {@link JavaType} which represents an ITD for this physical + * Java type. + * + * @param metadataProvider so the + * {@link ItdMetadataProvider#getItdUniquenessFilenameSuffix()} + * can be queried (never null) + * @return the {@link JavaType} applicable for this ITD (never null) + */ + JavaType getItdJavaType(ItdMetadataProvider metadataProvider); + + /** + * @return the location of the disk file containing this resource, in + * canonical name format (never null) + */ + String getPhysicalLocationCanonicalPath(); + + /** + * Returns the Java type for this physical type + * + * @return a non-null type + * @since 1.2.0 + */ + JavaType getType(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeMetadataProvider.java new file mode 100644 index 000000000..922638023 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/PhysicalTypeMetadataProvider.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.metadata.MetadataProvider; + +/** + * Provides a classpath-related finder for {@link PhysicalTypeMetadata} + * instances. + *

    + * Add-ons can rely on there being only one {@link PhysicalTypeMetadataProvider} + * active at a time. Initially this will be because there will only be one + * implementation that uses source code AST parsing, although it is intended + * that a bytecode-based parser may also be added in the future. If more than + * one implementation is eventually developed, they will be hidden below a + * single visible delegating implementation. As such add-ons do not need to + * consult a list of different implementations. + * + * @author Ben Alex + * @since 1.0 + */ +public interface PhysicalTypeMetadataProvider extends MetadataProvider { +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TriggerBasedMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/TriggerBasedMetadataProvider.java new file mode 100644 index 000000000..14cf17ce3 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TriggerBasedMetadataProvider.java @@ -0,0 +1,30 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.model.JavaType; + +/** + * A {@link MetadataProvider} that produces metadata when any of various trigger + * annotations are present on a user project type (known as the governor). + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface TriggerBasedMetadataProvider extends MetadataProvider { + + /** + * Causes this provider to generate metadata if the given annotation is + * present. + * + * @param trigger the trigger to register (can be null) + */ + void addMetadataTrigger(JavaType trigger); + + /** + * Stops this provider generating metadata if the given annotation is + * present. + * + * @param trigger the trigger to deregister (can be null) + */ + void removeMetadataTrigger(JavaType trigger); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeCache.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeCache.java new file mode 100644 index 000000000..642ef717f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeCache.java @@ -0,0 +1,29 @@ +package org.springframework.roo.classpath; + +import java.util.Set; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.maven.Pom; + +public interface TypeCache { + + void cacheFilePathAgainstTypeIdentifier(String typeFilePath, + String typeIdentifier); + + void cacheType(String typeFilePath, ClassOrInterfaceTypeDetails cid); + + void cacheTypeAgainstModule(Pom pom, JavaType javaType); + + Set getAllTypeIdentifiers(); + + String getPhysicalTypeIdentifier(JavaType javaType); + + ClassOrInterfaceTypeDetails getTypeDetails(String mid); + + String getTypeIdFromTypeFilePath(String typeFilePath); + + Set getTypeNamesForModuleFilePath(String moduleFilePath); + + void removeType(String typeIdentifier); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeCacheImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeCacheImpl.java new file mode 100644 index 000000000..b5cb2595b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeCacheImpl.java @@ -0,0 +1,130 @@ +package org.springframework.roo.classpath; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.maven.Pom; + +@Component +@Service +public class TypeCacheImpl implements TypeCache { + + private final Map midToTypeDetailsMap = new HashMap(); + private final Map> moduleFilePathToTypeNamesMap = new HashMap>(); + private final Map> simpleTypeNameTypesMap = new HashMap>(); + private final Map typeFilePathToMidMap = new HashMap(); + private final Map typeIdentifierToFilePathMap = new HashMap(); + private final Map typeNameToMidMap = new HashMap(); + private final Map typeNameToModuleFilePathMap = new HashMap(); + private final Map typeNameToModuleNameMap = new HashMap(); + private final Set types = new HashSet(); + + public void cacheFilePathAgainstTypeIdentifier(final String typeFilePath, + final String typeIdentifier) { + typeFilePathToMidMap.put(typeFilePath, typeIdentifier); + } + + public void cacheType(final String typeFilePath, + final ClassOrInterfaceTypeDetails cid) { + Validate.notBlank(typeFilePath, "Module name required"); + Validate.notNull(cid, "Type details required"); + + midToTypeDetailsMap.put(cid.getDeclaredByMetadataId(), cid); + typeFilePathToMidMap.put(typeFilePath, cid.getDeclaredByMetadataId()); + typeIdentifierToFilePathMap.put(cid.getDeclaredByMetadataId(), + typeFilePath); + types.add(cid.getName()); + + final String fullyQualifiedTypeName = cid.getName() + .getFullyQualifiedTypeName(); + final String simpleTypeName = cid.getName().getSimpleTypeName(); + typeNameToMidMap.put(fullyQualifiedTypeName, + cid.getDeclaredByMetadataId()); + if (!simpleTypeNameTypesMap.containsKey(simpleTypeName)) { + simpleTypeNameTypesMap.put(simpleTypeName, new HashSet()); + } + + simpleTypeNameTypesMap.get(simpleTypeName).add(fullyQualifiedTypeName); + } + + public void cacheTypeAgainstModule(final Pom pom, final JavaType javaType) { + Validate.notNull(pom, "Pom cannot be null"); + Validate.notNull(javaType, "Java type cannot be null"); + typeNameToModuleFilePathMap.put(javaType.getFullyQualifiedTypeName(), + pom.getPath()); + typeNameToModuleNameMap.put(javaType.getFullyQualifiedTypeName(), + pom.getModuleName()); + if (!moduleFilePathToTypeNamesMap.containsKey(pom.getPath())) { + moduleFilePathToTypeNamesMap.put(pom.getPath(), + new HashSet()); + } + moduleFilePathToTypeNamesMap.get(pom.getPath()).add( + javaType.getFullyQualifiedTypeName()); + } + + public Set getAllTypeIdentifiers() { + return new HashSet(midToTypeDetailsMap.keySet()); + } + + public Set getAllTypes() { + return new HashSet(types); + } + + public String getPhysicalTypeIdentifier(final JavaType javaType) { + Validate.notNull(javaType, "Java type cannot be null"); + return typeNameToMidMap.get(javaType.getFullyQualifiedTypeName()); + } + + public ClassOrInterfaceTypeDetails getTypeDetails(final String mid) { + Validate.notBlank(mid, "Physical type identifier required"); + return midToTypeDetailsMap.get(mid); + } + + public String getTypeIdFromTypeFilePath(final String typeFilePath) { + Validate.notBlank(typeFilePath, "Physical type file path required"); + return typeFilePathToMidMap.get(typeFilePath); + } + + public Set getTypeNamesForModuleFilePath(final String moduleFilePath) { + Validate.notBlank(moduleFilePath, "Pom file path required"); + if (!moduleFilePathToTypeNamesMap.containsKey(moduleFilePath)) { + moduleFilePathToTypeNamesMap.put(moduleFilePath, + new HashSet()); + } + return new HashSet( + moduleFilePathToTypeNamesMap.get(moduleFilePath)); + } + + public Set getTypesForSimpleTypeName(final String simpleTypeName) { + if (!simpleTypeNameTypesMap.containsKey(simpleTypeName)) { + return new HashSet(); + } + return simpleTypeNameTypesMap.get(simpleTypeName); + } + + public void removeType(final String typeIdentifier) { + Validate.notBlank(typeIdentifier, "Physical type identifier required"); + final ClassOrInterfaceTypeDetails cid = midToTypeDetailsMap + .get(typeIdentifier); + if (cid != null) { + typeNameToMidMap.remove(cid.getName().getFullyQualifiedTypeName()); + typeNameToModuleFilePathMap.remove(cid.getName() + .getFullyQualifiedTypeName()); + typeNameToModuleNameMap.remove(cid.getName() + .getFullyQualifiedTypeName()); + } + final String filePath = typeIdentifierToFilePathMap.get(typeIdentifier); + if (filePath != null) { + typeFilePathToMidMap.remove(filePath); + typeIdentifierToFilePathMap.remove(typeIdentifier); + } + + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeLocationService.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeLocationService.java new file mode 100644 index 000000000..9c9a2d8e2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeLocationService.java @@ -0,0 +1,204 @@ +package org.springframework.roo.classpath; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.maven.Pom; + +/** + * Locates types. + * + * @author Alan Stewart + * @author James Tyrrell + * @since 1.1 + */ +public interface TypeLocationService { + + /** + * Returns a set of {@link ClassOrInterfaceTypeDetails}s that possess the + * specified annotations (specified as a vararg). + * + * @param annotationsToDetect the annotations (as a vararg) to detect on a + * type. + * @return a set of ClassOrInterfaceTypeDetails that have the specified + * annotations. + */ + Set findClassesOrInterfaceDetailsWithAnnotation( + JavaType... annotationsToDetect); + + /** + * Returns a set of {@link ClassOrInterfaceTypeDetails}s that possess the + * specified tag. + * + * @param tag the tag to detect on a type. + * @return a set of ClassOrInterfaceTypeDetails that have the specified tag. + */ + Set findClassesOrInterfaceDetailsWithTag( + Object tag); + + /** + * Returns a set of {@link JavaType}s that possess the specified annotations + * (specified as a vararg). + * + * @param annotationsToDetect the annotations (as a vararg) to detect on a + * type. + * @return a set of types that have the specified annotations. + */ + Set findTypesWithAnnotation(JavaType... annotationsToDetect); + + /** + * Returns a set of {@link JavaType}s that possess the specified list of + * annotations. + * + * @param annotationsToDetect the list of annotations to detect on a type. + * @return a set of types that have the specified annotations. + */ + Set findTypesWithAnnotation(List annotationsToDetect); + + /** + * Returns the canonical path that the given {@link JavaType} would have + * within the given {@link LogicalPath}; this type need not exist. + *

    + * Equivalent to constructing the physical type id from the given arguments + * and calling {@link #getPhysicalTypeCanonicalPath(String)}. + * + * @param javaType the type's {@link JavaType} (required) + * @param path the type's logical path + * @return the canonical path (never blank, but might not exist) + */ + String getPhysicalTypeCanonicalPath(JavaType javaType, LogicalPath path); + + /** + * Returns the canonical path that a type with the given physical type id + * would have; this type need not exist. + * + * @param physicalTypeId the physical type's metadata id (required) + * @return the canonical path (never blank, but might not exist) + */ + String getPhysicalTypeCanonicalPath(String physicalTypeId); + + /** + * Looks for the given {@link JavaType} within the user project, and if + * found, returns the id for its {@link PhysicalTypeMetadata}. Use this + * method if you know that the {@link JavaType} exists but don't know its + * {@link LogicalPath}. + *

    + * This method resolves the issue that a {@link JavaType} is location + * independent, yet {@link PhysicalTypeIdentifier} instances are location + * dependent (i.e. a {@link PhysicalTypeIdentifier} relates to a given + * physical file, whereas a {@link JavaType} simply represents a type on the + * classpath). + * + * @param javaType the type to locate (required) + * @return the string (in {@link PhysicalTypeIdentifier} format) if found, + * or null if not found + */ + String getPhysicalTypeIdentifier(JavaType javaType); + + /** + * Returns the physical type identifier for the Java source file with the + * given canonical path. + * + * @param fileIdentifier the path to the physical type (required) + * @return the physical type identifier if the given path matches an + * existing Java source file, otherwise null + */ + String getPhysicalTypeIdentifier(String fileIdentifier); + + /** + * @param module + * @return + */ + List getPotentialTopLevelPackagesForModule(Pom module); + + /** + * @param module + * @return + */ + String getTopLevelPackageForModule(Pom module); + + /** + * Returns the details of the given Java type from within the user project. + * + * @param javaType the type to look for (required) + * @return null if the type doesn't exist in the project + */ + ClassOrInterfaceTypeDetails getTypeDetails(JavaType javaType); + + /** + * Resolves the {@link ClassOrInterfaceTypeDetails} to for the provided + * physical type identifier. If the physical type identifier doesn't + * represent a valid type an exception is thrown. This method will return + * null if the {@link ClassOrInterfaceTypeDetails} can't be found. + * + * @param physicalTypeId the physical type metadata id (can be blank) + * @return the resolved {@link ClassOrInterfaceTypeDetails}, or + * null if the details can't be found (e.g. the given + * ID is blank) + */ + ClassOrInterfaceTypeDetails getTypeDetails(String physicalTypeId); + + /** + * Returns the {@link LogicalPath} containing the given {@link JavaType}. + * + * @param javaType the {@link JavaType} for which to return the + * {@link LogicalPath} + * @return null if that type doesn't exist in the project + */ + LogicalPath getTypePath(JavaType javaType); + + /** + * Returns the Java types that belong to the given module. + * + * @param module + * @return a non-null collection + * @since 1.2.1 + */ + Collection getTypesForModule(Pom module); + + /** + * Returns the Java types that belong to the given module. + * + * @param modulePath + * @return a non-null collection of fully-qualified type names + * @deprecated use {@link #getTypesForModule(Pom)} instead; more strongly + * typed and also ignores any types found in pom-packaged + * modules + */ + @Deprecated + Collection getTypesForModule(String modulePath); + + /** + * Indicates whether the passed in type has changed since last invocation by + * the requesting class. + * + * @param requestingClass the class requesting the changed types + * @param javaType the type to lookup to see if a change has occurred + * @return a collection of MIDs which represent changed types + */ + boolean hasTypeChanged(String requestingClass, JavaType javaType); + + /** + * Indicates whether the given type exists anywhere in the user project + * + * @param javaType the type to check for (can be null) + * @return false if a null type is given + */ + boolean isInProject(JavaType javaType); + + /** + * Processes types with the specified list of annotations and uses the + * supplied {@link LocatedTypeCallback callback} implementation to process + * the located types. + * + * @param annotationsToDetect the list of annotations to detect on a type. + * @param callback the {@link LocatedTypeCallback} to handle the processing + * of the located type + */ + void processTypesWithAnnotation(List annotationsToDetect, + LocatedTypeCallback callback); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeLocationServiceImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeLocationServiceImpl.java new file mode 100644 index 000000000..034c976c1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeLocationServiceImpl.java @@ -0,0 +1,861 @@ +package org.springframework.roo.classpath; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.PhysicalPath; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.NaturalOrderComparator; +import org.springframework.roo.support.util.FileUtils; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link TypeLocationService}. + *

    + * For performance reasons automatically caches the queries. The cache is + * invalidated on changes to the file system. + * + * @author Alan Stewart + * @author Ben Alex + * @author Stefan Schmidt + * @author James Tyrrell + * @since 1.1 + */ +@Component +@Service +public class TypeLocationServiceImpl implements TypeLocationService { + + private static final Logger LOGGER = HandlerUtils + .getLogger(TypeLocationServiceImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + private static final Comparator LENGTH_COMPARATOR = new Comparator() { + public int compare(final String key1, final String key2) { + return Integer.valueOf(key1.length()).compareTo(key2.length()); + } + }; + + private static final String JAVA_FILES_ANT_PATH = "**" + File.separatorChar + + "*.java"; + + /** + * Returns all packages leading up to the given package, e.g. if the given + * package is "com.foo.bar", returns ["com", "com.foo", "com.foo.bar"]. + * + * @param leafPackage the fully-qualified package to parse (required) + * @return a non-null iterable + */ + static Set getAllPackages(final String leafPackage) { + final Set discoveredPackages = new HashSet(); + final String[] typeSegments = leafPackage.split("\\."); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < typeSegments.length; i++) { + final String typeSegment = typeSegments[i]; + if (i > 0) { + sb.append("."); + } + sb.append(typeSegment); + discoveredPackages.add(sb.toString()); + } + return discoveredPackages; + } + + /** + * Returns the lowest-level package that contains the given number of types. + * + * @param typeCount the number of types to look for + * @param typesByPackage maps package names to type names; a given type will + * appear under all of its parent packages (required) + * @return null if no package contains the given number of + * types + */ + static String getLowestCommonPackage(final int typeCount, + final Map> typesByPackage) { + final Map> typesBySortedPackage = sortByKeyLength(typesByPackage); + int longestPackage = 0; + String topLevelPackage = null; + for (final Entry> entry : typesBySortedPackage + .entrySet()) { + final String thisPackage = entry.getKey(); + final Collection typesInPackage = entry.getValue(); + if (typesInPackage.size() == typeCount + && thisPackage.length() > longestPackage) { + longestPackage = thisPackage.length(); + topLevelPackage = thisPackage; + } + } + return topLevelPackage; + } + + static String getPackageFromType(final String typeName) { + return typeName.substring(0, typeName.lastIndexOf('.')); + } + + /** + * Sorts the given string-keyed map by the length of its keys. + * + * @param the type of the map's values + * @param mapToSort the map to sort (not changed) + * @return the sorted version of the map + */ + static Map sortByKeyLength(final Map mapToSort) { + final List keys = new ArrayList(mapToSort.keySet()); + Collections.sort(keys, LENGTH_COMPARATOR); + final Map sortedMap = new LinkedHashMap(); + for (final String key : keys) { + sortedMap.put(key, mapToSort.get(key)); + } + return sortedMap; + } + + private FileManager fileManager; + private FileMonitorService fileMonitorService; + private MetadataService metadataService; + private ProjectOperations projectOperations; + private TypeCache typeCache; + private TypeResolutionService typeResolutionService; + + private final Map> annotationToMidMap = new HashMap>(); + private final Map> changeMap = new HashMap>(); + private final Set dirtyFiles = new HashSet(); + private final Set discoveredTypes = new HashSet(); + private final Map> typeCustomDataMap = new HashMap>(); + private final Map> tagToMidMap = new HashMap>(); + private final Map> typeAnnotationMap = new HashMap>(); + + private void cacheType(final String fileCanonicalPath) { + Validate.notBlank(fileCanonicalPath, "File canonical path required"); + if (doesPathIndicateJavaType(fileCanonicalPath)) { + final String id = getPhysicalTypeIdentifier(fileCanonicalPath); + if (id != null && PhysicalTypeIdentifier.isValid(id)) { + // Change to Java, so drop the cache + final ClassOrInterfaceTypeDetails cid = lookupClassOrInterfaceTypeDetails(id); + if (cid == null) { + if (!getFileManager().exists(fileCanonicalPath)) { + getTypeCache().removeType(id); + final JavaType type = getTypeCache().getTypeDetails(id) + .getName(); + updateChanges(type.getFullyQualifiedTypeName(), true); + } + return; + } + getTypeCache().cacheType(fileCanonicalPath, cid); + updateAttributeCache(cid); + updateChanges(cid.getName().getFullyQualifiedTypeName(), false); + } + } + } + + private Set discoverTypes() { + // Retrieve a list of paths that have been discovered or modified since + // the last invocation by this class + for (final String change : getFileMonitorService() + .getDirtyFiles(TypeLocationServiceImpl.class.getName())) { + if (doesPathIndicateJavaType(change)) { + discoveredTypes.add(change); + dirtyFiles.add(change); + } + } + return discoveredTypes; + } + + private boolean doesPathIndicateJavaType(final String fileCanonicalPath) { + Validate.notBlank(fileCanonicalPath, "File canonical path required"); + return fileCanonicalPath.endsWith(".java") + && !fileCanonicalPath.endsWith("package-info.java") + && JavaSymbolName + .isLegalJavaName(getProposedJavaType(fileCanonicalPath)); + } + + public Set findClassesOrInterfaceDetailsWithAnnotation( + final JavaType... annotationsToDetect) { + final List types = new ArrayList(); + processTypesWithAnnotation(Arrays.asList(annotationsToDetect), + new LocatedTypeCallback() { + public void process( + final ClassOrInterfaceTypeDetails located) { + if (located != null) { + types.add(located); + } + } + }); + Collections.sort(types, + new NaturalOrderComparator() { + @Override + protected String stringify( + final ClassOrInterfaceTypeDetails object) { + return object.getName().getSimpleTypeName(); + } + }); + + return Collections + .unmodifiableSet(new LinkedHashSet( + types)); + } + + public Set findClassesOrInterfaceDetailsWithTag( + final Object tag) { + Validate.notNull(tag, "Tag required"); + final Set types = new LinkedHashSet(); + processTypesWithTag(tag, new LocatedTypeCallback() { + public void process(final ClassOrInterfaceTypeDetails located) { + if (located != null) { + types.add(located); + } + } + }); + return Collections.unmodifiableSet(types); + } + + public Set findTypesWithAnnotation( + final JavaType... annotationsToDetect) { + return findTypesWithAnnotation(Arrays.asList(annotationsToDetect)); + } + + public Set findTypesWithAnnotation( + final List annotationsToDetect) { + Validate.notNull(annotationsToDetect, "Annotations to detect required"); + final Set types = new LinkedHashSet(); + processTypesWithAnnotation(annotationsToDetect, + new LocatedTypeCallback() { + public void process( + final ClassOrInterfaceTypeDetails located) { + if (located != null) { + types.add(located.getName()); + } + } + }); + return Collections.unmodifiableSet(types); + } + + private String getParentPath(final JavaType javaType) { + final String relativePath = javaType.getRelativeFileName(); + for (final String typePath : discoverTypes()) { + if (typePath.endsWith(relativePath)) { + return StringUtils.removeEnd(typePath, relativePath); + } + } + return null; + } + + private PhysicalPath getPhysicalPath(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + final String parentPath = getParentPath(javaType); + if (parentPath == null) { + return null; + } + for (final Pom pom : getProjectOperations().getPoms()) { + for (final PhysicalPath physicalPath : pom.getPhysicalPaths()) { + if (physicalPath.isSource()) { + final String pathLocation = FileUtils + .ensureTrailingSeparator(physicalPath + .getLocationPath()); + if (pathLocation.startsWith(parentPath)) { + getTypeCache().cacheTypeAgainstModule(pom, javaType); + return physicalPath; + } + } + } + } + return null; + } + + public String getPhysicalTypeCanonicalPath(final JavaType javaType, + final LogicalPath path) { + return getPhysicalTypeCanonicalPath(PhysicalTypeIdentifier + .createIdentifier(javaType, path)); + } + + public String getPhysicalTypeCanonicalPath(final String physicalTypeId) { + final LogicalPath logicalPath = PhysicalTypeIdentifier + .getPath(physicalTypeId); + final JavaType javaType = PhysicalTypeIdentifier + .getJavaType(physicalTypeId); + final Pom pom = getProjectOperations().getPomFromModuleName(logicalPath + .getModule()); + final String canonicalFilePath = pom.getPathLocation(logicalPath + .getPath()) + javaType.getRelativeFileName(); + if (getFileManager().exists(canonicalFilePath)) { + getTypeCache().cacheTypeAgainstModule(pom, javaType); + getTypeCache().cacheFilePathAgainstTypeIdentifier(canonicalFilePath, + physicalTypeId); + } + return canonicalFilePath; + } + + public String getPhysicalTypeIdentifier(final JavaType type) { + final PhysicalPath containingPhysicalPath = getPhysicalPath(type); + if (containingPhysicalPath == null) { + return null; + } + final LogicalPath logicalPath = containingPhysicalPath.getLogicalPath(); + return PhysicalTypeIdentifier.createIdentifier(type, logicalPath); + } + + public String getPhysicalTypeIdentifier(final String fileCanonicalPath) { + Validate.notBlank(fileCanonicalPath, "File canonical path required"); + if (!doesPathIndicateJavaType(fileCanonicalPath)) { + return null; + } + String physicalTypeIdentifier = getTypeCache() + .getTypeIdFromTypeFilePath(fileCanonicalPath); + if (physicalTypeIdentifier != null) { + return physicalTypeIdentifier; + } + final String typeDirectory = FileUtils + .getFirstDirectory(fileCanonicalPath); + final String simpleTypeName = StringUtils.replace(fileCanonicalPath, + typeDirectory + File.separator, "", 1).replace(".java", ""); + final JavaPackage javaPackage = getTypeResolutionService() + .getPackage(fileCanonicalPath); + if (javaPackage == null) { + return null; + } + final JavaType javaType = new JavaType( + javaPackage.getFullyQualifiedPackageName() + "." + + simpleTypeName); + final Pom module = getProjectOperations() + .getModuleForFileIdentifier(fileCanonicalPath); + Validate.notNull(module, "The module for the file '" + + fileCanonicalPath + "' could not be located"); + getTypeCache().cacheTypeAgainstModule(module, javaType); + + String reducedPath = fileCanonicalPath.replace( + javaType.getRelativeFileName(), ""); + reducedPath = StringUtils.stripEnd(reducedPath, File.separator); + + for (final PhysicalPath physicalPath : module.getPhysicalPaths()) { + if (physicalPath.getLocationPath().startsWith(reducedPath)) { + final LogicalPath path = physicalPath.getLogicalPath(); + physicalTypeIdentifier = MetadataIdentificationUtils.create( + PhysicalTypeIdentifier.class.getName(), path.getName() + + "?" + javaType.getFullyQualifiedTypeName()); + break; + } + } + getTypeCache().cacheFilePathAgainstTypeIdentifier(fileCanonicalPath, + physicalTypeIdentifier); + + return physicalTypeIdentifier; + } + + public List getPotentialTopLevelPackagesForModule(final Pom module) { + Validate.notNull(module, "Module required"); + + final Map> packageMap = new HashMap>(); + final Set moduleTypes = getTypesForModule(module.getPath()); + final List topLevelPackages = new ArrayList(); + if (moduleTypes.isEmpty()) { + topLevelPackages.add(module.getGroupId()); + return topLevelPackages; + } + for (final String typeName : moduleTypes) { + final StringBuilder sb = new StringBuilder(); + final String type = getPackageFromType(typeName); + final String[] typeSegments = type.split("\\."); + final Set discoveredPackages = new HashSet(); + for (int i = 0; i < typeSegments.length; i++) { + final String typeSegment = typeSegments[i]; + if (i > 0) { + sb.append("."); + } + sb.append(typeSegment); + discoveredPackages.add(sb.toString()); + } + + for (final String discoveredPackage : discoveredPackages) { + if (!packageMap.containsKey(discoveredPackage)) { + packageMap.put(discoveredPackage, new HashSet()); + } + packageMap.get(discoveredPackage).add(typeName); + } + } + + int longestPackage = 0; + for (final Map.Entry> entry : packageMap.entrySet()) { + if (entry.getValue().size() == moduleTypes.size()) { + topLevelPackages.add(entry.getKey()); + if (entry.getKey().length() > longestPackage) { + longestPackage = entry.getKey().length(); + } + } + } + return topLevelPackages; + } + + private String getProposedJavaType(final String fileCanonicalPath) { + Validate.notBlank(fileCanonicalPath, "File canonical path required"); + // Determine the JavaType for this file + String relativePath = ""; + final Pom moduleForFileIdentifier = getProjectOperations() + .getModuleForFileIdentifier(fileCanonicalPath); + if (moduleForFileIdentifier == null) { + return relativePath; + } + + for (final PhysicalPath physicalPath : moduleForFileIdentifier + .getPhysicalPaths()) { + final String moduleCanonicalPath = FileUtils + .ensureTrailingSeparator(FileUtils + .getCanonicalPath(physicalPath.getLocation())); + if (fileCanonicalPath.startsWith(moduleCanonicalPath)) { + relativePath = File.separator + + StringUtils.replace(fileCanonicalPath, + moduleCanonicalPath, "", 1); + break; + } + } + Validate.notBlank(relativePath, + "Could not determine compilation unit name for file '%s'", + fileCanonicalPath); + Validate.isTrue( + relativePath.startsWith(File.separator), + "Relative path unexpectedly dropped the '%s' prefix (received '%s' from '%s')", + File.separator, relativePath, fileCanonicalPath); + relativePath = relativePath.substring(1); + Validate.isTrue( + relativePath.endsWith(".java"), + "The relative path unexpectedly dropped the .java extension for file '%s'", + fileCanonicalPath); + relativePath = relativePath.substring(0, + relativePath.lastIndexOf(".java")); + return relativePath.replace(File.separatorChar, '.'); + } + + public String getTopLevelPackageForModule(final Pom module) { + return module.getGroupId(); + } + + /** + * Builds a map of a module's packages and all types anywhere within them, + * e.g. if there's two types com.foo.bar.A and com.foo.baz.B, the map will + * look like this: "com": {"com.foo.bar.A", "com.foo.baz.B"} "com.foo": + * {"com.foo.bar.A", "com.foo.baz.B"}, "com.foo.bar": {"com.foo.bar.A"}, + * "com.foo.baz": {"com.foo.baz.B"} All packages that directly contain a + * type are added to the given set. + * + * @param typesInModule the Java types within the module in question + * @param typePackages the set to which to add each type's package + * @return a non-null map laid out as above + */ + @SuppressWarnings("unused") + private Map> getTypesByPackage( + final Iterable typesInModule, final Set typePackages) { + final Map> typesByPackage = new HashMap>(); + for (final String typeName : typesInModule) { + final String typePackage = getPackageFromType(typeName); + typePackages.add(typePackage); + for (final String discoveredPackage : getAllPackages(typePackage)) { + if (typesByPackage.get(discoveredPackage) == null) { + typesByPackage + .put(discoveredPackage, new HashSet()); + } + typesByPackage.get(discoveredPackage).add(typeName); + } + } + return typesByPackage; + } + + public ClassOrInterfaceTypeDetails getTypeDetails(final JavaType type) { + return getTypeDetails(getPhysicalTypeIdentifier(type)); + } + + public ClassOrInterfaceTypeDetails getTypeDetails( + final String physicalTypeId) { + if (StringUtils.isBlank(physicalTypeId)) { + return null; + } + Validate.isTrue(PhysicalTypeIdentifier.isValid(physicalTypeId), + "Metadata id '%s' is not a valid physical type id", + physicalTypeId); + updateTypeCache(); + final ClassOrInterfaceTypeDetails cachedDetails = getTypeCache() + .getTypeDetails(physicalTypeId); + if (cachedDetails != null) { + return cachedDetails; + } + final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService() + .get(physicalTypeId); + if (physicalTypeMetadata == null) { + return null; + } + return physicalTypeMetadata.getMemberHoldingTypeDetails(); + } + + public LogicalPath getTypePath(final JavaType javaType) { + final String physicalTypeId = getPhysicalTypeIdentifier(javaType); + if (StringUtils.isBlank(physicalTypeId)) { + return null; + } + return PhysicalTypeIdentifier.getPath(physicalTypeId); + } + + public Collection getTypesForModule(final Pom module) { + if ("pom".equals(module.getPackaging())) { + return Collections.emptySet(); + } + final Set typeNames = getTypesForModule(module.getPath()); + final Collection javaTypes = new ArrayList(); + for (final String typeName : typeNames) { + javaTypes.add(new JavaType(typeName)); + } + return javaTypes; + } + + public Set getTypesForModule(final String modulePath) { + Validate.notNull(modulePath, "Module path required"); + return getTypeCache().getTypeNamesForModuleFilePath(modulePath); + } + + public boolean hasTypeChanged(final String requestingClass, + final JavaType javaType) { + Validate.notNull(requestingClass, "Requesting class required"); + Validate.notNull(javaType, "Java type required"); + + updateTypeCache(); + Set changesSinceLastRequest = changeMap.get(requestingClass); + if (changesSinceLastRequest == null) { + changesSinceLastRequest = new LinkedHashSet(); + for (final String typeIdentifier : getTypeCache() + .getAllTypeIdentifiers()) { + changesSinceLastRequest.add(getTypeCache() + .getTypeDetails(typeIdentifier).getName() + .getFullyQualifiedTypeName()); + } + changeMap.put(requestingClass, changesSinceLastRequest); + } + for (final String changedId : changesSinceLastRequest) { + if (changedId.equals(javaType.getFullyQualifiedTypeName())) { + changesSinceLastRequest.remove(changedId); + return true; + } + } + return false; + } + + private void initTypeMap() { + for (final Pom pom : getProjectOperations().getPoms()) { + for (final PhysicalPath path : pom.getPhysicalPaths()) { + if (path.isSource()) { + final String allJavaFiles = FileUtils + .ensureTrailingSeparator(path.getLocationPath()) + + JAVA_FILES_ANT_PATH; + for (final FileDetails file : getFileManager() + .findMatchingAntPath(allJavaFiles)) { + cacheType(file.getCanonicalPath()); + } + } + } + } + } + + public boolean isInProject(final JavaType javaType) { + return javaType != null && !javaType.isCoreType() + && getPhysicalPath(javaType) != null; + } + + /** + * Obtains the a fresh copy of the {@link ClassOrInterfaceTypeDetails} for + * the given physical type. + * + * @param physicalTypeIdentifier to lookup (required) + * @return the requested details (or null if unavailable) + */ + private ClassOrInterfaceTypeDetails lookupClassOrInterfaceTypeDetails( + final String physicalTypeIdentifier) { + final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService() + .evictAndGet(physicalTypeIdentifier); + if (physicalTypeMetadata == null) { + return null; + } + return physicalTypeMetadata.getMemberHoldingTypeDetails(); + } + + public void processTypesWithAnnotation( + final List annotationsToDetect, + final LocatedTypeCallback callback) { + Validate.notNull(annotationsToDetect, "Annotations to detect required"); + Validate.notNull(callback, "Callback required"); + // If the cache doesn't yet contain the annotation to be found it should + // be added + for (final JavaType annotationType : annotationsToDetect) { + if (!annotationToMidMap.containsKey(annotationType)) { + annotationToMidMap.put(annotationType, new HashSet()); + } + } + + // Before processing the call any changes to the project should be + // processed and the cache updated accordingly + updateTypeCache(); + + for (final JavaType annotationType : annotationsToDetect) { + for (final String locatedMid : annotationToMidMap + .get(annotationType)) { + final ClassOrInterfaceTypeDetails located = getTypeCache() + .getTypeDetails(locatedMid); + callback.process(located); + } + } + } + + private void processTypesWithTag(final Object tag, + final LocatedTypeCallback callback) { + Validate.notNull(tag, "Tag required"); + Validate.notNull(callback, "Callback required"); + // If the cache doesn't yet contain the tag it should be added + if (!tagToMidMap.containsKey(tag)) { + tagToMidMap.put(tag, new HashSet()); + } + + // Before processing the call any changes to the project should be + // processed and the cache updated accordingly + updateTypeCache(); + + for (final String locatedMid : tagToMidMap.get(tag)) { + final ClassOrInterfaceTypeDetails located = getTypeCache() + .getTypeDetails(locatedMid); + callback.process(located); + } + } + + private void updateAttributeCache(final MemberHoldingTypeDetails cid) { + Validate.notNull(cid, "Member holding type details required"); + if (!typeAnnotationMap.containsKey(cid.getDeclaredByMetadataId())) { + typeAnnotationMap.put(cid.getDeclaredByMetadataId(), + new HashSet()); + } + if (!typeCustomDataMap.containsKey(cid.getDeclaredByMetadataId())) { + typeCustomDataMap.put(cid.getDeclaredByMetadataId(), + new HashSet()); + } + final Set previousAnnotations = typeAnnotationMap.get(cid + .getDeclaredByMetadataId()); + for (final JavaType previousAnnotation : previousAnnotations) { + final Set midSet = annotationToMidMap + .get(previousAnnotation); + if (midSet != null) { + midSet.remove(cid.getDeclaredByMetadataId()); + } + } + previousAnnotations.clear(); + for (final AnnotationMetadata annotationMetadata : cid.getAnnotations()) { + if (!annotationToMidMap.containsKey(annotationMetadata + .getAnnotationType())) { + annotationToMidMap.put(annotationMetadata.getAnnotationType(), + new HashSet()); + } + previousAnnotations.add(annotationMetadata.getAnnotationType()); + annotationToMidMap.get(annotationMetadata.getAnnotationType()).add( + cid.getDeclaredByMetadataId()); + } + final Set previousCustomDataSet = typeCustomDataMap.get(cid + .getDeclaredByMetadataId()); + for (final Object previousCustomData : previousCustomDataSet) { + final Set midSet = tagToMidMap.get(previousCustomData); + if (midSet != null) { + midSet.remove(cid.getDeclaredByMetadataId()); + } + } + previousCustomDataSet.clear(); + for (final Object customData : cid.getCustomData().keySet()) { + if (!tagToMidMap.containsKey(customData)) { + tagToMidMap.put(customData, new HashSet()); + } + previousCustomDataSet.add(customData); + tagToMidMap.get(customData).add(cid.getDeclaredByMetadataId()); + } + } + + private void updateChanges(final String typeName, final boolean remove) { + Validate.notNull(typeName, "Type name required"); + for (final String requestingClass : changeMap.keySet()) { + if (remove) { + changeMap.get(requestingClass).remove(typeName); + } + else { + changeMap.get(requestingClass).add(typeName); + } + } + } + + private void updateTypeCache() { + if (getTypeCache().getAllTypeIdentifiers().isEmpty()) { + initTypeMap(); + } + discoverTypes(); + // Update the type cache + for (final String change : dirtyFiles) { + cacheType(change); + } + dirtyFiles.clear(); + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on TypeLocationServiceImpl."); + return null; + } + }else{ + return fileManager; + } + } + + public FileMonitorService getFileMonitorService(){ + if(fileMonitorService == null){ + // Get all Services implement FileMonitorService interface + try { + ServiceReference[] references = context.getAllServiceReferences(FileMonitorService.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileMonitorService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileMonitorService on TypeLocationServiceImpl."); + return null; + } + }else{ + return fileMonitorService; + } + } + + public MetadataService getMetadataService(){ + if(metadataService == null){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on TypeLocationServiceImpl."); + return null; + } + }else{ + return metadataService; + } + } + + public ProjectOperations getProjectOperations(){ + if(projectOperations == null){ + // Get all Services implement ProjectOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ProjectOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProjectOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProjectOperations on TypeLocationServiceImpl."); + return null; + } + }else{ + return projectOperations; + } + } + + public TypeCache getTypeCache(){ + if(typeCache == null){ + // Get all Services implement TypeCache interface + try { + ServiceReference[] references = context.getAllServiceReferences(TypeCache.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeCache) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeCache on TypeLocationServiceImpl."); + return null; + } + }else{ + return typeCache; + } + } + + public TypeResolutionService getTypeResolutionService(){ + if(typeResolutionService == null){ + // Get all Services implement TypeResolutionService interface + try { + ServiceReference[] references = context.getAllServiceReferences(TypeResolutionService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeResolutionService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeResolutionService on TypeLocationServiceImpl."); + return null; + } + }else{ + return typeResolutionService; + } + } + +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeManagementService.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeManagementService.java new file mode 100644 index 000000000..16f06c5a3 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeManagementService.java @@ -0,0 +1,54 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Creates and maintains types. + * + * @author Alan Stewart + * @since 1.1.2 + */ +public interface TypeManagementService { + + /** + * Adds a new enum constant to an existing class. + * + * @param physicalTypeIdentifier to add (required) + * @param constantName the name of the constant (required) + */ + void addEnumConstant(String physicalTypeIdentifier, + JavaSymbolName constantName); + + /** + * Adds a new field to an existing class. + *

    + * An exception is thrown if the class does not exist, cannot be modified or + * a field with the requested name is already declared. + * + * @param field the field to add (required) + */ + void addField(FieldMetadata field); + + /** + * Creates a physical type with the contents based on the + * {@link ClassOrInterfaceTypeDetails} passed in at the location denoted by + * the passed in path. This method expects the passed in file location to be + * correct. + * + * @param cid {@link ClassOrInterfaceTypeDetails} to base the file contents + * on (required) + */ + void createOrUpdateTypeOnDisk(ClassOrInterfaceTypeDetails cid); + + /** + * Creates a new class, with the location name name provided in the details. + *

    + * An exception is thrown if the class already exists. + * + * @param cid the {@link ClassOrInterfaceTypeDetails} to create (required) + */ + @Deprecated + void generateClassFile(ClassOrInterfaceTypeDetails cid); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeManagementServiceImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeManagementServiceImpl.java new file mode 100644 index 000000000..b5d143313 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeManagementServiceImpl.java @@ -0,0 +1,129 @@ +package org.springframework.roo.classpath; + +import java.io.File; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.ProjectOperations; + +/** + * Implementation of {@link TypeManagementService}. + * + * @author Alan Stewart + * @since 1.1.2 + */ +@Component +@Service +public class TypeManagementServiceImpl implements TypeManagementService { + + @Reference private FileManager fileManager; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeParsingService typeParsingService; + + public void addEnumConstant(final String physicalTypeIdentifier, + final JavaSymbolName constantName) { + Validate.notBlank(physicalTypeIdentifier, + "Type identifier not provided"); + Validate.notNull(constantName, "Constant name required"); + + // Obtain the physical type and itd mutable details + final PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService + .get(physicalTypeIdentifier); + Validate.notNull(ptm, "Java source code unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + final PhysicalTypeDetails ptd = ptm.getMemberHoldingTypeDetails(); + Validate.notNull(ptd, + "Java source code details unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + (ClassOrInterfaceTypeDetails) ptd); + + // Ensure it's an enum + Validate.isTrue( + cidBuilder.getPhysicalTypeCategory() == PhysicalTypeCategory.ENUMERATION, + "%s is not an enum", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + + cidBuilder.addEnumConstant(constantName); + createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void addField(final FieldMetadata field) { + Validate.notNull(field, "Field metadata not provided"); + + // Obtain the physical type and ITD mutable details + final PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService + .get(field.getDeclaredByMetadataId()); + Validate.notNull(ptm, "Java source code unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(field + .getDeclaredByMetadataId())); + final PhysicalTypeDetails ptd = ptm.getMemberHoldingTypeDetails(); + Validate.notNull(ptd, + "Java source code details unavailable for type %s", + PhysicalTypeIdentifier.getFriendlyName(field + .getDeclaredByMetadataId())); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + (ClassOrInterfaceTypeDetails) ptd); + + // Automatically add JSR 303 (Bean Validation API) support if there is + // no current JSR 303 support but a JSR 303 annotation is present + boolean jsr303Required = false; + for (final AnnotationMetadata annotation : field.getAnnotations()) { + if (annotation.getAnnotationType().getFullyQualifiedTypeName() + .startsWith("javax.validation")) { + jsr303Required = true; + break; + } + } + + final LogicalPath path = PhysicalTypeIdentifier.getPath(cidBuilder + .getDeclaredByMetadataId()); + + if (jsr303Required) { + // It's more likely the version below represents a later version + // than any specified in the user's own dependency list + projectOperations.addDependency(path.getModule(), + "javax.validation", "validation-api", "1.0.0.GA"); + } + cidBuilder.addField(field); + createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + public void createOrUpdateTypeOnDisk(final ClassOrInterfaceTypeDetails cid) { + final String fileCanonicalPath = typeLocationService + .getPhysicalTypeCanonicalPath(cid.getDeclaredByMetadataId()); + String newContents; + File file; + boolean existsFile = false; + if (fileCanonicalPath != null) { + file = new File(fileCanonicalPath); + existsFile = file.exists() && file.isFile(); + } + if (existsFile) { + newContents = typeParsingService + .updateAndGetCompilationUnitContents(fileCanonicalPath, cid); + } + else { + newContents = typeParsingService.getCompilationUnitContents(cid); + } + fileManager.createOrUpdateTextFileIfRequired(fileCanonicalPath, + newContents, true); + } + + @Deprecated + public void generateClassFile(final ClassOrInterfaceTypeDetails cid) { + createOrUpdateTypeOnDisk(cid); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeParsingService.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeParsingService.java new file mode 100644 index 000000000..05f1b2979 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeParsingService.java @@ -0,0 +1,74 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaType; + +public interface TypeParsingService { + + /** + * Returns the compilation unit contents that represents the passed class or + * interface details. This is useful if an add-on requires a compilation + * unit representation but doesn't wish to cause that representation to be + * emitted to disk via {@link TypeManagementService}. One concrete time this + * is useful is when an add-on wishes to emulate an ITD-like model for an + * external system that cannot support ITDs and may wish to insert a custom + * header etc before writing it to disk. + * + * @param cid a parsed representation of a class or interface (required) + * @return a valid Java compilation unit for the passed object (never null + * or empty) + */ + String getCompilationUnitContents(ClassOrInterfaceTypeDetails cid); + + /** + * Builds a {@link ClassOrInterfaceTypeDetails} object that represents the + * requested {@link org.springframework.roo.model.JavaType} from the type at + * the passed in type path. + * + * @param fileIdentifier the location of the type to be parsed (required) + * @param declaredByMetadataId the metadata ID that should be used in the + * returned object (required) + * @param javaType the Java type to locate in the compilation unit and parse + * (required) + * @return a parsed representation of the requested type from the passed + * compilation unit (never null) + */ + ClassOrInterfaceTypeDetails getTypeAtLocation(String fileIdentifier, + String declaredByMetadataId, JavaType javaType); + + /** + * Builds a {@link ClassOrInterfaceTypeDetails} object that represents the + * requested {@link org.springframework.roo.model.JavaType} from the passed + * compilation unit text. This is useful if an add-on wishes to parse some + * arbitrary compilation unit contents it acquired from outside the user + * project, such as a template that ships with the add-on. The add-on can + * subsequently modify the returned object (via the builder) and eventually + * write the final version to the user's project. This therefore allows more + * elegant add-on usage patterns, as they need not write "stub" compilation + * units into a user project simply to parse them for subsequent re-writing. + * + * @param typeContents the text of a legal Java compilation unit (required) + * @param declaredByMetadataId the metadata ID that should be used in the + * returned object (required) + * @param javaType the Java type to locate in the compilation unit and parse + * (required) + * @return a parsed representation of the requested type from the passed + * compilation unit (never null) + */ + ClassOrInterfaceTypeDetails getTypeFromString(String typeContents, + String declaredByMetadataId, JavaType javaType); + + /** + * Returns the compilation unit contents that represents the java file + * updated with the passed class or interface details. The difference with + * {@link #getCompilationUnitContents(ClassOrInterfaceTypeDetails)} is that + * this method doesn't create a new compilation unit but tries to update the + * original return the final file contents. + * + * @param fileIdentifier canonical path of file + * @param cid a parsed representation of a class or interface (required) + * @return a valid Java compilation unit contents (never null or empty) + */ + String updateAndGetCompilationUnitContents(String fileIdentifier, + ClassOrInterfaceTypeDetails cid); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/TypeResolutionService.java b/classpath/src/main/java/org/springframework/roo/classpath/TypeResolutionService.java new file mode 100644 index 000000000..451486a4d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/TypeResolutionService.java @@ -0,0 +1,11 @@ +package org.springframework.roo.classpath; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +public interface TypeResolutionService { + + JavaType getJavaType(String fileIdentifier); + + JavaPackage getPackage(String fileIdentifier); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/ConstructorFieldsConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/ConstructorFieldsConverter.java new file mode 100644 index 000000000..441b7702a --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/ConstructorFieldsConverter.java @@ -0,0 +1,46 @@ +package org.springframework.roo.classpath.converters; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion between a space-separated list of field names to a set of + * field names for use in a constructor. + * + * @author Alan Stewart + * @since 1.2.3 + */ +@Component +@Service +public class ConstructorFieldsConverter implements Converter> { + + public Set convertFromText(final String value, + final Class requiredType, final String optionContext) { + final Set fields = new LinkedHashSet(); + final StringTokenizer st = new StringTokenizer(value, " "); + while (st.hasMoreTokens()) { + fields.add(st.nextToken()); + } + return fields; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Set.class.isAssignableFrom(requiredType) + && optionContext.contains("constructor-fields"); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaPackageConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaPackageConverter.java new file mode 100644 index 000000000..efddf70f4 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaPackageConverter.java @@ -0,0 +1,121 @@ +package org.springframework.roo.classpath.converters; + +import static org.springframework.roo.shell.OptionContexts.UPDATE; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * A {@link Converter} for {@link JavaPackage}s, with support for using + * {@value #TOP_LEVEL_PACKAGE_SYMBOL} to denote the user's top-level package. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class JavaPackageConverter implements Converter { + + /** + * The shell character that represents the current project or module's top + * level Java package. + */ + public static final String TOP_LEVEL_PACKAGE_SYMBOL = "~"; + + @Reference FileManager fileManager; + @Reference LastUsed lastUsed; + @Reference ProjectOperations projectOperations; + @Reference TypeLocationService typeLocationService; + + public JavaPackage convertFromText(final String value, + final Class requiredType, final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + final JavaPackage result = new JavaPackage( + convertToFullyQualifiedPackageName(value)); + if (optionContext != null && optionContext.contains(UPDATE)) { + lastUsed.setPackage(result); + } + return result; + } + + private String convertToFullyQualifiedPackageName(final String text) { + final String normalisedText = StringUtils.removeEnd(text, ".") + .toLowerCase(); + if (normalisedText.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) { + return replaceTopLevelPackageSymbol(normalisedText); + } + return normalisedText; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + if (projectOperations.isFocusedProjectAvailable()) { + completions.addAll(getCompletionsForAllKnownPackages()); + } + return false; + } + + private Collection getCompletionsForAllKnownPackages() { + final Collection completions = new LinkedHashSet(); + for (final Pom pom : projectOperations.getPoms()) { + for (final JavaType javaType : typeLocationService + .getTypesForModule(pom)) { + final String type = javaType.getFullyQualifiedTypeName(); + completions.add(new Completion(type.substring(0, + type.lastIndexOf('.')))); + } + } + return completions; + } + + private String getTopLevelPackage() { + if (projectOperations.isFocusedProjectAvailable()) { + return typeLocationService + .getTopLevelPackageForModule(projectOperations + .getFocusedModule()); + } + // Shouldn't happen if there's a project, i.e. most of the time + return ""; + } + + /** + * Replaces the {@link #TOP_LEVEL_PACKAGE_SYMBOL} at the beginning of the + * given text with the current project/module's top-level package + * + * @param text + * @return a well-formed Java package name (might have a trailing dot) + */ + private String replaceTopLevelPackageSymbol(final String text) { + final String topLevelPackage = getTopLevelPackage(); + if (TOP_LEVEL_PACKAGE_SYMBOL.equals(text)) { + return topLevelPackage; + } + final String textWithoutSymbol = StringUtils.removeStart(text, + TOP_LEVEL_PACKAGE_SYMBOL); + return topLevelPackage + "." + + StringUtils.removeStart(textWithoutSymbol, "."); + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return JavaPackage.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaSymbolNameConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaSymbolNameConverter.java new file mode 100644 index 000000000..64124e184 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaSymbolNameConverter.java @@ -0,0 +1,42 @@ +package org.springframework.roo.classpath.converters; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion to and from {@link JavaSymbolName}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class JavaSymbolNameConverter implements Converter { + + public JavaSymbolName convertFromText(final String value, + final Class requiredType, final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + + return new JavaSymbolName(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return JavaSymbolName.class.isAssignableFrom(requiredType); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaTypeConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaTypeConverter.java new file mode 100644 index 000000000..255131733 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/JavaTypeConverter.java @@ -0,0 +1,457 @@ +package org.springframework.roo.classpath.converters; + +import static org.springframework.roo.classpath.converters.JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL; +import static org.springframework.roo.project.LogicalPath.MODULE_PATH_SEPARATOR; +import static org.springframework.roo.shell.OptionContexts.INTERFACE; +import static org.springframework.roo.shell.OptionContexts.PROJECT; +import static org.springframework.roo.shell.OptionContexts.SUPERCLASS; +import static org.springframework.roo.shell.OptionContexts.UPDATE; +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_CYAN; +import static org.springframework.roo.support.util.AnsiEscapeCode.decorate; + +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion to and from {@link JavaType}, with full support for using + * {@value JavaPackageConverter#TOP_LEVEL_PACKAGE_SYMBOL} as denoting the user's + * top-level package. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class JavaTypeConverter implements Converter { + + /** + * The value that converts to the most recently used {@link JavaType}. + */ + static final String LAST_USED_INDICATOR = "*"; + + private static final List NUMBER_PRIMITIVES = Arrays.asList("byte", + "short", "int", "long", "float", "double"); + + @Reference FileManager fileManager; + @Reference LastUsed lastUsed; + @Reference ProjectOperations projectOperations; + @Reference TypeLocationService typeLocationService; + + public JavaType convertFromText(String value, final Class requiredType, + final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + + // Check for number primitives + if (NUMBER_PRIMITIVES.contains(value)) { + return getNumberPrimitiveType(value); + } + + if (LAST_USED_INDICATOR.equals(value)) { + final JavaType result = lastUsed.getJavaType(); + if (result == null) { + throw new IllegalStateException( + "Unknown type; please indicate the type as a command option (ie --xxxx)"); + } + return result; + } + + String topLevelPath; + Pom module = projectOperations.getFocusedModule(); + + if (value.contains(MODULE_PATH_SEPARATOR)) { + final String moduleName = value.substring(0, + value.indexOf(MODULE_PATH_SEPARATOR)); + module = projectOperations.getPomFromModuleName(moduleName); + topLevelPath = typeLocationService + .getTopLevelPackageForModule(module); + value = value.substring(value.indexOf(MODULE_PATH_SEPARATOR) + 1, + value.length()).trim(); + if (StringUtils.contains(optionContext, UPDATE)) { + projectOperations.setModule(module); + } + } + else { + topLevelPath = typeLocationService + .getTopLevelPackageForModule(projectOperations + .getFocusedModule()); + } + + if (value.equals(topLevelPath)) { + return null; + } + + String newValue = locateExisting(value, topLevelPath); + if (newValue == null) { + newValue = locateNew(value, topLevelPath); + } + + if (StringUtils.isNotBlank(newValue)) { + final String physicalTypeIdentifier = typeLocationService + .getPhysicalTypeIdentifier(new JavaType(newValue)); + if (StringUtils.isNotBlank(physicalTypeIdentifier)) { + module = projectOperations + .getPomFromModuleName(PhysicalTypeIdentifier.getPath( + physicalTypeIdentifier).getModule()); + } + } + + // If the user did not provide a java type name containing a dot, it's + // taken as relative to the current package directory + if (!newValue.contains(".")) { + newValue = (lastUsed.getJavaPackage() == null ? lastUsed + .getTopLevelPackage().getFullyQualifiedPackageName() + : lastUsed.getJavaPackage().getFullyQualifiedPackageName()) + + "." + newValue; + } + + // Automatically capitalise the first letter of the last name segment + // (i.e. capitalise the type name, but not the package) + final int index = newValue.lastIndexOf("."); + if (index > -1 && !newValue.endsWith(".")) { + String typeName = newValue.substring(index + 1); + typeName = StringUtils.capitalize(typeName); + newValue = newValue.substring(0, index).toLowerCase() + "." + + typeName; + } + final JavaType result = new JavaType(newValue); + + // ROO-3581: On this time we don't know if current result + // exists as type on generated project. We need to save as + // not verified + if (StringUtils.contains(optionContext, UPDATE)) { + lastUsed.setTypeNotVerified(result, module); + } + return result; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, String existingData, + final String optionContext, final MethodTarget target) { + existingData = StringUtils.stripToEmpty(existingData); + + if (StringUtils.isBlank(optionContext) + || optionContext.contains(PROJECT) + || optionContext.contains(SUPERCLASS) + || optionContext.contains(INTERFACE)) { + completeProjectSpecificPaths(completions, existingData, + optionContext); + } + + if (StringUtils.contains(optionContext, "java")) { + completeJavaSpecificPaths(completions, existingData, optionContext); + } + + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return JavaType.class.isAssignableFrom(requiredType); + } + + private void addCompletionsForOtherModuleNames( + final List completions, final Pom targetModule) { + for (final String moduleName : projectOperations.getModuleNames()) { + if (StringUtils.isNotBlank(moduleName) + && !moduleName.equals(targetModule.getModuleName())) { + completions.add(new Completion(moduleName + + MODULE_PATH_SEPARATOR, decorate(moduleName + + MODULE_PATH_SEPARATOR, FG_CYAN), "Modules", 0)); + } + } + } + + private void addCompletionsForTypesInTargetModule( + final List completions, final String optionContext, + final Pom targetModule, final String heading, final String prefix, + final String formattedPrefix, final String topLevelPackage, + final String basePackage) { + final Collection typesInModule = getTypesForModule( + optionContext, targetModule); + if (typesInModule.isEmpty()) { + completions.add(new Completion(prefix + targetModule.getGroupId(), + formattedPrefix + targetModule.getGroupId(), heading, 1)); + } + else { + completions.add(new Completion(prefix + topLevelPackage, + formattedPrefix + topLevelPackage, heading, 1)); + for (final JavaType javaType : typesInModule) { + String type = javaType.getFullyQualifiedTypeName(); + if (type.startsWith(basePackage)) { + type = StringUtils.replace(type, topLevelPackage, + TOP_LEVEL_PACKAGE_SYMBOL, 1); + completions.add(new Completion(prefix + type, + formattedPrefix + type, heading, 1)); + } + } + } + } + + private Collection getTypesForModule(final String optionContext, + final Pom targetModule) { + final Collection typesForModule = typeLocationService + .getTypesForModule(targetModule); + if (!(optionContext.contains(SUPERCLASS) || optionContext + .contains(INTERFACE))) { + return typesForModule; + } + + final Collection types = new ArrayList(); + for (final JavaType javaType : typesForModule) { + final ClassOrInterfaceTypeDetails typeDetails = typeLocationService + .getTypeDetails(javaType); + if ((optionContext.contains(SUPERCLASS) && (Modifier + .isFinal(typeDetails.getModifier()) || typeDetails + .getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE)) + || (optionContext.contains(INTERFACE) && typeDetails + .getPhysicalTypeCategory() != PhysicalTypeCategory.INTERFACE)) { + continue; + } + types.add(javaType); + } + return types; + } + + /** + * Adds common "java." types to the completions. For now we just provide + * them statically. + */ + private void completeJavaSpecificPaths(final List completions, + final String existingData, String optionContext) { + final SortedSet types = new TreeSet(); + + if (StringUtils.isBlank(optionContext)) { + optionContext = "java-all"; + } + + if (optionContext.contains("java-all") + || optionContext.contains("java-lang")) { + // lang - other + types.add(Boolean.class.getName()); + types.add(String.class.getName()); + } + + if (optionContext.contains("java-all") + || optionContext.contains("java-lang") + || optionContext.contains("java-number")) { + // lang - numeric + types.add(Number.class.getName()); + types.add(Short.class.getName()); + types.add(Byte.class.getName()); + types.add(Integer.class.getName()); + types.add(Long.class.getName()); + types.add(Float.class.getName()); + types.add(Double.class.getName()); + types.add(Byte.TYPE.getName()); + types.add(Short.TYPE.getName()); + types.add(Integer.TYPE.getName()); + types.add(Long.TYPE.getName()); + types.add(Float.TYPE.getName()); + types.add(Double.TYPE.getName()); + } + + if (optionContext.contains("java-all") + || optionContext.contains("java-number")) { + // misc + types.add(BigDecimal.class.getName()); + types.add(BigInteger.class.getName()); + } + + if (optionContext.contains("java-all") + || optionContext.contains("java-util") + || optionContext.contains("java-collections")) { + // util + types.add(Collection.class.getName()); + types.add(List.class.getName()); + types.add(Queue.class.getName()); + types.add(Set.class.getName()); + types.add(SortedSet.class.getName()); + types.add(Map.class.getName()); + } + + if (optionContext.contains("java-all") + || optionContext.contains("java-util") + || optionContext.contains("java-date")) { + // util + types.add(Date.class.getName()); + types.add(Calendar.class.getName()); + } + + for (final String type : types) { + if (type.startsWith(existingData) || existingData.startsWith(type)) { + completions.add(new Completion(type)); + } + } + } + + private void completeProjectSpecificPaths( + final List completions, final String existingData, + final String optionContext) { + if (!projectOperations.isFocusedProjectAvailable()) { + return; + } + final Pom targetModule; + final String heading; + final String prefix; + final String formattedPrefix; + final String typeName; + if (existingData.contains(MODULE_PATH_SEPARATOR)) { + // Looking for a type in another module + final String targetModuleName = existingData.substring(0, + existingData.indexOf(MODULE_PATH_SEPARATOR)); + targetModule = projectOperations + .getPomFromModuleName(targetModuleName); + heading = ""; + prefix = targetModuleName + MODULE_PATH_SEPARATOR; + formattedPrefix = decorate( + targetModuleName + MODULE_PATH_SEPARATOR, FG_CYAN); + typeName = StringUtils.substringAfterLast(existingData, + MODULE_PATH_SEPARATOR); + } + else { + // Looking for a type in the currently focused module + targetModule = projectOperations.getFocusedModule(); + heading = targetModule.getModuleName(); + prefix = ""; + formattedPrefix = ""; + typeName = existingData; + } + final String topLevelPackage = typeLocationService + .getTopLevelPackageForModule(targetModule); + final String basePackage = resolveTopLevelPackageSymbol(typeName, + topLevelPackage); + + addCompletionsForOtherModuleNames(completions, targetModule); + + if (!"pom".equals(targetModule.getPackaging())) { + addCompletionsForTypesInTargetModule(completions, optionContext, + targetModule, heading, prefix, formattedPrefix, + topLevelPackage, basePackage); + } + } + + private JavaType getNumberPrimitiveType(final String value) { + if ("byte".equals(value)) { + return JavaType.BYTE_PRIMITIVE; + } + else if ("short".equals(value)) { + return JavaType.SHORT_PRIMITIVE; + } + else if ("int".equals(value)) { + return JavaType.INT_PRIMITIVE; + } + else if ("long".equals(value)) { + return JavaType.LONG_PRIMITIVE; + } + else if ("float".equals(value)) { + return JavaType.FLOAT_PRIMITIVE; + } + else if ("double".equals(value)) { + return JavaType.DOUBLE_PRIMITIVE; + } + else { + return null; + } + } + + private String locateExisting(final String value, String topLevelPath) { + String newValue = value; + if (value.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) { + boolean found = false; + while (!found) { + if (value.length() > 1) { + newValue = (value.charAt(1) == '.' ? topLevelPath + : topLevelPath + ".") + value.substring(1); + } + else { + newValue = topLevelPath + "."; + } + final String physicalTypeIdentifier = typeLocationService + .getPhysicalTypeIdentifier(new JavaType(newValue)); + if (physicalTypeIdentifier != null) { + topLevelPath = typeLocationService + .getTopLevelPackageForModule(projectOperations + .getPomFromModuleName(PhysicalTypeIdentifier + .getPath(physicalTypeIdentifier) + .getModule())); + found = true; + } + else { + final int index = topLevelPath.lastIndexOf('.'); + if (index == -1) { + break; + } + topLevelPath = topLevelPath.substring(0, + topLevelPath.lastIndexOf('.')); + } + } + if (!found) { + return null; + } + } + + lastUsed.setTopLevelPackage(new JavaPackage(topLevelPath)); + return newValue; + } + + private String locateNew(final String value, final String topLevelPath) { + String newValue = value; + if (value.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) { + if (value.length() > 1) { + newValue = (value.charAt(1) == '.' ? topLevelPath + : topLevelPath + ".") + value.substring(1); + } + else { + newValue = topLevelPath + "."; + } + } + lastUsed.setTopLevelPackage(new JavaPackage(topLevelPath)); + return newValue; + } + + private String resolveTopLevelPackageSymbol(final String existingData, + final String topLevelPackage) { + if (TOP_LEVEL_PACKAGE_SYMBOL.equals(existingData)) { + // existing data = "~" => "com.foo." + return topLevelPackage + "."; + } + if (existingData.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) { + // e.g. turn "~.blah" or "~blah" into "com.foo.blah" + return topLevelPackage + (existingData.charAt(1) == '.' ? "" : ".") + + existingData.substring(1); + } + return existingData; + } + +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/LastUsed.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/LastUsed.java new file mode 100644 index 000000000..9eaadb722 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/LastUsed.java @@ -0,0 +1,64 @@ +package org.springframework.roo.classpath.converters; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.maven.Pom; + +/** + * Interface for {@link LastUsedImpl}. + * + * @author Ben Alex + * @since 1.1 + */ +public interface LastUsed { + + /** + * @return the package, either explicitly set or via a type set (may also be + * null if never set) + */ + JavaPackage getJavaPackage(); + + /** + * @return the type or null + */ + JavaType getJavaType(); + + JavaPackage getTopLevelPackage(); + + /** + * Sets the package, and clears the type field. Ignores attempts to set to + * java.*. + */ + void setPackage(JavaPackage javaPackage); + + void setTopLevelPackage(JavaPackage topLevelPackage); + + /** + * Sets the type, and also sets the package field. Ignores attempts to set + * to java.*. + */ + void setType(JavaType javaType); + + /** + * Sets the type, and also sets the package field. Ignores attempts to set + * to java.*. But with not verified + */ + void setTypeNotVerified(JavaType javaType); + + /** + * Sets the last used type and the module to which it belongs + * + * @param javaType + * @param module + */ + void setType(JavaType javaType, Pom module); + + /** + * Sets the last used type and the module to which it belongs not verified + * + * @param result + * @param module + */ + void setTypeNotVerified(JavaType result, Pom module); + +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/LastUsedImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/LastUsedImpl.java new file mode 100644 index 000000000..33495440b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/LastUsedImpl.java @@ -0,0 +1,214 @@ +package org.springframework.roo.classpath.converters; + +import static org.springframework.roo.classpath.converters.JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL; +import static org.springframework.roo.project.LogicalPath.MODULE_PATH_SEPARATOR; + +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.CommandListener; +import org.springframework.roo.shell.ParseResult; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.AnsiEscapeCode; + +/** + * Records the last Java package and type used. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class LastUsedImpl implements LastUsed, CommandListener { + + private static final Logger LOGGER = HandlerUtils + .getLogger(LastUsedImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void deactivate(final ComponentContext cContext) { + if (shell != null) { + shell.removeListener(this); + } + } + + // Verified fields + private JavaPackage topLevelPackage; + private JavaPackage javaPackage; + private JavaType javaType; + private Pom module; + + // Not Verified fields + private JavaPackage topLevelPackageNotVerified; + private JavaPackage javaPackageNotVerified; + private JavaType javaTypeNotVerified; + private Pom moduleNotVerified; + + private boolean isVerified; + + @Reference private ProjectOperations projectOperations; + @Reference private Shell shell; + @Reference private TypeLocationService typeLocationService; + + private boolean listenerRegistered; + + public JavaPackage getJavaPackage() { + return javaPackage; + } + + public JavaType getJavaType() { + return javaType; + } + + public JavaPackage getTopLevelPackage() { + return topLevelPackage; + } + + public JavaPackage getJavaPackageNotVerified() { + return javaPackageNotVerified; + } + + public JavaType getJavaTypeNotVerified() { + return javaTypeNotVerified; + } + + public JavaPackage getTopLevelPackageNotVerified() { + return topLevelPackageNotVerified; + } + + public boolean isVerified(){ + return isVerified; + } + + public void setPackage(final JavaPackage javaPackage) { + Validate.notNull(javaPackage, "JavaPackage required"); + if (javaPackage.getFullyQualifiedPackageName().startsWith("java.")) { + return; + } + javaType = null; + this.javaPackage = javaPackage; + setPromptPath(javaPackage.getFullyQualifiedPackageName()); + } + + private void setPromptPath(final String fullyQualifiedName) { + if (topLevelPackage == null) { + return; + } + + String moduleName = ""; + if (module != null && StringUtils.isNotBlank(module.getModuleName())) { + moduleName = AnsiEscapeCode.decorate(module.getModuleName() + + MODULE_PATH_SEPARATOR, AnsiEscapeCode.FG_CYAN); + } + + topLevelPackage = new JavaPackage( + typeLocationService + .getTopLevelPackageForModule(projectOperations + .getFocusedModule())); + final String path = moduleName + + fullyQualifiedName.replace( + topLevelPackage.getFullyQualifiedPackageName(), + TOP_LEVEL_PACKAGE_SYMBOL); + shell.setPromptPath(path, StringUtils.isNotBlank(moduleName)); + } + + public void setTopLevelPackage(final JavaPackage topLevelPackage) { + this.topLevelPackage = topLevelPackage; + } + + public void setType(final JavaType javaType) { + Validate.notNull(javaType, "JavaType required"); + if (javaType.getPackage().getFullyQualifiedPackageName() + .startsWith("java.")) { + return; + } + this.javaType = javaType; + javaPackage = javaType.getPackage(); + setPromptPath(javaType.getFullyQualifiedTypeName()); + } + + public void setTypeNotVerified(JavaType javaType) { + Validate.notNull(javaType, "JavaType required"); + if (javaType.getPackage().getFullyQualifiedPackageName() + .startsWith("java.")) { + return; + } + registerListener(); + this.javaTypeNotVerified = javaType; + javaPackageNotVerified = javaType.getPackage(); + this.isVerified = false; + } + + private void registerListener() { + if (listenerRegistered) { + return; + } + shell.addListerner(this); + listenerRegistered = true; + } + + public void setType(final JavaType javaType, final Pom module) { + Validate.notNull(javaType, "JavaType required"); + if (javaType.getPackage().getFullyQualifiedPackageName() + .startsWith("java.")) { + return; + } + this.module = module; + this.javaType = javaType; + javaPackage = javaType.getPackage(); + setPromptPath(javaType.getFullyQualifiedTypeName()); + } + + public void setTypeNotVerified(JavaType javaType, Pom module) { + Validate.notNull(javaType, "JavaType required"); + if (javaType.getPackage().getFullyQualifiedPackageName() + .startsWith("java.")) { + return; + } + registerListener(); + this.moduleNotVerified = module; + this.javaTypeNotVerified = javaType; + javaPackageNotVerified = javaType.getPackage(); + this.isVerified = false; + } + + + /** + * CommandListener methods + */ + @Override + public void onCommandSuccess() { + // If is not verified but finish success, set last used + if (!isVerified){ + setType(javaTypeNotVerified, moduleNotVerified); + } + } + + @Override + public void onCommandFails() { + this.moduleNotVerified = null; + this.javaTypeNotVerified = null; + javaPackageNotVerified = null; + this.isVerified = false; + } + + @Override + public void onCommandBegin(ParseResult parseResult) { + // TODO Auto-generated method stub + + } + +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/LogicalPathConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/LogicalPathConverter.java new file mode 100644 index 000000000..baf46876d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/LogicalPathConverter.java @@ -0,0 +1,48 @@ +package org.springframework.roo.classpath.converters; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.PhysicalPath; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +@Component +@Service +public class LogicalPathConverter implements Converter { + + @Reference ProjectOperations projectOperations; + + public LogicalPath convertFromText(final String value, + final Class targetType, final String optionContext) { + LogicalPath logicalPath = LogicalPath.getInstance(value); + if (logicalPath.getModule().equals("FOCUSED")) { + logicalPath = LogicalPath.getInstance(logicalPath.getPath(), + projectOperations.getFocusedModuleName()); + } + return logicalPath; + } + + public boolean getAllPossibleValues(final List completions, + final Class targetType, final String existingData, + final String optionContext, final MethodTarget target) { + for (final Pom pom : projectOperations.getPoms()) { + for (final PhysicalPath physicalPath : pom.getPhysicalPaths()) { + completions.add(new Completion(physicalPath.getLogicalPath() + .getName())); + } + } + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return LogicalPath.class.isAssignableFrom(requiredType); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/converters/PathConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/converters/PathConverter.java new file mode 100644 index 000000000..ffcf38ec9 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/converters/PathConverter.java @@ -0,0 +1,50 @@ +package org.springframework.roo.classpath.converters; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.Path; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * Provides conversion to and from {@link Path}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class PathConverter implements Converter { + + // TODO: Allow context to limit to source paths only, limit to resource + // paths only + public Path convertFromText(final String value, + final Class requiredType, final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + + return Path.valueOf(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + for (final Path candidate : Path.values()) { + if ("".equals(existingData) + || candidate.name().startsWith(existingData)) { + completions.add(new Completion(candidate.name())); + } + } + return true; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Path.class.isAssignableFrom(requiredType); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/CustomDataKeys.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/CustomDataKeys.java new file mode 100644 index 000000000..287269a4c --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/CustomDataKeys.java @@ -0,0 +1,103 @@ +package org.springframework.roo.classpath.customdata; + +import org.springframework.roo.classpath.customdata.tagkeys.ConstructorMetadataCustomDataKey; +import org.springframework.roo.classpath.customdata.tagkeys.FieldMetadataCustomDataKey; +import org.springframework.roo.classpath.customdata.tagkeys.MemberHoldingTypeDetailsCustomDataKey; +import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey; +import org.springframework.roo.model.CustomData; + +/** + * {@link CustomData} tag definitions. + * + * @author Stefan Schmidt + * @author James Tyrrell + * @author Alan Stewart + * @since 1.1.3 + */ +public final class CustomDataKeys { + + // TODO: Once CustomDataKey builders have been created they should be used + // here -JT + + public static final MethodMetadataCustomDataKey CLEAR_METHOD = new MethodMetadataCustomDataKey( + "CLEAR_METHOD"); + + public static final FieldMetadataCustomDataKey COLUMN_FIELD = new FieldMetadataCustomDataKey( + "COLUMN_FIELD"); + public static final MethodMetadataCustomDataKey COUNT_ALL_METHOD = new MethodMetadataCustomDataKey( + "COUNT_ALL_METHOD"); + + // Dynamic finder method names; CustomData value expected to be a + // java.util.List of finder names + public static final MethodMetadataCustomDataKey DYNAMIC_FINDER_NAMES = new MethodMetadataCustomDataKey( + "DYNAMIC_FINDER_NAMES"); + public static final FieldMetadataCustomDataKey EMBEDDED_FIELD = new FieldMetadataCustomDataKey( + "EMBEDDED_FIELD"); + public static final FieldMetadataCustomDataKey EMBEDDED_ID_FIELD = new FieldMetadataCustomDataKey( + "EMBEDDED_ID_FIELD"); + public static final FieldMetadataCustomDataKey ENUMERATED_FIELD = new FieldMetadataCustomDataKey( + "ENUMERATED_FIELD"); + public static final MethodMetadataCustomDataKey FIND_ALL_METHOD = new MethodMetadataCustomDataKey( + "FIND_ALL_METHOD"); + public static final MethodMetadataCustomDataKey FIND_ENTRIES_METHOD = new MethodMetadataCustomDataKey( + "FIND_ENTRIES_METHOD"); + public static final MethodMetadataCustomDataKey FIND_ALL_SORTED_METHOD = new MethodMetadataCustomDataKey( + "FIND_ALL_SORTED_METHOD"); + public static final MethodMetadataCustomDataKey FIND_ENTRIES_SORTED_METHOD = new MethodMetadataCustomDataKey( + "FIND_ENTRIES_SORTED_METHOD"); + public static final MethodMetadataCustomDataKey FIND_METHOD = new MethodMetadataCustomDataKey( + "FIND_METHOD"); + public static final MethodMetadataCustomDataKey FLUSH_METHOD = new MethodMetadataCustomDataKey( + "FLUSH_METHOD"); + public static final MethodMetadataCustomDataKey IDENTIFIER_ACCESSOR_METHOD = new MethodMetadataCustomDataKey( + "IDENTIFIER_ACCESSOR_METHOD"); + public static final FieldMetadataCustomDataKey IDENTIFIER_FIELD = new FieldMetadataCustomDataKey( + "IDENTIFIER_FIELD"); + public static final MethodMetadataCustomDataKey IDENTIFIER_MUTATOR_METHOD = new MethodMetadataCustomDataKey( + "IDENTIFIER_MUTATOR_METHOD"); + // Persistence keys + public static final MemberHoldingTypeDetailsCustomDataKey IDENTIFIER_TYPE = new MemberHoldingTypeDetailsCustomDataKey( + "IDENTIFIER_TYPE"); + // Layer-related key + public static final MemberHoldingTypeDetailsCustomDataKey LAYER_TYPE = new MemberHoldingTypeDetailsCustomDataKey( + "LAYER_TYPE"); + + public static final FieldMetadataCustomDataKey LOB_FIELD = new FieldMetadataCustomDataKey( + "LOB_FIELD"); + public static final FieldMetadataCustomDataKey MANY_TO_MANY_FIELD = new FieldMetadataCustomDataKey( + "MANY_TO_MANY_FIELD"); + public static final FieldMetadataCustomDataKey MANY_TO_ONE_FIELD = new FieldMetadataCustomDataKey( + "MANY_TO_ONE_FIELD"); + public static final MethodMetadataCustomDataKey MERGE_METHOD = new MethodMetadataCustomDataKey( + "MERGE_METHOD"); + public static final ConstructorMetadataCustomDataKey NO_ARG_CONSTRUCTOR = new ConstructorMetadataCustomDataKey( + "NO_ARG_CONSTRUCTOR"); + public static final FieldMetadataCustomDataKey ONE_TO_MANY_FIELD = new FieldMetadataCustomDataKey( + "ONE_TO_MANY_FIELD"); + public static final FieldMetadataCustomDataKey ONE_TO_ONE_FIELD = new FieldMetadataCustomDataKey( + "ONE_TO_ONE_FIELD"); + public static final MethodMetadataCustomDataKey PERSIST_METHOD = new MethodMetadataCustomDataKey( + "PERSIST_METHOD"); + public static final MemberHoldingTypeDetailsCustomDataKey PERSISTENT_TYPE = new MemberHoldingTypeDetailsCustomDataKey( + "PERSISTENT_TYPE"); + public static final MethodMetadataCustomDataKey REMOVE_METHOD = new MethodMetadataCustomDataKey( + "REMOVE_METHOD"); + public static final FieldMetadataCustomDataKey SERIAL_VERSION_UUID_FIELD = new FieldMetadataCustomDataKey( + "SERIAL_VERSION_UUID_FIELD"); + public static final FieldMetadataCustomDataKey TRANSIENT_FIELD = new FieldMetadataCustomDataKey( + "TRANSIENT_FIELD"); + public static final MethodMetadataCustomDataKey VERSION_ACCESSOR_METHOD = new MethodMetadataCustomDataKey( + "VERSION_ACCESSOR_METHOD"); + + public static final FieldMetadataCustomDataKey VERSION_FIELD = new FieldMetadataCustomDataKey( + "VERSION_FIELD"); + + public static final MethodMetadataCustomDataKey VERSION_MUTATOR_METHOD = new MethodMetadataCustomDataKey( + "VERSION_MUTATOR_METHOD"); + + /** + * Constructor is private to prevent instantiation + */ + private CustomDataKeys() { + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/AnnotatedTypeMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/AnnotatedTypeMatcher.java new file mode 100644 index 000000000..e5a5f9c65 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/AnnotatedTypeMatcher.java @@ -0,0 +1,65 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomDataKey; +import org.springframework.roo.model.JavaType; + +/** + * A {@link TypeMatcher} that looks for any of a given set of annotations. + * + * @author James Tyrrell + */ +public class AnnotatedTypeMatcher extends TypeMatcher { + + private final List annotationTypesToMatchOn; + private final CustomDataKey customDataKey; + + /** + * Constructor + * + * @param customDataKey the {@link CustomDataKey} to apply (required) + * @param annotationTypesToMatchOn + */ + public AnnotatedTypeMatcher( + final CustomDataKey customDataKey, + final JavaType... annotationTypesToMatchOn) { + Validate.notNull(customDataKey, "Custom data key required"); + this.annotationTypesToMatchOn = Arrays.asList(annotationTypesToMatchOn); + this.customDataKey = customDataKey; + } + + public CustomDataKey getCustomDataKey() { + return customDataKey; + } + + public Object getTagValue(final MemberHoldingTypeDetails key) { + return null; + } + + public List matches( + final List memberHoldingTypeDetailsList) { + final Map matched = new HashMap(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberHoldingTypeDetailsList) { + for (final AnnotationMetadata annotationMetadata : memberHoldingTypeDetails + .getAnnotations()) { + for (final JavaType annotationTypeToMatchOn : annotationTypesToMatchOn) { + if (annotationMetadata.getAnnotationType().equals( + annotationTypeToMatchOn)) { + matched.put(memberHoldingTypeDetails + .getDeclaredByMetadataId(), + memberHoldingTypeDetails); + } + } + } + } + return new ArrayList(matched.values()); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/ConstructorMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/ConstructorMatcher.java new file mode 100644 index 000000000..4acff948d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/ConstructorMatcher.java @@ -0,0 +1,83 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.model.CustomDataKey; +import org.springframework.roo.model.JavaType; + +/** + * {@link ConstructorMetadata}-specific implementation of {@link Matcher}. + * Currently ConstructorMetadata instances are only matched based on parameter + * types. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class ConstructorMatcher implements Matcher { + + private final CustomDataKey customDataKey; + private final List parameterTypes; + + /** + * Constructor + * + * @param customDataKey (required) + * @param parameterTypes can be null for none + */ + public ConstructorMatcher( + final CustomDataKey customDataKey, + final Collection parameterTypes) { + Validate.notNull(customDataKey, + "Custom data key is required, e.g. a ConstructorMetadataCustomDataKey"); + this.customDataKey = customDataKey; + this.parameterTypes = new ArrayList(); + if (parameterTypes != null) { + this.parameterTypes.addAll(parameterTypes); + } + } + + /** + * Constructor + * + * @param {@link JavaType} or any subclass + * @param customDataKey (required) + * @param parameterTypes + * @since 1.2.0 + */ + public ConstructorMatcher( + final CustomDataKey customDataKey, + final T... parameterTypes) { + this(customDataKey, Arrays.asList(parameterTypes)); + } + + public CustomDataKey getCustomDataKey() { + return customDataKey; + } + + public Object getTagValue(final ConstructorMetadata key) { + return null; + } + + public List matches( + final List memberHoldingTypeDetailsList) { + final List constructors = new ArrayList(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberHoldingTypeDetailsList) { + for (final ConstructorMetadata constructor : memberHoldingTypeDetails + .getDeclaredConstructors()) { + if (parameterTypes.equals(AnnotatedJavaType + .convertFromAnnotatedJavaTypes(constructor + .getParameterTypes()))) { + constructors.add(constructor); + } + } + } + return constructors; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/CustomDataKeyDecorator.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/CustomDataKeyDecorator.java new file mode 100644 index 000000000..2fb2ca91e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/CustomDataKeyDecorator.java @@ -0,0 +1,60 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import org.springframework.roo.classpath.scanner.MemberDetailsDecorator; +import org.springframework.roo.model.CustomDataAccessor; + +/** + * Provides a universal registry for {@link Matcher} objects. Initially no + * checks are being performed upon adding new Matcher instances, it is + * envisioned that this will change and an alert would be provided if a + * {@link Matcher} object was in conflict with another. For this to happen a + * more fleshed out matching API needs to be implemented as so comparison of + * {@link Matcher}s can be performed easily. In addition to registering + * {@link Matcher}s this interface allows {@link Matcher} objects to be + * unregistered based on the registering class which is useful if the + * registering class is no longer in play. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public interface CustomDataKeyDecorator extends MemberDetailsDecorator { + + /** + * Registers the given matcher on behalf of the class with the given fully- + * qualified name. + * + * @param addingClass the name of the class registering the matcher + * (required) + * @param matcher the matcher to register (required) + */ + void registerMatcher(String addingClass, + Matcher matcher); + + /** + * Registers the given matchers on behalf of the given class + * + * @param addingClass the class registering the matchers (can be null not to + * register any matchers) + * @param matchers the matchers to register (can be none) + * @since 1.2.0 + */ + void registerMatchers(Class addingClass, + Matcher... matchers); + + /** + * Unregisters any matchers registered by the given class + * + * @param addingClass the class whose matchers are to be unregistered + * (required) + * @since 1.2.0 + */ + void unregisterMatchers(Class addingClass); + + /** + * Unregisters any matchers registered by the given class + * + * @param addingClass the fully-qualified name of the class whose matchers + * are to be unregistered (required) + */ + void unregisterMatchers(String addingClass); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/CustomDataKeyDecoratorImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/CustomDataKeyDecoratorImpl.java new file mode 100644 index 000000000..750dfe530 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/CustomDataKeyDecoratorImpl.java @@ -0,0 +1,206 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.jvnet.inflector.Noun; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsBuilder; +import org.springframework.roo.model.CustomDataAccessor; + +/** + * An implementation of {@link CustomDataKeyDecorator}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +@Component +@Service +public class CustomDataKeyDecoratorImpl implements CustomDataKeyDecorator { + + private final Map pluralMap = new HashMap(); + private final Map> taggerMap = new HashMap>(); + + public MemberDetails decorate(final String requestingClass, + final MemberDetails memberDetails) { + final MemberDetailsBuilder memberDetailsBuilder = new MemberDetailsBuilder( + memberDetails); + + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails + .getDetails()) { + if (memberHoldingTypeDetails instanceof ClassOrInterfaceTypeDetails) { + if (!pluralMap.containsKey(memberHoldingTypeDetails + .getDeclaredByMetadataId())) { + pluralMap.put( + memberHoldingTypeDetails.getDeclaredByMetadataId(), + getInflectorPlural(memberHoldingTypeDetails + .getName().getSimpleTypeName(), + Locale.ENGLISH)); + } + } + } + + // Locate any requests that we add custom data to identifiable java + // structures + for (final FieldMatcher fieldTagger : getFieldTaggers()) { + for (final FieldMetadata field : fieldTagger.matches(memberDetails + .getDetails())) { + memberDetailsBuilder.tag(field, fieldTagger.getCustomDataKey(), + fieldTagger.getTagValue(field)); + } + } + + for (final MethodMatcher methodTagger : getMethodTaggers()) { + for (final MethodMetadata method : methodTagger.matches( + memberDetails.getDetails(), pluralMap)) { + memberDetailsBuilder.tag(method, + methodTagger.getCustomDataKey(), + methodTagger.getTagValue(method)); + } + } + + for (final ConstructorMatcher constructorTagger : getConstructorTaggers()) { + for (final ConstructorMetadata constructor : constructorTagger + .matches(memberDetails.getDetails())) { + memberDetailsBuilder.tag(constructor, + constructorTagger.getCustomDataKey(), + constructorTagger.getTagValue(constructor)); + } + } + + for (final TypeMatcher typeTagger : getTypeTaggers()) { + for (final MemberHoldingTypeDetails typeDetails : typeTagger + .matches(memberDetails.getDetails())) { + memberDetailsBuilder.tag(typeDetails, + typeTagger.getCustomDataKey(), + typeTagger.getTagValue(typeDetails)); + } + } + + return memberDetailsBuilder.build(); + } + + public MemberDetails decorateTypes(final String requestingClass, + final MemberDetails memberDetails) { + final MemberDetailsBuilder memberDetailsBuilder = new MemberDetailsBuilder( + memberDetails); + for (final TypeMatcher typeTagger : getTypeTaggers()) { + for (final MemberHoldingTypeDetails typeDetails : typeTagger + .matches(memberDetails.getDetails())) { + memberDetailsBuilder.tag(typeDetails, + typeTagger.getCustomDataKey(), + typeTagger.getTagValue(typeDetails)); + } + } + return memberDetailsBuilder.build(); + } + + public List getConstructorTaggers() { + final List constructorTaggers = new ArrayList(); + for (final Matcher matcher : taggerMap + .values()) { + if (matcher instanceof ConstructorMatcher) { + constructorTaggers.add((ConstructorMatcher) matcher); + } + } + return constructorTaggers; + } + + public List getFieldTaggers() { + final List fieldTaggers = new ArrayList(); + for (final Matcher matcher : taggerMap + .values()) { + if (matcher instanceof FieldMatcher) { + fieldTaggers.add((FieldMatcher) matcher); + } + } + return fieldTaggers; + } + + /** + * This method returns the plural term as per inflector. ATTENTION: this + * method does NOT take @RooPlural into account. Use getPlural(..) instead! + * + * @param term The term to be pluralized + * @param locale Locale + * @return pluralized term + */ + public String getInflectorPlural(final String term, final Locale locale) { + try { + return Noun.pluralOf(term, locale); + } + catch (final RuntimeException re) { + // Inflector failed (see for example ROO-305), so don't pluralize it + return term; + } + } + + public List getMethodTaggers() { + final List methodTaggers = new ArrayList(); + for (final Matcher matcher : taggerMap + .values()) { + if (matcher instanceof MethodMatcher) { + methodTaggers.add((MethodMatcher) matcher); + } + } + return methodTaggers; + } + + public List getTypeTaggers() { + final List typeTaggers = new ArrayList(); + for (final Matcher matcher : taggerMap + .values()) { + if (matcher instanceof TypeMatcher) { + typeTaggers.add((TypeMatcher) matcher); + } + } + return typeTaggers; + } + + public void registerMatcher(final String addingClass, + final Matcher matcher) { + Validate.notNull(addingClass, "The calling class must be specified"); + Validate.notNull(matcher, "The matcher must be specified"); + taggerMap.put(addingClass + matcher.getCustomDataKey(), matcher); + } + + public void registerMatchers(final Class addingClass, + final Matcher... matchers) { + if (addingClass != null) { + for (final Matcher matcher : matchers) { + // We don't keep a reference to the class, as OSGi might unload + // it later + registerMatcher(addingClass.getName(), matcher); + } + } + } + + public void unregisterMatchers(final Class addingClass) { + unregisterMatchers(addingClass.getName()); + } + + public void unregisterMatchers(final String addingClass) { + final Set toRemove = new HashSet(); + for (final String taggerKey : taggerMap.keySet()) { + if (taggerKey.startsWith(addingClass)) { + toRemove.add(taggerKey); + } + } + for (final String taggerKey : toRemove) { + taggerMap.remove(taggerKey); + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/FieldMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/FieldMatcher.java new file mode 100644 index 000000000..96b0f76ac --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/FieldMatcher.java @@ -0,0 +1,166 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.COLUMN_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_ID_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ENUMERATED_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.IDENTIFIER_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.LOB_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.MANY_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_MANY_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.ONE_TO_ONE_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.TRANSIENT_FIELD; +import static org.springframework.roo.classpath.customdata.CustomDataKeys.VERSION_FIELD; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_COLUMN_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_EMBEDDED_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_EMBEDDED_ID_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_ENUMERATED_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_ID_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_LOB_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_MANY_TO_MANY_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_MANY_TO_ONE_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_ONE_TO_MANY_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_ONE_TO_ONE_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_TRANSIENT_ANNOTATION; +import static org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder.JPA_VERSION_ANNOTATION; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomDataKey; +import org.springframework.roo.model.JavaSymbolName; + +/** + * A {@link Matcher} for {@link FieldMetadata}-that matches on the presence of + * at least one of a given list of annotations. + * + * @author James Tyrrell + * @author Andrew Swan + * @since 1.1.3 + */ +public class FieldMatcher implements Matcher { + + public static final FieldMatcher JPA_COLUMN = new FieldMatcher( + COLUMN_FIELD, JPA_COLUMN_ANNOTATION); + public static final FieldMatcher JPA_EMBEDDED = new FieldMatcher( + EMBEDDED_FIELD, JPA_EMBEDDED_ANNOTATION); + public static final FieldMatcher JPA_EMBEDDED_ID = new FieldMatcher( + EMBEDDED_ID_FIELD, JPA_EMBEDDED_ID_ANNOTATION); + public static final FieldMatcher JPA_ENUMERATED = new FieldMatcher( + ENUMERATED_FIELD, JPA_ENUMERATED_ANNOTATION); + public static final FieldMatcher JPA_ID = new FieldMatcher( + IDENTIFIER_FIELD, JPA_ID_ANNOTATION); + public static final FieldMatcher JPA_LOB = new FieldMatcher(LOB_FIELD, + JPA_LOB_ANNOTATION); + public static final FieldMatcher JPA_MANY_TO_MANY = new FieldMatcher( + MANY_TO_MANY_FIELD, JPA_MANY_TO_MANY_ANNOTATION); + public static final FieldMatcher JPA_MANY_TO_ONE = new FieldMatcher( + MANY_TO_ONE_FIELD, JPA_MANY_TO_ONE_ANNOTATION); + public static final FieldMatcher JPA_ONE_TO_MANY = new FieldMatcher( + ONE_TO_MANY_FIELD, JPA_ONE_TO_MANY_ANNOTATION); + public static final FieldMatcher JPA_ONE_TO_ONE = new FieldMatcher( + ONE_TO_ONE_FIELD, JPA_ONE_TO_ONE_ANNOTATION); + public static final FieldMatcher JPA_TRANSIENT = new FieldMatcher( + TRANSIENT_FIELD, JPA_TRANSIENT_ANNOTATION); + public static final FieldMatcher JPA_VERSION = new FieldMatcher( + VERSION_FIELD, JPA_VERSION_ANNOTATION); + + private final List annotations; + private final CustomDataKey customDataKey; + + /** + * Constructor for matching on any of the given annotations + * + * @param customDataKey the custom data key indicating the type of field + * (required) + * @param annotations the annotations to match upon + * @since 1.2.0 + */ + public FieldMatcher(final CustomDataKey customDataKey, + final AnnotationMetadata... annotations) { + this(customDataKey, Arrays.asList(annotations)); + } + + /** + * Constructor for matching on any of the given annotations + * + * @param customDataKey the custom data key indicating the type of field + * (required) + * @param annotations the annotations to match upon (can be null) + */ + public FieldMatcher(final CustomDataKey customDataKey, + final Collection annotations) { + Validate.notNull(customDataKey, "Custom data key is required"); + this.annotations = new ArrayList(); + this.customDataKey = customDataKey; + if (annotations != null) { + this.annotations.addAll(annotations); + } + } + + private Map getAttributeMap(final FieldMetadata field) { + final Map map = new HashMap(); + final AnnotationMetadata annotationMetadata = getMatchingAnnotation(field); + if (annotationMetadata != null) { + for (final JavaSymbolName attributeName : annotationMetadata + .getAttributeNames()) { + map.put(attributeName.getSymbolName(), annotationMetadata + .getAttribute(attributeName).getValue()); + } + } + return map; + } + + public CustomDataKey getCustomDataKey() { + return customDataKey; + } + + /** + * Returns the first annotation of the given field that matches any of this + * matcher's target annotations + * + * @param field the field whose annotations are to be checked (required) + * @return + */ + private AnnotationMetadata getMatchingAnnotation(final FieldMetadata field) { + for (final AnnotationMetadata fieldAnnotation : field.getAnnotations()) { + for (final AnnotationMetadata matchingAnnotation : annotations) { + if (fieldAnnotation + .getAnnotationType() + .getFullyQualifiedTypeName() + .equals(matchingAnnotation.getAnnotationType() + .getFullyQualifiedTypeName())) { + return fieldAnnotation; + } + } + } + return null; + } + + public Object getTagValue(final FieldMetadata field) { + return getAttributeMap(field); + } + + public List matches( + final List memberHoldingTypeDetailsList) { + final List fields = new ArrayList(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberHoldingTypeDetailsList) { + for (final FieldMetadata field : memberHoldingTypeDetails + .getDeclaredFields()) { + if (getMatchingAnnotation(field) != null) { + fields.add(field); + } + } + } + return fields; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/Matcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/Matcher.java new file mode 100644 index 000000000..f5e387c7d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/Matcher.java @@ -0,0 +1,45 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import java.util.List; + +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.model.CustomDataAccessor; +import org.springframework.roo.model.CustomDataKey; + +/** + * Matches {@link CustomDataAccessor}s based on a specific criteria and provides + * the relevant key and value to be applied to a matched + * {@link CustomDataAccessor}. + * + * @author James Tyrrell + * @since 1.1.3 + * @param the type of {@link CustomDataAccessor} returned for each match + */ +public interface Matcher { + + /** + * Returns a key indicating the type of custom data returned by + * {@link #matches(List)} + * + * @return a non-null key + */ + CustomDataKey getCustomDataKey(); + + /** + * Returns the value associated with the given key that should be applied to + * the matched instance. + * + * @param key the custom data key + * @return a value (can be null) + */ + Object getTagValue(T key); + + /** + * Returns the {@link CustomDataAccessor}s for any elements of the given + * list that meet this matcher's inclusion criteria. + * + * @param memberHoldingTypeDetailsList the list to check for matches + * @return a non-null list + */ + List matches(List memberHoldingTypeDetailsList); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/MethodMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/MethodMatcher.java new file mode 100644 index 000000000..7b830a233 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/MethodMatcher.java @@ -0,0 +1,248 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import static org.springframework.roo.model.RooJavaType.ROO_PLURAL; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomDataKey; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * {@link MethodMetadata} specific implementation of {@link Matcher}. Matches + * are based on field name which is dynamically determined based on: the + * {@link FieldMatcher}s presented; the type of method (accessor/mutator); the + * default method name; the user specified method name obtained from a + * particular Roo annotation; a plural/singular suffix of the referenced entity; + * and, an additional suffix. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class MethodMatcher implements Matcher { + + private String additionalSuffix = ""; + private JavaType catalystAnnotationType; + + private final CustomDataKey customDataKey; + private String defaultName; + private final List fieldTaggers = new ArrayList(); + private boolean isAccessor; + private boolean suffixPlural; + private boolean suffixSingular; + private JavaSymbolName userDefinedNameAttribute; + + /** + * Constructor + * + * @param fieldTaggers can be null for none + * @param customDataKey + * @param isAccessor + */ + public MethodMatcher(final Collection fieldTaggers, + final CustomDataKey customDataKey, + final boolean isAccessor) { + this.customDataKey = customDataKey; + this.isAccessor = isAccessor; + if (fieldTaggers != null) { + this.fieldTaggers.addAll(fieldTaggers); + } + } + + /** + * Constructor + * + * @param customDataKey + * @param catalystAnnotationType + * @param userDefinedNameAttribute + * @param defaultName + */ + public MethodMatcher(final CustomDataKey customDataKey, + final JavaType catalystAnnotationType, + final JavaSymbolName userDefinedNameAttribute, + final String defaultName) { + this.catalystAnnotationType = catalystAnnotationType; + this.customDataKey = customDataKey; + this.userDefinedNameAttribute = userDefinedNameAttribute; + this.defaultName = defaultName; + } + + /** + * Constructor + * + * @param customDataKey + * @param catalystAnnotationType + * @param userDefinedNameAttribute + * @param defaultName + * @param suffixPlural + * @param suffixSingular + */ + public MethodMatcher(final CustomDataKey customDataKey, + final JavaType catalystAnnotationType, + final JavaSymbolName userDefinedNameAttribute, + final String defaultName, final boolean suffixPlural, + final boolean suffixSingular) { + this(customDataKey, catalystAnnotationType, userDefinedNameAttribute, + defaultName); + this.suffixPlural = suffixPlural; + this.suffixSingular = suffixSingular; + } + + /** + * Constructor + * + * @param customDataKey + * @param catalystAnnotationType + * @param userDefinedNameAttribute + * @param defaultName + * @param suffixPlural + * @param suffixSingular + * @param additionalSuffix + */ + public MethodMatcher(final CustomDataKey customDataKey, + final JavaType catalystAnnotationType, + final JavaSymbolName userDefinedNameAttribute, + final String defaultName, final boolean suffixPlural, + final boolean suffixSingular, final String additionalSuffix) { + this(customDataKey, catalystAnnotationType, userDefinedNameAttribute, + defaultName, suffixPlural, suffixSingular); + this.additionalSuffix = additionalSuffix; + } + + public CustomDataKey getCustomDataKey() { + return customDataKey; + } + + private List getFieldsInterestedIn( + final List memberHoldingTypeDetailsList) { + final List fields = new ArrayList(); + for (final FieldMatcher fieldTagger : fieldTaggers) { + fields.addAll(fieldTagger.matches(memberHoldingTypeDetailsList)); + } + return fields; + } + + private ClassOrInterfaceTypeDetails getMostConcreteClassOrInterfaceTypeDetails( + final List memberHoldingTypeDetailsList) { + ClassOrInterfaceTypeDetails cid = null; + // The last ClassOrInterfaceTypeDetails is the most concrete as dictated + // by the logic in MemberDetailsScannerImpl + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberHoldingTypeDetailsList) { + if (memberHoldingTypeDetails instanceof ClassOrInterfaceTypeDetails) { + cid = (ClassOrInterfaceTypeDetails) memberHoldingTypeDetails; + } + } + Validate.notNull(cid, "No concrete type found; cannot continue"); + return cid; + } + + private String getPrefix() { + return isAccessor ? "get" : "set"; + } + + private String getSuffix( + final List memberHoldingTypeDetailsList, + final boolean singular, final Map pluralMap) { + final ClassOrInterfaceTypeDetails cid = getMostConcreteClassOrInterfaceTypeDetails(memberHoldingTypeDetailsList); + if (singular) { + return cid.getName().getSimpleTypeName(); + } + String plural = pluralMap.get(cid.getDeclaredByMetadataId()); + for (final AnnotationMetadata annotationMetadata : cid.getAnnotations()) { + if (annotationMetadata.getAnnotationType() + .getFullyQualifiedTypeName() + .equals(ROO_PLURAL.getFullyQualifiedTypeName())) { + final AnnotationAttributeValue annotationAttributeValue = annotationMetadata + .getAttribute(new JavaSymbolName("value")); + if (annotationAttributeValue != null) { + plural = annotationAttributeValue.getValue().toString(); + } + break; + } + } + if (StringUtils.isNotBlank(plural)) { + plural = StringUtils.capitalize(plural); + } + return plural; + } + + public Object getTagValue(final MethodMetadata key) { + return null; + } + + private JavaSymbolName getUserDefinedMethod( + final List memberHoldingTypeDetailsList, + final Map pluralMap) { + if (catalystAnnotationType == null || userDefinedNameAttribute == null) { + return null; + } + final String suffix = suffixPlural || suffixSingular ? getSuffix( + memberHoldingTypeDetailsList, suffixSingular, pluralMap) : ""; + final ClassOrInterfaceTypeDetails cid = getMostConcreteClassOrInterfaceTypeDetails(memberHoldingTypeDetailsList); + for (final AnnotationMetadata annotationMetadata : cid.getAnnotations()) { + if (annotationMetadata.getAnnotationType() + .getFullyQualifiedTypeName() + .equals(catalystAnnotationType.getFullyQualifiedTypeName())) { + final AnnotationAttributeValue annotationAttributeValue = annotationMetadata + .getAttribute(userDefinedNameAttribute); + if (annotationAttributeValue != null + && StringUtils.isNotBlank(annotationAttributeValue + .getValue().toString())) { + return new JavaSymbolName(annotationAttributeValue + .getValue().toString() + suffix); + } + break; + } + } + return defaultName == null ? null : new JavaSymbolName(defaultName + + suffix); + } + + public List matches( + final List memberHoldingTypeDetailsList) { + return null; // TODO: This needs to be dealt with -JT + } + + public List matches( + final List memberHoldingTypeDetailsList, + final Map pluralMap) { + final List fields = getFieldsInterestedIn(memberHoldingTypeDetailsList); + final List methods = new ArrayList(); + final Set methodNames = new HashSet(); + final JavaSymbolName userDefinedMethodName = getUserDefinedMethod( + memberHoldingTypeDetailsList, pluralMap); + if (userDefinedMethodName == null) { + for (final FieldMetadata field : fields) { + methodNames.add(new JavaSymbolName(getPrefix() + + StringUtils.capitalize(field.getFieldName() + .getSymbolName()))); + } + } + else { + methodNames.add(new JavaSymbolName(userDefinedMethodName + .getSymbolName() + additionalSuffix)); + } + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberHoldingTypeDetailsList) { + for (final MethodMetadata method : memberHoldingTypeDetails + .getDeclaredMethods()) { + if (methodNames.contains(method.getMethodName())) { + methods.add(method); + } + } + } + return methods; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/MidTypeMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/MidTypeMatcher.java new file mode 100644 index 000000000..fd14181f1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/MidTypeMatcher.java @@ -0,0 +1,68 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.model.CustomDataKey; + +/** + * {@link MemberHoldingTypeDetails}-specific implementation of {@link Matcher}. + * Matches are based on the the type's MID. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class MidTypeMatcher extends TypeMatcher { + + private final CustomDataKey customDataKey; + private final String declaredBy; + + /** + * Constructor + * + * @param customDataKey + * @param declaredBy the declaring class (required) + * @since 1.2 + */ + public MidTypeMatcher( + final CustomDataKey customDataKey, + final Class declaredBy) { + this(customDataKey, declaredBy.getName()); + } + + /** + * Constructor + * + * @param customDataKey + * @param declaredBy (required) + */ + public MidTypeMatcher( + final CustomDataKey customDataKey, + final String declaredBy) { + Validate.notBlank(declaredBy, "declaredBy is required"); + this.customDataKey = customDataKey; + this.declaredBy = declaredBy; + } + + public CustomDataKey getCustomDataKey() { + return customDataKey; + } + + public Object getTagValue(final MemberHoldingTypeDetails key) { + return null; + } + + public List matches( + final List memberHoldingTypeDetailsList) { + final List types = new ArrayList(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberHoldingTypeDetailsList) { + if (memberHoldingTypeDetails.getDeclaredByMetadataId().startsWith( + "MID:" + declaredBy)) { + types.add(memberHoldingTypeDetails); + } + } + return types; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/TypeMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/TypeMatcher.java new file mode 100644 index 000000000..551e3afc5 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/taggers/TypeMatcher.java @@ -0,0 +1,13 @@ +package org.springframework.roo.classpath.customdata.taggers; + +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; + +/** + * {@link MemberHoldingTypeDetails}-specific implementation of {@link Matcher}. + * Matches are based on the the type's MID. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public abstract class TypeMatcher implements Matcher { +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/ConstructorMetadataCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/ConstructorMetadataCustomDataKey.java new file mode 100644 index 000000000..7bb853caa --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/ConstructorMetadataCustomDataKey.java @@ -0,0 +1,33 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import org.springframework.roo.classpath.details.ConstructorMetadata; + +/** + * {@link ConstructorMetadata}-specific implementation of + * {@link InvocableMemberMetadataCustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class ConstructorMetadataCustomDataKey extends + InvocableMemberMetadataCustomDataKey { + private final String name; + + public ConstructorMetadataCustomDataKey(final String name) { + this.name = name; + } + + @Override + public boolean meets(final ConstructorMetadata constructor) { + return super.meets(constructor); + } + + public String name() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/FieldMetadataCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/FieldMetadataCustomDataKey.java new file mode 100644 index 000000000..b15f3143f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/FieldMetadataCustomDataKey.java @@ -0,0 +1,71 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import java.util.List; + +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * {@link FieldMetadata}-specific implementation of + * {@link IdentifiableAnnotatedJavaStructureCustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class FieldMetadataCustomDataKey extends + IdentifiableAnnotatedJavaStructureCustomDataKey { + private String fieldInitializer; + private JavaSymbolName fieldName; + private JavaType fieldType; + private String name; + + public FieldMetadataCustomDataKey(final Integer modifier, + final List annotations) { + super(modifier, annotations); + } + + public FieldMetadataCustomDataKey(final Integer modifier, + final List annotations, + final JavaType fieldType, final JavaSymbolName fieldName, + final String fieldInitializer) { + super(modifier, annotations); + this.fieldType = fieldType; + this.fieldName = fieldName; + this.fieldInitializer = fieldInitializer; + } + + public FieldMetadataCustomDataKey(final String name) { + super(null, null); + this.name = name; + } + + public String getFieldInitializer() { + return fieldInitializer; + } + + public JavaSymbolName getFieldName() { + return fieldName; + } + + public JavaType getFieldType() { + return fieldType; + } + + @Override + public boolean meets(final FieldMetadata field) { + // TODO: Add in validation logic for fieldType, fieldName, + // fieldInitializer + return super.meets(field); + } + + public String name() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/IdentifiableAnnotatedJavaStructureCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/IdentifiableAnnotatedJavaStructureCustomDataKey.java new file mode 100644 index 000000000..08508930e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/IdentifiableAnnotatedJavaStructureCustomDataKey.java @@ -0,0 +1,39 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import java.util.List; + +import org.springframework.roo.classpath.details.IdentifiableAnnotatedJavaStructure; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; + +/** + * {@link IdentifiableAnnotatedJavaStructure}-specific implementation of + * {@link IdentifiableJavaStructureCustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public abstract class IdentifiableAnnotatedJavaStructureCustomDataKey + extends IdentifiableJavaStructureCustomDataKey { + private List annotations; + + protected IdentifiableAnnotatedJavaStructureCustomDataKey() { + super(); + } + + protected IdentifiableAnnotatedJavaStructureCustomDataKey( + final Integer modifier, final List annotations) { + super(modifier); + this.annotations = annotations; + } + + public List getAnnotations() { + return annotations; + } + + @Override + public boolean meets(final T identifiableAnnotatedJavaStructure) + throws IllegalStateException { + // TODO: Add in validation logic for annotations + return super.meets(identifiableAnnotatedJavaStructure); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/IdentifiableJavaStructureCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/IdentifiableJavaStructureCustomDataKey.java new file mode 100644 index 000000000..10a5c4784 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/IdentifiableJavaStructureCustomDataKey.java @@ -0,0 +1,33 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import org.springframework.roo.classpath.details.IdentifiableJavaStructure; +import org.springframework.roo.model.CustomDataKey; + +/** + * {@link IdentifiableJavaStructure}-specific implementation of + * {@link org.springframework.roo.model.CustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public abstract class IdentifiableJavaStructureCustomDataKey + implements CustomDataKey { + private Integer modifier; + + protected IdentifiableJavaStructureCustomDataKey() { + } + + public IdentifiableJavaStructureCustomDataKey(final Integer modifier) { + this.modifier = modifier; + } + + public Integer getModifier() { + return modifier; + } + + public boolean meets(final T identifiableJavaStructure) + throws IllegalStateException { + // TODO: Add in validation logic for modifier + return true; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/InvocableMemberMetadataCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/InvocableMemberMetadataCustomDataKey.java new file mode 100644 index 000000000..35c95cc2b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/InvocableMemberMetadataCustomDataKey.java @@ -0,0 +1,58 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import java.util.List; + +import org.springframework.roo.classpath.details.InvocableMemberMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * {@link InvocableMemberMetadata}-specific implementation of + * {@link IdentifiableAnnotatedJavaStructureCustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public abstract class InvocableMemberMetadataCustomDataKey + extends IdentifiableAnnotatedJavaStructureCustomDataKey { + private List parameterNames; + private List parameterTypes; + private List throwsTypes; + + protected InvocableMemberMetadataCustomDataKey() { + super(); + } + + protected InvocableMemberMetadataCustomDataKey(final Integer modifier, + final List annotations, + final List parameterTypes, + final List parameterNames, + final List throwsTypes) { + super(modifier, annotations); + this.parameterTypes = parameterTypes; + this.parameterNames = parameterNames; + this.throwsTypes = throwsTypes; + } + + public List getParameterNames() { + return parameterNames; + } + + public List getParameterTypes() { + return parameterTypes; + } + + public List getThrowsTypes() { + return throwsTypes; + } + + @Override + public boolean meets(final T invocableMemberMetadata) + throws IllegalStateException { + // TODO: Add in validation logic for parameterTypes, parameterNames, + // throwsTypes + return super.meets(invocableMemberMetadata); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/MemberHoldingTypeDetailsCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/MemberHoldingTypeDetailsCustomDataKey.java new file mode 100644 index 000000000..8873f00be --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/MemberHoldingTypeDetailsCustomDataKey.java @@ -0,0 +1,34 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.model.CustomDataKey; + +/** + * {@link MemberHoldingTypeDetails}-specific implementation of + * {@link org.springframework.roo.model.CustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class MemberHoldingTypeDetailsCustomDataKey implements + CustomDataKey { + private final String name; + + public MemberHoldingTypeDetailsCustomDataKey(final String name) { + this.name = name; + } + + public boolean meets(final MemberHoldingTypeDetails memberHoldingTypeDetails) + throws IllegalStateException { + return true; + } + + public String name() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/MethodMetadataCustomDataKey.java b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/MethodMetadataCustomDataKey.java new file mode 100644 index 000000000..d592393b2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/customdata/tagkeys/MethodMetadataCustomDataKey.java @@ -0,0 +1,36 @@ +package org.springframework.roo.classpath.customdata.tagkeys; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.MethodMetadata; + +/** + * {@link MethodMetadata}-specific implementation of + * {@link InvocableMemberMetadataCustomDataKey}. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public class MethodMetadataCustomDataKey extends + InvocableMemberMetadataCustomDataKey { + + private final String tag; + + /** + * Constructor + * + * @param tag + */ + public MethodMetadataCustomDataKey(final String tag) { + Validate.notBlank(tag, "Invalid tag '%s'", tag); + this.tag = tag; + } + + public String name() { + return tag; + } + + @Override + public String toString() { + return tag; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableAnnotatedJavaStructureBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableAnnotatedJavaStructureBuilder.java new file mode 100644 index 000000000..a59791413 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableAnnotatedJavaStructureBuilder.java @@ -0,0 +1,238 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Assists in the creation of a {@link Builder} for types that eventually + * implement {@link IdentifiableAnnotatedJavaStructure}. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractIdentifiableAnnotatedJavaStructureBuilder + extends AbstractIdentifiableJavaStructureBuilder { + private List annotations = new ArrayList(); + + protected AbstractIdentifiableAnnotatedJavaStructureBuilder( + final IdentifiableAnnotatedJavaStructure existing) { + super(existing); + init(existing); + } + + protected AbstractIdentifiableAnnotatedJavaStructureBuilder( + final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + protected AbstractIdentifiableAnnotatedJavaStructureBuilder( + final String declaredbyMetadataId, + final IdentifiableAnnotatedJavaStructure existing) { + super(declaredbyMetadataId, existing); + init(existing); + } + + public final boolean addAnnotation( + final AnnotationMetadata annotationMetadata) { + if (annotationMetadata == null) { + return false; + } + return addAnnotation(new AnnotationMetadataBuilder(annotationMetadata)); + } + + public final boolean addAnnotation( + final AnnotationMetadataBuilder annotationBuilder) { + if (annotationBuilder == null) { + return false; + } + onAddAnnotation(annotationBuilder); + return annotations.add(annotationBuilder); + } + + public final List buildAnnotations() { + final List result = new ArrayList(); + for (final AnnotationMetadataBuilder annotationBuilder : annotations) { + result.add(annotationBuilder.build()); + } + return result; + } + + public final List getAnnotations() { + return annotations; + } + + /** + * Locates the specified type-level annotation. + * + * @param type to locate (can be null) + * @return the annotation, or null if not found + * @since 1.2.0 + */ + public AnnotationMetadataBuilder getDeclaredTypeAnnotation( + final JavaType type) { + for (final AnnotationMetadataBuilder annotationBuilder : getAnnotations()) { + if (annotationBuilder.getAnnotationType().equals(type)) { + return annotationBuilder; + } + } + return null; + } + + private void init(final IdentifiableAnnotatedJavaStructure existing) { + for (final AnnotationMetadata element : existing.getAnnotations()) { + this.annotations.add(new AnnotationMetadataBuilder(element)); + } + } + + protected void onAddAnnotation( + final AnnotationMetadataBuilder annotationMetadata) { + } + + public void removeAnnotation(final JavaType annotationType) { + for (final AnnotationMetadataBuilder annotationMetadataBuilder : annotations) { + if (annotationMetadataBuilder.getAnnotationType().equals( + annotationType)) { + annotations.remove(annotationMetadataBuilder); + break; + } + } + } + + public final void setAnnotations( + final Collection annotations) { + final List annotationBuilders = new ArrayList(); + for (final AnnotationMetadata annotationMetadata : annotations) { + annotationBuilders.add(new AnnotationMetadataBuilder( + annotationMetadata)); + } + setAnnotations(annotationBuilders); + } + + public final void setAnnotations( + final List annotations) { + this.annotations = annotations; + } + + public boolean updateTypeAnnotation(final AnnotationMetadata annotation) { + return updateTypeAnnotation(annotation, null); + } + + public boolean updateTypeAnnotation(final AnnotationMetadata annotation, + final Set attributesToDeleteIfPresent) { + boolean hasChanged = false; + + // We are going to build a replacement AnnotationMetadata. + // This variable tracks the new attribute values the replacement will + // hold. + final Map> replacementAttributeValues = new LinkedHashMap>(); + + final AnnotationMetadataBuilder existingBuilder = getDeclaredTypeAnnotation(annotation + .getAnnotationType()); + + if (existingBuilder == null) { + // Not already present, so just go and add it + for (final JavaSymbolName incomingAttributeName : annotation + .getAttributeNames()) { + // Do not copy incoming attributes which exist in the + // attributesToDeleteIfPresent Set + if (attributesToDeleteIfPresent == null + || !attributesToDeleteIfPresent + .contains(incomingAttributeName)) { + final AnnotationAttributeValue incomingValue = annotation + .getAttribute(incomingAttributeName); + replacementAttributeValues.put(incomingAttributeName, + incomingValue); + } + } + + final AnnotationMetadataBuilder replacement = new AnnotationMetadataBuilder( + annotation.getAnnotationType(), + new ArrayList>( + replacementAttributeValues.values())); + addAnnotation(replacement); + return true; + } + + final AnnotationMetadata existing = existingBuilder.build(); + + // Copy the existing attributes into the new attributes + for (final JavaSymbolName existingAttributeName : existing + .getAttributeNames()) { + if (attributesToDeleteIfPresent != null + && attributesToDeleteIfPresent + .contains(existingAttributeName)) { + hasChanged = true; + } + else { + final AnnotationAttributeValue existingValue = existing + .getAttribute(existingAttributeName); + replacementAttributeValues.put(existingAttributeName, + existingValue); + } + } + + // Now we ensure every incoming attribute replaces the existing + for (final JavaSymbolName incomingAttributeName : annotation + .getAttributeNames()) { + final AnnotationAttributeValue incomingValue = annotation + .getAttribute(incomingAttributeName); + + // Add this attribute to the end of the list if the attribute is not + // already present + if (replacementAttributeValues.keySet().contains( + incomingAttributeName)) { + // There was already an attribute. Need to determine if this new + // attribute value is materially different + final AnnotationAttributeValue existingValue = replacementAttributeValues + .get(incomingAttributeName); + Validate.notNull(existingValue, + "Existing value should have been provided by earlier loop"); + if (!existingValue.equals(incomingValue)) { + replacementAttributeValues.put(incomingAttributeName, + incomingValue); + hasChanged = true; + } + } + else if (attributesToDeleteIfPresent == null + || !attributesToDeleteIfPresent + .contains(incomingAttributeName)) { + // This is a new attribute that does not already exist, so add + // it to the end of the replacement attributes + replacementAttributeValues.put(incomingAttributeName, + incomingValue); + hasChanged = true; + } + } + // Were there any material changes? + if (!hasChanged) { + return false; + } + + // Make a new AnnotationMetadata representing the replacement + final AnnotationMetadataBuilder replacement = new AnnotationMetadataBuilder( + annotation.getAnnotationType(), + new ArrayList>( + replacementAttributeValues.values())); + annotations.remove(existingBuilder); + addAnnotation(replacement); + + return true; + } + + public boolean updateTypeAnnotation( + final AnnotationMetadataBuilder annotationBuilder) { + return updateTypeAnnotation(annotationBuilder.build()); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableAnnotatedJavaStructureProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableAnnotatedJavaStructureProvider.java new file mode 100644 index 000000000..758b7781b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableAnnotatedJavaStructureProvider.java @@ -0,0 +1,74 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Abstract class for {@link IdentifiableAnnotatedJavaStructure} subclasses. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractIdentifiableAnnotatedJavaStructureProvider extends + AbstractIdentifiableJavaStructureProvider implements + IdentifiableAnnotatedJavaStructure { + + private final List annotations = new ArrayList(); + + /** + * Constructor + * + * @param customData + * @param declaredByMetadataId + * @param modifier + * @param annotations can be null for none + */ + protected AbstractIdentifiableAnnotatedJavaStructureProvider( + final CustomData customData, final String declaredByMetadataId, + final int modifier, final Collection annotations) { + super(customData, declaredByMetadataId, modifier); + CollectionUtils.populate(this.annotations, annotations); + } + + public AnnotationMetadata getAnnotation(final JavaType type) { + Validate.notNull(type, "Annotation type to locate required"); + for (final AnnotationMetadata md : getAnnotations()) { + if (md.getAnnotationType().equals(type)) { + return md; + } + } + return null; + } + + public List getAnnotations() { + return Collections.unmodifiableList(annotations); + } + + public AnnotationMetadata getTypeAnnotation(final JavaType annotationType) { + Validate.notNull(annotationType, "Annotation type required"); + IdentifiableAnnotatedJavaStructure current = this; + while (current != null) { + final AnnotationMetadata result = current + .getAnnotation(annotationType); + if (result != null) { + return result; + } + if (current instanceof ClassOrInterfaceTypeDetails) { + current = ((ClassOrInterfaceTypeDetails) current) + .getSuperclass(); + } + else { + current = null; + } + } + return null; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableJavaStructureBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableJavaStructureBuilder.java new file mode 100644 index 000000000..35518f516 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableJavaStructureBuilder.java @@ -0,0 +1,79 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.model.AbstractCustomDataAccessorBuilder; +import org.springframework.roo.model.Builder; + +/** + * Assists in the creation of a {@link Builder} for types that eventually + * implement {@link IdentifiableJavaStructure}. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractIdentifiableJavaStructureBuilder + extends AbstractCustomDataAccessorBuilder { + + private final String declaredByMetadataId; + private int modifier; + + /** + * Constructor + * + * @param existing + */ + protected AbstractIdentifiableJavaStructureBuilder( + final IdentifiableJavaStructure existing) { + super(existing); + this.declaredByMetadataId = existing.getDeclaredByMetadataId(); + this.modifier = existing.getModifier(); + } + + /** + * Constructor + * + * @param declaredByMetadataId + */ + protected AbstractIdentifiableJavaStructureBuilder( + final String declaredByMetadataId) { + Validate.isTrue( + MetadataIdentificationUtils + .isIdentifyingInstance(declaredByMetadataId), + "Declared by metadata ID must identify a specific instance (not '%s')", + declaredByMetadataId); + this.declaredByMetadataId = declaredByMetadataId; + } + + /** + * Constructor + * + * @param declaredbyMetadataId + * @param existing + */ + protected AbstractIdentifiableJavaStructureBuilder( + final String declaredbyMetadataId, + final IdentifiableJavaStructure existing) { + super(existing); + this.declaredByMetadataId = declaredbyMetadataId; + this.modifier = existing.getModifier(); + } + + public String getDeclaredByMetadataId() { + return declaredByMetadataId; + } + + public final int getModifier() { + return modifier; + } + + /** + * Sets the modifier of the built Java structure + * + * @param modifier the modifier to set + * @see Modifier#PUBLIC, etc. + */ + public final void setModifier(final int modifier) { + this.modifier = modifier; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableJavaStructureProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableJavaStructureProvider.java new file mode 100644 index 000000000..d18c21bf9 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractIdentifiableJavaStructureProvider.java @@ -0,0 +1,43 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.AbstractCustomDataAccessorProvider; +import org.springframework.roo.model.CustomData; + +/** + * Abstract class for {@link IdentifiableJavaStructure} subclasses. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractIdentifiableJavaStructureProvider extends + AbstractCustomDataAccessorProvider implements IdentifiableJavaStructure { + + private final String declaredByMetadataId; + private final int modifier; + + /** + * Constructor + * + * @param customData + * @param declaredByMetadataId + * @param modifier + */ + protected AbstractIdentifiableJavaStructureProvider( + final CustomData customData, final String declaredByMetadataId, + final int modifier) { + super(customData); + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + this.declaredByMetadataId = declaredByMetadataId; + this.modifier = modifier; + } + + public final String getDeclaredByMetadataId() { + return declaredByMetadataId; + } + + public final int getModifier() { + return modifier; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractInvocableMemberMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractInvocableMemberMetadata.java new file mode 100644 index 000000000..29a5c06d5 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractInvocableMemberMetadata.java @@ -0,0 +1,82 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Abstract implementation of {@link InvocableMemberMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractInvocableMemberMetadata extends + AbstractIdentifiableAnnotatedJavaStructureProvider implements + InvocableMemberMetadata { + + private final String body; + private final List parameterNames = new ArrayList(); + private final List parameterTypes = new ArrayList(); + private final List throwsTypes = new ArrayList(); + private CommentStructure commentStructure; + + /** + * Constructor + * + * @param customData + * @param declaredByMetadataId + * @param modifier + * @param annotations + * @param parameterTypes + * @param parameterNames + * @param throwsTypes + * @param body + */ + protected AbstractInvocableMemberMetadata(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final List annotations, + final List parameterTypes, + final List parameterNames, + final List throwsTypes, final String body) { + super(customData, declaredByMetadataId, modifier, annotations); + this.body = body; + CollectionUtils.populate(this.parameterNames, parameterNames); + CollectionUtils.populate(this.parameterTypes, parameterTypes); + CollectionUtils.populate(this.throwsTypes, throwsTypes); + } + + public final String getBody() { + return body; + } + + public final List getParameterNames() { + return Collections.unmodifiableList(parameterNames); + } + + public final List getParameterTypes() { + return Collections.unmodifiableList(parameterTypes); + } + + public final List getThrowsTypes() { + return Collections.unmodifiableList(throwsTypes); + } + + @Override + public CommentStructure getCommentStructure() { + return commentStructure; + } + + @Override + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } + +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractInvocableMemberMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractInvocableMemberMetadataBuilder.java new file mode 100644 index 000000000..e1b715e7d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractInvocableMemberMetadataBuilder.java @@ -0,0 +1,123 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Assists in the development of builders that build objects that extend + * {@link AbstractInvocableMemberMetadata}. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractInvocableMemberMetadataBuilder + extends AbstractIdentifiableAnnotatedJavaStructureBuilder { + + private InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + private List parameterNames = new ArrayList(); + private List parameterTypes = new ArrayList(); + private List throwsTypes = new ArrayList(); + private CommentStructure commentStructure; + + protected AbstractInvocableMemberMetadataBuilder( + final InvocableMemberMetadata existing) { + super(existing); + this.parameterNames = new ArrayList( + existing.getParameterNames()); + this.parameterTypes = new ArrayList( + existing.getParameterTypes()); + this.throwsTypes = new ArrayList(existing.getThrowsTypes()); + bodyBuilder.append(existing.getBody()); + } + + protected AbstractInvocableMemberMetadataBuilder( + final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + protected AbstractInvocableMemberMetadataBuilder( + final String declaredbyMetadataId, + final InvocableMemberMetadata existing) { + super(declaredbyMetadataId, existing); + this.parameterNames = new ArrayList( + existing.getParameterNames()); + this.parameterTypes = new ArrayList( + existing.getParameterTypes()); + this.throwsTypes = new ArrayList(existing.getThrowsTypes()); + bodyBuilder.append(existing.getBody()); + } + + public void addParameter(final String parameterName, + final JavaType parameterType) { + addParameterName(new JavaSymbolName(parameterName)); + addParameterType(AnnotatedJavaType.convertFromJavaType(parameterType)); + } + + public boolean addParameterName(final JavaSymbolName parameterName) { + return parameterNames.add(parameterName); + } + + public boolean addParameterType(final AnnotatedJavaType parameterType) { + return parameterTypes.add(parameterType); + } + + public boolean addThrowsType(final JavaType throwsType) { + return throwsTypes.add(throwsType); + } + + public String getBody() { + if (bodyBuilder != null) { + return bodyBuilder.getOutput(); + } + return null; + } + + public InvocableMemberBodyBuilder getBodyBuilder() { + if (bodyBuilder == null) { + bodyBuilder = new InvocableMemberBodyBuilder(); + } + return bodyBuilder; + } + + public List getParameterNames() { + return parameterNames; + } + + public List getParameterTypes() { + return parameterTypes; + } + + public List getThrowsTypes() { + return throwsTypes; + } + + public void setBodyBuilder(final InvocableMemberBodyBuilder bodyBuilder) { + this.bodyBuilder = bodyBuilder; + } + + public void setParameterNames(final List parameterNames) { + this.parameterNames = parameterNames; + } + + public void setParameterTypes(final List parameterTypes) { + this.parameterTypes = parameterTypes; + } + + public void setThrowsTypes(final List throwsTypes) { + this.throwsTypes = throwsTypes; + } + + public CommentStructure getCommentStructure() { + return commentStructure; + } + + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetails.java new file mode 100644 index 000000000..41fe7f83a --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetails.java @@ -0,0 +1,206 @@ +package org.springframework.roo.classpath.details; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.LAYER_TYPE; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Convenient superclass for {@link MemberHoldingTypeDetails} implementations. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public abstract class AbstractMemberHoldingTypeDetails extends + AbstractIdentifiableAnnotatedJavaStructureProvider implements + MemberHoldingTypeDetails { + + /** + * Constructor + * + * @param customData + * @param declaredByMetadataId + * @param modifier + * @param annotations + */ + protected AbstractMemberHoldingTypeDetails(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final Collection annotations) { + super(customData, declaredByMetadataId, modifier, annotations); + } + + public ConstructorMetadata getDeclaredConstructor( + final List parameters) { + final Collection parameterList = CollectionUtils.populate( + new ArrayList(), parameters); + for (final ConstructorMetadata constructor : getDeclaredConstructors()) { + if (parameterList.equals(AnnotatedJavaType + .convertFromAnnotatedJavaTypes(constructor + .getParameterTypes()))) { + return constructor; + } + } + return null; + } + + public FieldMetadata getDeclaredField(final JavaSymbolName fieldName) { + for (final FieldMetadata field : getDeclaredFields()) { + if (field.getFieldName().equals(fieldName)) { + return field; + } + } + return null; + } + + public ClassOrInterfaceTypeDetails getDeclaredInnerType( + final JavaType typeName) { + Validate.notNull(typeName, "Name of inner type required"); + for (final ClassOrInterfaceTypeDetails cid : getDeclaredInnerTypes()) { + if (cid.getName().getSimpleTypeName() + .equals(typeName.getSimpleTypeName())) { + return cid; + } + } + return null; + } + + public FieldMetadata getField(final JavaSymbolName fieldName) { + Validate.notNull(fieldName, "Field name required"); + MemberHoldingTypeDetails current = this; + while (current != null) { + final FieldMetadata result = current.getDeclaredField(fieldName); + if (result != null) { + return result; + } + if (current instanceof ClassOrInterfaceTypeDetails) { + current = ((ClassOrInterfaceTypeDetails) current) + .getSuperclass(); + } + else { + current = null; + } + } + return null; + } + + public List getFieldsWithAnnotation(final JavaType annotation) { + Validate.notNull(annotation, "Annotation required"); + final List result = new ArrayList(); + MemberHoldingTypeDetails current = this; + while (current != null) { + for (final FieldMetadata field : current.getDeclaredFields()) { + if (MemberFindingUtils.getAnnotationOfType( + field.getAnnotations(), annotation) != null) { + // Found the annotation on this field + result.add(field); + } + } + if (current instanceof ClassOrInterfaceTypeDetails) { + current = ((ClassOrInterfaceTypeDetails) current) + .getSuperclass(); + } + else { + current = null; + } + } + return result; + } + + @SuppressWarnings("unchecked") + public List getLayerEntities() { + final Object entities = getCustomData().get(LAYER_TYPE); + if (entities == null) { + return Collections.emptyList(); + } + return (List) entities; + } + + public MethodMetadata getMethod(final JavaSymbolName methodName) { + Validate.notNull(methodName, "Method name required"); + + MemberHoldingTypeDetails current = this; + while (current != null) { + final MethodMetadata result = MemberFindingUtils.getDeclaredMethod( + current, methodName); + if (result != null) { + return result; + } + if (current instanceof ClassOrInterfaceTypeDetails) { + current = ((ClassOrInterfaceTypeDetails) current) + .getSuperclass(); + } + else { + current = null; + } + } + return null; + } + + public MethodMetadata getMethod(final JavaSymbolName methodName, + List parameters) { + Validate.notNull(methodName, "Method name required"); + if (parameters == null) { + parameters = new ArrayList(); + } + + MemberHoldingTypeDetails current = this; + while (current != null) { + final MethodMetadata result = MemberFindingUtils.getDeclaredMethod( + current, methodName, parameters); + if (result != null) { + return result; + } + if (current instanceof ClassOrInterfaceTypeDetails) { + current = ((ClassOrInterfaceTypeDetails) current) + .getSuperclass(); + } + else { + current = null; + } + } + return null; + } + + public List getMethods() { + final List result = new ArrayList(); + MemberHoldingTypeDetails current = this; + while (current != null) { + for (final MethodMetadata method : current.getDeclaredMethods()) { + result.add(method); + } + if (current instanceof ClassOrInterfaceTypeDetails) { + current = ((ClassOrInterfaceTypeDetails) current) + .getSuperclass(); + } + else { + current = null; + } + } + return result; + } + + public JavaSymbolName getUniqueFieldName(final String proposedName) { + Validate.notBlank(proposedName, "Proposed field name is required"); + String candidateName = proposedName; + while (getField(new JavaSymbolName(candidateName)) != null) { + // The proposed field name is taken; differentiate it + candidateName += "_"; + } + // We've derived a unique name + return new JavaSymbolName(candidateName); + } + + public boolean implementsType(final JavaType interfaceType) { + return getImplementsTypes().contains(interfaceType); + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetailsBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetailsBuilder.java new file mode 100644 index 000000000..c384ad678 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetailsBuilder.java @@ -0,0 +1,454 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.JavaType; + +/** + * Abstract {@link Builder} to assist building {@link MemberHoldingTypeDetails} + * implementations. + * + * @author Ben Alex + * @since 1.1 + * @param the type of {@link MemberHoldingTypeDetails} being built + */ +public abstract class AbstractMemberHoldingTypeDetailsBuilder + extends AbstractIdentifiableAnnotatedJavaStructureBuilder { + + private final List declaredConstructors = new ArrayList(); + private final List declaredFields = new ArrayList(); + private final List declaredInitializers = new ArrayList(); + private final List declaredInnerTypes = new ArrayList(); + private final List declaredMethods = new ArrayList(); + private final List extendsTypes = new ArrayList(); + private final List implementsTypes = new ArrayList(); + + /** + * Constructor + * + * @param existing + */ + protected AbstractMemberHoldingTypeDetailsBuilder( + final MemberHoldingTypeDetails existing) { + super(existing); + init(existing); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + */ + protected AbstractMemberHoldingTypeDetailsBuilder( + final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + * @param existing + */ + protected AbstractMemberHoldingTypeDetailsBuilder( + final String declaredbyMetadataId, + final MemberHoldingTypeDetails existing) { + super(declaredbyMetadataId, existing); + init(existing); + } + + public final boolean addConstructor(final ConstructorMetadata constructor) { + if (constructor == null) { + return false; + } + return addConstructor(new ConstructorMetadataBuilder(constructor)); + } + + public final boolean addConstructor( + final ConstructorMetadataBuilder constructorBuilder) { + if (constructorBuilder == null + || !getDeclaredByMetadataId().equals( + constructorBuilder.getDeclaredByMetadataId())) { + return false; + } + onAddConstructor(constructorBuilder); + return declaredConstructors.add(constructorBuilder); + } + + public final boolean addExtendsTypes(final JavaType extendsType) { + if (extendsType == null) { + return false; + } + onAddExtendsTypes(extendsType); + return extendsTypes.add(extendsType); + } + + public final boolean addField(final FieldMetadata field) { + if (field == null) { + return false; + } + return addField(new FieldMetadataBuilder(field)); + } + + public final boolean addField(final FieldMetadataBuilder fieldBuilder) { + if (fieldBuilder == null + || !getDeclaredByMetadataId().equals( + fieldBuilder.getDeclaredByMetadataId())) { + return false; + } + onAddField(fieldBuilder); + return declaredFields.add(fieldBuilder); + } + + public final boolean addImplementsType(final JavaType implementsType) { + if (implementsType == null) { + return false; + } + onAddImplementType(implementsType); + return implementsTypes.add(implementsType); + } + + /** + * Adds the given imports to this builder if not already present + * + * @param imports the imports to add; can be null + * @since 1.2.0 + */ + public abstract void addImports(Collection imports); + + public final boolean addInitializer( + final InitializerMetadataBuilder initializer) { + if (initializer == null + || !getDeclaredByMetadataId().equals( + initializer.getDeclaredByMetadataId())) { + return false; + } + onAddInitializer(initializer); + return declaredInitializers.add(initializer); + } + + public final boolean addInnerType( + final ClassOrInterfaceTypeDetails innerType) { + if (innerType == null) { + return false; + } + return addInnerType(new ClassOrInterfaceTypeDetailsBuilder(innerType)); + } + + public final boolean addInnerType( + final ClassOrInterfaceTypeDetailsBuilder innerType) { + /* + * There was originally a check to see if the declaredMIDs matched, but + * this doesn't really make much sense. We need to come up with a better + * model for inner types. I thought about adding an enclosingType + * attribute but this prototype just felt like a hack. In the short term + * I have just disabled the MID comparison as I think this is fairly + * reasonable until this is given some more time. JTT - 24/08/11 + */ + if (innerType == null) { + return false; + } + onAddInnerType(innerType); + return declaredInnerTypes.add(innerType); + } + + /** + * Adds the given method to this builder + * + * @param method the method to add; can be null + * @return true if the state of this builder changed + */ + public final boolean addMethod(final MethodMetadata method) { + if (method == null) { + return false; + } + return addMethod(new MethodMetadataBuilder(method)); + } + + /** + * Adds the given method to this builder + * + * @param methodBuilder the method builder to add; ignored if + * null or if its MID doesn't match this builder's + * MID + * @return true if the state of this builder changed + */ + public final boolean addMethod(final MethodMetadataBuilder methodBuilder) { + if (methodBuilder == null + || !getDeclaredByMetadataId().equals( + methodBuilder.getDeclaredByMetadataId())) { + return false; + } + onAddMethod(methodBuilder); + return declaredMethods.add(methodBuilder); + } + + public final List buildConstructors() { + final List result = new ArrayList(); + for (final ConstructorMetadataBuilder builder : declaredConstructors) { + result.add(builder.build()); + } + return result; + } + + public final List buildFields() { + final List result = new ArrayList(); + for (final FieldMetadataBuilder builder : declaredFields) { + result.add(builder.build()); + } + return result; + } + + public final List buildInitializers() { + final List result = new ArrayList(); + for (final InitializerMetadataBuilder builder : declaredInitializers) { + result.add(builder.build()); + } + return result; + } + + public final List buildInnerTypes() { + final List result = new ArrayList(); + for (final ClassOrInterfaceTypeDetailsBuilder cidBuilder : declaredInnerTypes) { + result.add(cidBuilder.build()); + } + return result; + } + + public final List buildMethods() { + final List result = new ArrayList(); + for (final MethodMetadataBuilder builder : declaredMethods) { + result.add(builder.build()); + } + return result; + } + + /** + * Removes all declared methods from this builder + */ + public void clearDeclaredMethods() { + this.declaredMethods.clear(); + } + + public final List getDeclaredConstructors() { + return declaredConstructors; + } + + public final List getDeclaredFields() { + return declaredFields; + } + + public List getDeclaredInitializers() { + return declaredInitializers; + } + + public List getDeclaredInnerTypes() { + return declaredInnerTypes; + } + + /** + * Returns the declared methods in this builder + * + * @return an unmodifiable copy of this list + */ + public final List getDeclaredMethods() { + return Collections.unmodifiableList(declaredMethods); + } + + /** + * Returns the types that the built instance will extend, if any. Does not + * return a copy, i.e. modifying the returned list will modify this builder! + * TODO improve encapsulation by returning a defensive copy and + * updating callers accordingly + * + * @return a non-null list + */ + public final List getExtendsTypes() { + return extendsTypes; + } + + public final List getImplementsTypes() { + return implementsTypes; + } + + private void init(final MemberHoldingTypeDetails existing) { + for (final ConstructorMetadata element : existing + .getDeclaredConstructors()) { + declaredConstructors.add(new ConstructorMetadataBuilder(element)); + } + for (final FieldMetadata element : existing.getDeclaredFields()) { + declaredFields.add(new FieldMetadataBuilder(element)); + } + for (final MethodMetadata element : existing.getDeclaredMethods()) { + declaredMethods.add(new MethodMetadataBuilder(element)); + } + for (final ClassOrInterfaceTypeDetails element : existing + .getDeclaredInnerTypes()) { + declaredInnerTypes.add(new ClassOrInterfaceTypeDetailsBuilder( + element)); + } + for (final InitializerMetadata element : existing + .getDeclaredInitializers()) { + declaredInitializers.add(new InitializerMetadataBuilder(element)); + } + extendsTypes.addAll(existing.getExtendsTypes()); + implementsTypes.addAll(existing.getImplementsTypes()); + } + + protected void onAddConstructor( + final ConstructorMetadataBuilder constructorBuilder) { + } + + protected void onAddExtendsTypes(final JavaType extendsType) { + } + + protected void onAddField(final FieldMetadataBuilder fieldBuilder) { + } + + protected void onAddImplementType(final JavaType implementsType) { + } + + protected void onAddInitializer(final InitializerMetadataBuilder initializer) { + } + + protected void onAddInnerType( + final ClassOrInterfaceTypeDetailsBuilder innerType) { + } + + /** + * Subclasses can perform their own actions upon a method builder being + * added. This implementation does nothing. + * + * @param methodBuilder the method being added; never null + */ + protected void onAddMethod(final MethodMetadataBuilder methodBuilder) { + } + + /** + * Removes the given methods from this builder + * + * @param methodsToRemove can be null for none + * @return true if this builder changed as a result + * @see List#removeAll(Collection) + */ + public boolean removeAll( + final Collection methodsToRemove) { + if (methodsToRemove == null) { + return false; + } + return this.declaredMethods.removeAll(methodsToRemove); + } + + /** + * Ensures that the type being built does not extend any of the given types + * + * @param superTypes the types to remove as supertypes + */ + public void removeExtendsTypes(final JavaType... superTypes) { + extendsTypes.removeAll(Arrays.asList(superTypes)); + } + + /** + * Sets the builders for the constructors that are to be declared + * + * @param declaredConstructors can be null for none + */ + public final void setDeclaredConstructors( + final Collection declaredConstructors) { + this.declaredConstructors.clear(); + if (declaredConstructors != null) { + this.declaredConstructors.addAll(declaredConstructors); + } + } + + /** + * Sets the builders for the fields to be declared by the type being built + * + * @param declaredFields the builders to set (can be null for + * none) + */ + public final void setDeclaredFields( + final Collection declaredFields) { + this.declaredFields.clear(); + if (declaredFields != null) { + this.declaredFields.addAll(declaredFields); + } + } + + /** + * Sets the builders for the initializers of the type being built + * + * @param declaredInitializers the builders to set; can be null + * for none + */ + public void setDeclaredInitializers( + final Collection declaredInitializers) { + this.declaredInitializers.clear(); + if (declaredInitializers != null) { + this.declaredInitializers.addAll(declaredInitializers); + } + } + + /** + * Sets the builders for the inner types of the type being built + * + * @param declaredInnerTypes the builders to set; can be null + * for none + */ + public void setDeclaredInnerTypes( + final Collection declaredInnerTypes) { + this.declaredInnerTypes.clear(); + if (declaredInnerTypes != null) { + this.declaredInnerTypes.addAll(declaredInnerTypes); + } + } + + /** + * Sets the declared methods for this builder; equivalent to calling + * {@link #addMethod(MethodMetadataBuilder)} once for each item of the given + * {@link Iterable}. + * + * @param declaredMethods the methods to set; can be null for + * none, otherwise the {@link Iterable} is defensively copied + */ + public final void setDeclaredMethods( + final Iterable declaredMethods) { + this.declaredMethods.clear(); + if (declaredMethods != null) { + for (final MethodMetadataBuilder methodBuilder : declaredMethods) { + addMethod(methodBuilder); + } + } + } + + /** + * Sets the types that the built instance will extend + * + * @param extendsTypes can be null for none + */ + public final void setExtendsTypes( + final Collection extendsTypes) { + this.extendsTypes.clear(); + if (extendsTypes != null) { + this.extendsTypes.addAll(extendsTypes); + } + } + + /** + * Sets the types to be implemented by the type being built + * + * @param implementsTypes can be null for none + */ + public final void setImplementsTypes( + final Collection implementsTypes) { + this.implementsTypes.clear(); + if (implementsTypes != null) { + this.implementsTypes.addAll(implementsTypes); + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/AnnotationMetadataUtils.java b/classpath/src/main/java/org/springframework/roo/classpath/details/AnnotationMetadataUtils.java new file mode 100644 index 000000000..cf8507b75 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/AnnotationMetadataUtils.java @@ -0,0 +1,211 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.CharAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.DoubleAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.classpath.details.annotations.LongAttributeValue; +import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.ImportRegistrationResolver; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Utilities to use with {@link AnnotationMetadata}. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +public abstract class AnnotationMetadataUtils { + + private static String computeAttributeValue( + final AnnotationAttributeValue value, + final ImportRegistrationResolver resolver) { + String attributeValue = null; + if (value instanceof BooleanAttributeValue) { + attributeValue = ((BooleanAttributeValue) value).getValue() + .toString(); + } + else if (value instanceof CharAttributeValue) { + attributeValue = "'" + + ((CharAttributeValue) value).getValue().toString() + "'"; + } + else if (value instanceof ClassAttributeValue) { + final JavaType clazz = ((ClassAttributeValue) value).getValue(); + if (resolver == null + || resolver + .isFullyQualifiedFormRequiredAfterAutoImport(clazz)) { + attributeValue = clazz.getFullyQualifiedTypeName() + ".class"; + } + else { + attributeValue = clazz.getSimpleTypeName() + ".class"; + } + } + else if (value instanceof DoubleAttributeValue) { + final DoubleAttributeValue dbl = (DoubleAttributeValue) value; + if (dbl.isFloatingPrecisionOnly()) { + attributeValue = dbl.getValue().toString() + "F"; + } + else { + attributeValue = dbl.getValue().toString() + "D"; + } + } + else if (value instanceof EnumAttributeValue) { + final EnumDetails enumDetails = ((EnumAttributeValue) value) + .getValue(); + final JavaType clazz = enumDetails.getType(); + if (resolver == null + || resolver + .isFullyQualifiedFormRequiredAfterAutoImport(clazz)) { + attributeValue = clazz.getFullyQualifiedTypeName() + "." + + enumDetails.getField().getSymbolName(); + } + else { + attributeValue = clazz.getSimpleTypeName() + "." + + enumDetails.getField().getSymbolName(); + } + } + else if (value instanceof IntegerAttributeValue) { + attributeValue = ((IntegerAttributeValue) value).getValue() + .toString(); + } + else if (value instanceof LongAttributeValue) { + attributeValue = ((LongAttributeValue) value).getValue().toString() + + "L"; + } + else if (value instanceof StringAttributeValue) { + attributeValue = "\"" + ((StringAttributeValue) value).getValue() + + "\""; + } + else if (value instanceof NestedAnnotationAttributeValue) { + final AnnotationMetadata annotationMetadata = ((NestedAnnotationAttributeValue) value) + .getValue(); + final StringBuilder data = new StringBuilder("@"); + final JavaType annotationType = annotationMetadata + .getAnnotationType(); + if (resolver == null + || resolver + .isFullyQualifiedFormRequiredAfterAutoImport(annotationType)) { + data.append(annotationType.getFullyQualifiedTypeName()); + } + else { + data.append(annotationType.getSimpleTypeName()); + } + if (!annotationMetadata.getAttributeNames().isEmpty()) { + data.append("("); + int i = 0; + for (final JavaSymbolName attributeName : annotationMetadata + .getAttributeNames()) { + i++; + if (i > 1) { + data.append(", "); + } + data.append(attributeName.getSymbolName()).append(" = "); + data.append(computeAttributeValue( + annotationMetadata.getAttribute(attributeName), + resolver)); + } + data.append(")"); + } + attributeValue = data.toString(); + } + else if (value instanceof ArrayAttributeValue) { + final ArrayAttributeValue array = (ArrayAttributeValue) value; + final StringBuilder data = new StringBuilder("{ "); + int i = 0; + for (final AnnotationAttributeValue val : array.getValue()) { + i++; + if (i > 1) { + data.append(", "); + } + data.append(computeAttributeValue(val, resolver)); + } + data.append(" }"); + attributeValue = data.toString(); + } + return attributeValue; + } + + /** + * Converts the annotation into a string-based form. + * + * @param annotation to covert (required) + * @return a string-based representation (never null) + */ + public static String toSourceForm(final AnnotationMetadata annotation) { + return toSourceForm(annotation, null); + } + + /** + * Converts the annotation into a string-based form. + * + * @param annotation to covert (required) + * @param resolver to use for automatic addition of used types (may be null) + * @return a string-based representation (never null) + */ + public static String toSourceForm(final AnnotationMetadata annotation, + final ImportRegistrationResolver resolver) { + Validate.notNull(annotation, "Annotation required"); + + final StringBuilder sb = new StringBuilder(); + sb.append("@"); + + if (resolver != null) { + if (resolver.isFullyQualifiedFormRequiredAfterAutoImport(annotation + .getAnnotationType())) { + sb.append(annotation.getAnnotationType() + .getFullyQualifiedTypeName()); + } + else { + sb.append(annotation.getAnnotationType().getSimpleTypeName()); + } + } + else { + sb.append(annotation.getAnnotationType() + .getFullyQualifiedTypeName()); + } + + if (annotation.getAttributeNames().isEmpty()) { + return sb.toString(); + } + + sb.append("("); + boolean requireComma = false; + for (final JavaSymbolName attributeName : annotation + .getAttributeNames()) { + // Add a comma, to separate the last annotation attribute + if (requireComma) { + sb.append(", "); + requireComma = false; + } + + // Compute the value + final AnnotationAttributeValue value = annotation + .getAttribute(attributeName); + + final String attributeValue = computeAttributeValue(value, resolver); + + if (attributeValue != null) { + // We have a supported attribute + if (!"value".equals(attributeName.getSymbolName()) + || annotation.getAttributeNames().size() > 1) { + sb.append(attributeName.getSymbolName()); + sb.append(" = "); + } + sb.append(attributeValue); + requireComma = true; + } + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/BeanInfoUtils.java b/classpath/src/main/java/org/springframework/roo/classpath/details/BeanInfoUtils.java new file mode 100644 index 000000000..cf6d6b1f1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/BeanInfoUtils.java @@ -0,0 +1,223 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Provides utility methods for querying JavaBeans. + * + * @author Ben Alex + * @since 1.1.1 + */ +public final class BeanInfoUtils { + + /** + * Returns the accessor name for the given field. + * + * @param field the field to determine the accessor name + * @return the accessor method name + */ + public static JavaSymbolName getAccessorMethodName(final FieldMetadata field) { + Validate.notNull(field, "Field required"); + return getAccessorMethodName(field.getFieldName(), field.getFieldType()); + } + + /** + * Returns the accessor name for the given field name and field type. + * + * @param fieldName the field name used to determine the accessor name + * @param fieldType the field type + * @return the accessor method name + */ + public static JavaSymbolName getAccessorMethodName( + final JavaSymbolName fieldName, final JavaType fieldType) { + Validate.notNull(fieldName, "Field name required"); + Validate.notNull(fieldType, "Field type required"); + final String capitalizedFieldName = StringUtils.capitalize(fieldName + .getSymbolName()); + return fieldType.equals(JavaType.BOOLEAN_PRIMITIVE) ? new JavaSymbolName( + "is" + capitalizedFieldName) : new JavaSymbolName("get" + + capitalizedFieldName); + } + + /** + * Attempts to locate the field which is represented by the presented java + * bean method. + *

    + * Not every JavaBean getter or setter actually backs to a field with an + * identical name. In such cases, null will be returned. + * + * @param memberDetails the member holders to scan (required) + * @param method the method name (required) + * @return the field if found, or null if it could not be found + */ + public static FieldMetadata getFieldForJavaBeanMethod( + final MemberDetails memberDetails, final MethodMetadata method) { + Validate.notNull(memberDetails, "Member details required"); + Validate.notNull(method, "Method is required"); + final JavaSymbolName propertyName = getPropertyNameForJavaBeanMethod(method); + return getFieldForPropertyName(memberDetails, propertyName); + } + + /** + * Attempts to locate the field which is represented by the presented + * property name. + *

    + * Not every JavaBean getter or setter actually backs to a field with an + * identical name. In such cases, null will be returned. + * + * @param memberDetails the member holders to scan (required) + * @param propertyName the property name (required) + * @return the field if found, or null if it could not be found + */ + public static FieldMetadata getFieldForPropertyName( + final MemberDetails memberDetails, final JavaSymbolName propertyName) { + Validate.notNull(memberDetails, "Member details required"); + Validate.notNull(propertyName, "Property name required"); + for (final MemberHoldingTypeDetails holder : memberDetails.getDetails()) { + FieldMetadata result = holder.getDeclaredField(propertyName); + if (result != null) { + return result; + } + // To get here means we couldn't find the property using the exact + // same case; + // try to scan with a lowercase first character (see ROO-203) + result = holder.getDeclaredField(new JavaSymbolName(StringUtils + .uncapitalize(propertyName.getSymbolName()))); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Returns the mutator name for the given field. + * + * @param field the field used to determine the accessor name + * @return the mutator method name + */ + public static JavaSymbolName getMutatorMethodName(final FieldMetadata field) { + Validate.notNull(field, "Field metadata required"); + return getMutatorMethodName(field.getFieldName()); + } + + /** + * Returns the mutator name for the given field name. + * + * @param fieldName the field name used to determine the accessor name + * @return the mutator method name + */ + public static JavaSymbolName getMutatorMethodName( + final JavaSymbolName fieldName) { + Validate.notNull(fieldName, "Field name required"); + return new JavaSymbolName("set" + + StringUtils.capitalize(fieldName.getSymbolName())); + } + + /** + * Obtains the property name for the specified JavaBean accessor or mutator + * method. This is determined by discarding the first 2 or 3 letters of the + * method name (depending whether it is a "get", "set" or "is" method). + * There is no special searching back to the actual field name. + * + * @param method to search (required, and must be a "get", "set" or "is" + * method) + * @return the name of the property (never returned null) + */ + public static JavaSymbolName getPropertyNameForJavaBeanMethod( + final MethodMetadata method) { + Validate.notNull(method, "Method is required"); + final String name = method.getMethodName().getSymbolName(); + if (name.startsWith("set") || name.startsWith("get")) { + return new JavaSymbolName(name.substring(3)); + } + if (name.startsWith("is")) { + return new JavaSymbolName(name.substring(2)); + } + throw new IllegalStateException("Method name '" + name + + "' does not observe JavaBean method naming conventions"); + } + + /** + * Attempts to locate an accessor and a mutator method for a given field. + *

    + * Not every JavaBean getter or setter actually backs to a field with an + * identical name. In such cases, false will be returned. + * + * @param field the member holders to scan (required) + * @param memberDetails the member details to scan + * @return true if an accessor and a mutator are present, or false otherwise + */ + public static boolean hasAccessorAndMutator(final FieldMetadata field, + final MemberDetails memberDetails) { + Validate.notNull(field, "Field required"); + Validate.notNull(memberDetails, "Member details required"); + + if (memberDetails.getMethod(getAccessorMethodName(field), + new ArrayList()) != null + && memberDetails.getMethod(getMutatorMethodName(field), + Arrays.asList(field.getFieldType())) != null) { + return true; + } + + return false; + } + + /** + * Indicates if the presented method compiles with the JavaBean conventions + * around accessor methods (public, "set" or "is", 0 args etc). + * + * @param method to evaluate (required) + * @return true if the presented method is an accessor, otherwise false + */ + public static boolean isAccessorMethod(final MethodMetadata method) { + Validate.notNull(method, "Method is required"); + return (method.getMethodName().getSymbolName().startsWith("get") || method + .getMethodName().getSymbolName().startsWith("is")) + && method.getParameterTypes().isEmpty() + && Modifier.isPublic(method.getModifier()); + } + + /** + * Determines whether the presented entity is a test class or not. + * + * @param entity the type to test + * @return true if the entity is likely not a test class, otherwise false + */ + public static boolean isEntityReasonablyNamed(final JavaType entity) { + Validate.notNull(entity, "Entity required"); + return !entity.getSimpleTypeName().startsWith("Test") + && !entity.getSimpleTypeName().endsWith("TestCase") + && !entity.getSimpleTypeName().endsWith("Test"); + } + + /** + * Indicates if the presented method compiles with the JavaBean conventions + * around mutator methods (public, "set", 1 arg etc). + * + * @param method to evaluate (required) + * @return true if the presented method is a mutator, otherwise false + */ + public static boolean isMutatorMethod(final MethodMetadata method) { + Validate.notNull(method, "Method is required"); + return method.getMethodName().getSymbolName().startsWith("set") + && method.getParameterTypes().size() == 1 + && Modifier.isPublic(method.getModifier()); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private BeanInfoUtils() { + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ClassOrInterfaceTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ClassOrInterfaceTypeDetails.java new file mode 100644 index 000000000..b6a3039a6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ClassOrInterfaceTypeDetails.java @@ -0,0 +1,57 @@ +package org.springframework.roo.classpath.details; + +import java.util.List; +import java.util.Set; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Provides information about the different components of a class, interface or + * enum. + *

    + * As per this interface's extension of {@link MemberHoldingTypeDetails}, + * instances of implementing classes must be immutable. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ClassOrInterfaceTypeDetails extends MemberHoldingTypeDetails { + + /** + * Indicates whether this class or interface declares a field with the given + * name + * + * @param fieldName the field name to check for (can be null) + * @return false if a null field name is given + */ + boolean declaresField(JavaSymbolName fieldName); + + /** + * Lists the enum constants this type provides. Always empty except if an + * enum type. + * + * @return the constants (may be empty, but never null) + */ + List getEnumConstants(); + + /** + * @return the explicitly-registered imports this user wishes to have + * defined in the type (cannot be null, but may be empty) + */ + Set getRegisteredImports(); + + /** + * Obtains the superclass if this is a class and it is available. + * + * @return the superclass, if available (null will be returned for + * interfaces, or if the class isn't available) + */ + ClassOrInterfaceTypeDetails getSuperclass(); + + /** + * Indicates whether this class, interface, or enum is abstract + * + * @return see above + */ + boolean isAbstract(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ClassOrInterfaceTypeDetailsBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ClassOrInterfaceTypeDetailsBuilder.java new file mode 100644 index 000000000..516a1c2fe --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ClassOrInterfaceTypeDetailsBuilder.java @@ -0,0 +1,319 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Builder for {@link ClassOrInterfaceTypeDetails}. + * + * @author Ben Alex + * @since 1.1 + */ +public class ClassOrInterfaceTypeDetailsBuilder extends + AbstractMemberHoldingTypeDetailsBuilder { + + private final List enumConstants = new ArrayList(); + private JavaType name; + private PhysicalTypeCategory physicalTypeCategory; + private final Set registeredImports = new HashSet(); + private ClassOrInterfaceTypeDetailsBuilder superclass; + + /** + * Constructor + * + * @param existing + */ + public ClassOrInterfaceTypeDetailsBuilder( + final ClassOrInterfaceTypeDetails existing) { + super(existing); + init(existing); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + */ + public ClassOrInterfaceTypeDetailsBuilder(final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + * @param existing + */ + public ClassOrInterfaceTypeDetailsBuilder( + final String declaredbyMetadataId, + final ClassOrInterfaceTypeDetails existing) { + super(declaredbyMetadataId, existing); + init(existing); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + * @param modifier + * @param name + * @param physicalTypeCategory + */ + public ClassOrInterfaceTypeDetailsBuilder( + final String declaredbyMetadataId, final int modifier, + final JavaType name, final PhysicalTypeCategory physicalTypeCategory) { + this(declaredbyMetadataId); + setModifier(modifier); + this.name = name; + this.physicalTypeCategory = physicalTypeCategory; + } + + /** + * Adds the given imports to this builder, if not already present + * + * @param imports the imports to add; can be null for none + * @return true if the state of this builder changed + */ + public boolean add(final Collection imports) { + if (imports == null) { + return false; + } + return registeredImports.addAll(imports); + } + + /** + * Adds the given import to this builder + * + * @param importMetadata the import to add; can be null not to + * add anything + * @return true if the state of this builder changed + */ + public boolean add(final ImportMetadata importMetadata) { + if (importMetadata == null) { + return false; + } + return registeredImports.add(importMetadata); + } + + public boolean addEnumConstant(final JavaSymbolName javaSymbolName) { + return enumConstants.add(javaSymbolName); + } + + @Override + public void addImports(final Collection imports) { + if (imports != null) { + registeredImports.addAll(imports); + } + } + + public ClassOrInterfaceTypeDetails build() { + ClassOrInterfaceTypeDetails superclass = null; + if (this.superclass != null) { + superclass = this.superclass.build(); + } + return new DefaultClassOrInterfaceTypeDetails(getCustomData().build(), + getDeclaredByMetadataId(), getModifier(), buildAnnotations(), + getName(), getPhysicalTypeCategory(), buildConstructors(), + buildFields(), buildMethods(), buildInnerTypes(), + buildInitializers(), superclass, getExtendsTypes(), + getImplementsTypes(), getEnumConstants(), + getRegisteredImports()); + } + + /** + * Copies this builder's modifications into the given ITD builder + * + * @param targetBuilder the ITD builder to receive the additions (required) + * @param governorTypeDetails the {@link ClassOrInterfaceTypeDetails} of the + * governor (required) + */ + public void copyTo( + final AbstractMemberHoldingTypeDetailsBuilder targetBuilder, + final ClassOrInterfaceTypeDetails governorTypeDetails) { + Validate.notNull(targetBuilder, "Target builder required"); + Validate.notNull(governorTypeDetails, + "Governor member holding types required"); + // Copy fields + fieldAdditions: for (final FieldMetadataBuilder field : getDeclaredFields()) { + for (final FieldMetadataBuilder targetField : targetBuilder + .getDeclaredFields()) { + if (targetField.getFieldType().equals(field.getFieldType()) + && targetField.getFieldName().equals( + field.getFieldName())) { + // The field already exists, so move on + continue fieldAdditions; + } + } + if (!governorTypeDetails.declaresField(field.getFieldName())) { + targetBuilder.addField(field); + } + } + + // Copy methods + methodAdditions: for (final MethodMetadataBuilder method : getDeclaredMethods()) { + for (final MethodMetadataBuilder targetMethod : targetBuilder + .getDeclaredMethods()) { + if (targetMethod.getMethodName().equals(method.getMethodName()) + && targetMethod.getParameterTypes().equals( + method.getParameterTypes())) { + continue methodAdditions; + } + } + targetBuilder.addMethod(method); + } + + // Copy annotations + annotationAdditions: for (final AnnotationMetadataBuilder annotation : getAnnotations()) { + for (final AnnotationMetadataBuilder targetAnnotation : targetBuilder + .getAnnotations()) { + if (targetAnnotation.getAnnotationType().equals( + annotation.getAnnotationType())) { + continue annotationAdditions; + } + } + targetBuilder.addAnnotation(annotation); + } + + // Copy custom data + if (getCustomData() != null) { + targetBuilder.append(getCustomData().build()); + } + + // Copy constructors + constructorAdditions: for (final ConstructorMetadataBuilder constructor : getDeclaredConstructors()) { + for (final ConstructorMetadataBuilder targetConstructor : targetBuilder + .getDeclaredConstructors()) { + if (targetConstructor.getParameterTypes().equals( + constructor.getParameterTypes())) { + continue constructorAdditions; + } + } + targetBuilder.addConstructor(constructor); + } + + // Copy initializers + for (final InitializerMetadataBuilder initializer : getDeclaredInitializers()) { + targetBuilder.addInitializer(initializer); + } + + // Copy inner types + innerTypeAdditions: for (final ClassOrInterfaceTypeDetailsBuilder innerType : getDeclaredInnerTypes()) { + for (final ClassOrInterfaceTypeDetailsBuilder targetInnerType : targetBuilder + .getDeclaredInnerTypes()) { + if (targetInnerType.getName().equals(innerType.getName())) { + continue innerTypeAdditions; + } + } + targetBuilder.addInnerType(innerType); + } + + // Copy extends types + for (final JavaType type : getExtendsTypes()) { + if (!targetBuilder.getExtendsTypes().contains(type)) { + targetBuilder.addExtendsTypes(type); + } + } + + // Copy implements types + for (final JavaType type : getImplementsTypes()) { + if (!targetBuilder.getImplementsTypes().contains(type)) { + targetBuilder.addImplementsType(type); + } + } + + // Copy imports + targetBuilder.addImports(getRegisteredImports()); + } + + public List getEnumConstants() { + return enumConstants; + } + + public JavaType getName() { + return name; + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return physicalTypeCategory; + } + + /** + * Returns this builder's imports + * + * @return a non-null copy + */ + public Set getRegisteredImports() { + return new HashSet(registeredImports); + } + + public ClassOrInterfaceTypeDetailsBuilder getSuperclass() { + return superclass; + } + + private void init(final ClassOrInterfaceTypeDetails existing) { + name = existing.getName(); + physicalTypeCategory = existing.getPhysicalTypeCategory(); + if (existing.getSuperclass() != null) { + superclass = new ClassOrInterfaceTypeDetailsBuilder( + existing.getSuperclass()); + } + enumConstants.addAll(existing.getEnumConstants()); + registeredImports.clear(); + registeredImports.addAll(existing.getRegisteredImports()); + } + + /** + * Sets this builder's enum constants to the given collection + * + * @param enumConstants can be null for none, otherwise is + * defensively copied + */ + public void setEnumConstants( + final Collection enumConstants) { + this.enumConstants.clear(); + if (enumConstants != null) { + this.enumConstants.addAll(enumConstants); + } + } + + public void setName(final JavaType name) { + this.name = name; + } + + public void setPhysicalTypeCategory( + final PhysicalTypeCategory physicalTypeCategory) { + this.physicalTypeCategory = physicalTypeCategory; + } + + /** + * Sets this builder's imports + * + * @param registeredImports can be null for none; defensively + * copied + */ + public void setRegisteredImports( + final Collection registeredImports) { + this.registeredImports.clear(); + if (registeredImports != null) { + this.registeredImports.addAll(registeredImports); + } + } + + public void setSuperclass(final ClassOrInterfaceTypeDetails superclass) { + setSuperclass(new ClassOrInterfaceTypeDetailsBuilder(superclass)); + } + + public void setSuperclass( + final ClassOrInterfaceTypeDetailsBuilder superclass) { + this.superclass = superclass; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ConstructorMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ConstructorMetadata.java new file mode 100644 index 000000000..7091329f6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ConstructorMetadata.java @@ -0,0 +1,10 @@ +package org.springframework.roo.classpath.details; + +/** + * Metadata concerning a particular constructor. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ConstructorMetadata extends InvocableMemberMetadata { +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ConstructorMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ConstructorMetadataBuilder.java new file mode 100644 index 000000000..4dc4e3d1b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ConstructorMetadataBuilder.java @@ -0,0 +1,47 @@ +package org.springframework.roo.classpath.details; + +/** + * Builder for {@link ConstructorMetadata}. + * + * @author Ben Alex + * @since 1.1 + */ +public class ConstructorMetadataBuilder extends + AbstractInvocableMemberMetadataBuilder { + + /** + * Constructor + * + * @param existing + */ + public ConstructorMetadataBuilder(final ConstructorMetadata existing) { + super(existing); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + */ + public ConstructorMetadataBuilder(final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + * @param existing + */ + public ConstructorMetadataBuilder(final String declaredbyMetadataId, + final ConstructorMetadata existing) { + super(declaredbyMetadataId, existing); + } + + public ConstructorMetadata build() { + return new DefaultConstructorMetadata(getCustomData().build(), + getDeclaredByMetadataId(), getModifier(), buildAnnotations(), + getParameterTypes(), getParameterNames(), getThrowsTypes(), + getBodyBuilder().getOutput()); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DeclaredFieldAnnotationDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DeclaredFieldAnnotationDetails.java new file mode 100644 index 000000000..bb31e70eb --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DeclaredFieldAnnotationDetails.java @@ -0,0 +1,73 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; + +/** + * Convenience class to hold annotation details which should be introduced to a + * field via an AspectJ ITD + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class DeclaredFieldAnnotationDetails { + + private final FieldMetadata field; + private final AnnotationMetadata fieldAnnotation; + private final boolean removeAnnotation; + + /** + * Overloaded constructor which is used in the most typical case of ADDING + * an annotation to a field, not removing one. + * + * @param field FieldMetadata of existing field (may not be null) + * @param fieldAnnotation Annotation to be added to field via an ITD (may + * not be null) + */ + public DeclaredFieldAnnotationDetails(final FieldMetadata field, + final AnnotationMetadata fieldAnnotation) { + this(field, fieldAnnotation, false); + } + + /** + * Constructor must contain {@link FieldMetadata} of existing field (may + * already contain field annotations) and a list of new Annotations which + * should be introduced by an AspectJ ITD. The added annotations can not + * already be present in {@link FieldMetadata}. + * + * @param field FieldMetadata of existing field (may not be null) + * @param fieldAnnotation Annotation to be added to field via an ITD (may + * not be null) + * @param removeAnnotation if true, will cause the specified annotation to + * be REMOVED via AspectJ's "-" syntax (usually would be false) + */ + public DeclaredFieldAnnotationDetails(final FieldMetadata field, + final AnnotationMetadata fieldAnnotation, + final boolean removeAnnotation) { + Validate.notNull(field, "Field metadata required"); + Validate.notNull(fieldAnnotation, "Field annotation required"); + if (removeAnnotation) { + Validate.isTrue( + fieldAnnotation.getAttributeNames().isEmpty(), + "Field annotation '@%s' (on target field %s.%s) must not have any attributes when requesting its removal", + fieldAnnotation.getAnnotationType().getSimpleTypeName(), + field.getFieldType().getFullyQualifiedTypeName(), field + .getFieldName().getSymbolName()); + } + this.field = field; + this.fieldAnnotation = fieldAnnotation; + this.removeAnnotation = removeAnnotation; + } + + public FieldMetadata getField() { + return field; + } + + public AnnotationMetadata getFieldAnnotation() { + return fieldAnnotation; + } + + public final boolean isRemoveAnnotation() { + return removeAnnotation; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DeclaredMethodAnnotationDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DeclaredMethodAnnotationDetails.java new file mode 100644 index 000000000..37e969f86 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DeclaredMethodAnnotationDetails.java @@ -0,0 +1,43 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; + +/** + * Convenience class to hold annotation details which should be introduced to a + * method via an AspectJ ITD + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class DeclaredMethodAnnotationDetails { + + private final AnnotationMetadata methodAnnotation; + private final MethodMetadata methodMetadata; + + /** + * Contructor must contain {@link MethodMetadata} of existing method (may + * already contain method annotations) and a list of new Annotations which + * should be introduced by an AspectJ ITD. The added annotations can not + * already be present in {@link MethodMetadata}. + * + * @param methodMetadata MethodMetadata of existing method (may not be null) + * @param methodAnnotation Annotation to be added to field via an ITD (may + * not be null) + */ + public DeclaredMethodAnnotationDetails(final MethodMetadata methodMetadata, + final AnnotationMetadata methodAnnotation) { + Validate.notNull(methodMetadata, "Method metadata required"); + Validate.notNull(methodAnnotation, "Method annotation required"); + this.methodMetadata = methodMetadata; + this.methodAnnotation = methodAnnotation; + } + + public AnnotationMetadata getMethodAnnotation() { + return methodAnnotation; + } + + public MethodMetadata getMethodMetadata() { + return methodMetadata; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultClassOrInterfaceTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultClassOrInterfaceTypeDetails.java new file mode 100644 index 000000000..66529f296 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultClassOrInterfaceTypeDetails.java @@ -0,0 +1,233 @@ +package org.springframework.roo.classpath.details; + +import static org.springframework.roo.classpath.PhysicalTypeCategory.CLASS; +import static org.springframework.roo.classpath.PhysicalTypeCategory.ENUMERATION; +import static org.springframework.roo.classpath.PhysicalTypeCategory.INTERFACE; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Default representation of a {@link ClassOrInterfaceTypeDetails}. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultClassOrInterfaceTypeDetails extends + AbstractMemberHoldingTypeDetails implements ClassOrInterfaceTypeDetails { + + private List declaredConstructors = new ArrayList(); + private List declaredFields = new ArrayList(); + private List declaredInitializers = new ArrayList(); + private List declaredInnerTypes = new ArrayList(); + private List declaredMethods = new ArrayList(); + private List enumConstants = new ArrayList(); + private List extendsTypes = new ArrayList(); + private List implementsTypes = new ArrayList(); + private final JavaType name; + private final PhysicalTypeCategory physicalTypeCategory; + private Set registeredImports = new HashSet(); + private final ClassOrInterfaceTypeDetails superclass; + + /** + * Constructor is package protected to mandate the use of + * {@link ClassOrInterfaceTypeDetailsBuilder} + * + * @param customData + * @param declaredByMetadataId + * @param modifier + * @param annotations + * @param name + * @param physicalTypeCategory + * @param declaredConstructors + * @param declaredFields + * @param declaredMethods + * @param declaredInnerTypes + * @param declaredInitializers + * @param superclass + * @param extendsTypes + * @param implementsTypes + * @param enumConstants + * @param registeredImports + */ + DefaultClassOrInterfaceTypeDetails(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final List annotations, final JavaType name, + final PhysicalTypeCategory physicalTypeCategory, + final List declaredConstructors, + final List declaredFields, + final List declaredMethods, + final List declaredInnerTypes, + final List declaredInitializers, + final ClassOrInterfaceTypeDetails superclass, + final List extendsTypes, + final List implementsTypes, + final List enumConstants, + final Collection registeredImports) { + + super(customData, declaredByMetadataId, modifier, annotations); + Validate.notNull(name, "Name required"); + Validate.notNull(physicalTypeCategory, + "Physical type category required"); + + this.name = name; + this.physicalTypeCategory = physicalTypeCategory; + this.superclass = superclass; + + if (declaredConstructors != null) { + this.declaredConstructors = declaredConstructors; + } + + if (declaredFields != null) { + this.declaredFields = declaredFields; + } + + if (declaredMethods != null) { + this.declaredMethods = declaredMethods; + } + + if (declaredInnerTypes != null) { + this.declaredInnerTypes = declaredInnerTypes; + } + + if (declaredInitializers != null) { + this.declaredInitializers = declaredInitializers; + } + + if (extendsTypes != null) { + this.extendsTypes = extendsTypes; + } + + if (implementsTypes != null) { + this.implementsTypes = implementsTypes; + } + + if (enumConstants != null && physicalTypeCategory == ENUMERATION) { + this.enumConstants = enumConstants; + } + + this.registeredImports = new HashSet(); + if (registeredImports != null) { + this.registeredImports.addAll(registeredImports); + } + } + + public boolean declaresField(final JavaSymbolName fieldName) { + return getDeclaredField(fieldName) != null; + } + + public boolean extendsType(final JavaType type) { + return extendsTypes.contains(type); + } + + public List getDeclaredConstructors() { + return Collections.unmodifiableList(declaredConstructors); + } + + public List getDeclaredFields() { + return Collections.unmodifiableList(declaredFields); + } + + public List getDeclaredInitializers() { + return Collections.unmodifiableList(declaredInitializers); + } + + public List getDeclaredInnerTypes() { + return Collections.unmodifiableList(declaredInnerTypes); + } + + public List getDeclaredMethods() { + return Collections.unmodifiableList(declaredMethods); + } + + @SuppressWarnings("unchecked") + public List getDynamicFinderNames() { + final List dynamicFinders = new ArrayList(); + final Object finders = getCustomData().get( + CustomDataKeys.DYNAMIC_FINDER_NAMES); + if (finders instanceof Collection) { + dynamicFinders.addAll((Collection) finders); + } + return dynamicFinders; + } + + public List getEnumConstants() { + return Collections.unmodifiableList(enumConstants); + } + + public List getExtendsTypes() { + return Collections.unmodifiableList(extendsTypes); + } + + public List getImplementsTypes() { + return Collections.unmodifiableList(implementsTypes); + } + + public JavaType getName() { + return getType(); + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return physicalTypeCategory; + } + + public Set getRegisteredImports() { + return Collections.unmodifiableSet(registeredImports); + } + + public ClassOrInterfaceTypeDetails getSuperclass() { + return superclass; + } + + public JavaType getType() { + return name; + } + + public boolean implementsAny(final JavaType... types) { + for (final JavaType type : types) { + if (implementsTypes.contains(type)) { + return true; + } + } + return false; + } + + public boolean isAbstract() { + return physicalTypeCategory == INTERFACE + || physicalTypeCategory == CLASS + && Modifier.isAbstract(getModifier()); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name); + builder.append("modifier", Modifier.toString(getModifier())); + builder.append("physicalTypeCategory", physicalTypeCategory); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("declaredConstructors", declaredConstructors); + builder.append("declaredFields", declaredFields); + builder.append("declaredMethods", declaredMethods); + builder.append("enumConstants", enumConstants); + builder.append("superclass", superclass); + builder.append("extendsTypes", extendsTypes); + builder.append("implementsTypes", implementsTypes); + builder.append("annotations", getAnnotations()); + builder.append("customData", getCustomData()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultConstructorMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultConstructorMetadata.java new file mode 100644 index 000000000..bc92d6ff0 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultConstructorMetadata.java @@ -0,0 +1,45 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; +import java.util.List; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Default implementation of {@link ConstructorMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultConstructorMetadata extends AbstractInvocableMemberMetadata + implements ConstructorMetadata { + + // Package protected to mandate the use of ConstructorMetadataBuilder + DefaultConstructorMetadata(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final List annotations, + final List parameterTypes, + final List parameterNames, + final List throwsTypes, final String body) { + super(customData, declaredByMetadataId, modifier, annotations, + parameterTypes, parameterNames, throwsTypes, body); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("modifier", Modifier.toString(getModifier())); + builder.append("parameterTypes", getParameterTypes()); + builder.append("parameterNames", getParameterNames()); + builder.append("annotations", getAnnotations()); + builder.append("customData", getCustomData()); + builder.append("body", getBody()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultFieldMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultFieldMetadata.java new file mode 100644 index 000000000..7acb60e21 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultFieldMetadata.java @@ -0,0 +1,79 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Default implementation of {@link FieldMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultFieldMetadata extends + AbstractIdentifiableAnnotatedJavaStructureProvider implements + FieldMetadata { + + private final String fieldInitializer; + private final JavaSymbolName fieldName; + private final JavaType fieldType; + private CommentStructure commentStructure; + + // Package protected to mandate the use of FieldMetadataBuilder + DefaultFieldMetadata(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final List annotations, + final JavaSymbolName fieldName, final JavaType fieldType, + final String fieldInitializer) { + super(customData, declaredByMetadataId, modifier, annotations); + Validate.notBlank(declaredByMetadataId, + "Declared by metadata ID required"); + Validate.notNull(fieldName, "Field name required"); + Validate.notNull(fieldType, "Field type required"); + this.fieldName = fieldName; + this.fieldType = fieldType; + this.fieldInitializer = fieldInitializer; + } + + @Override + public CommentStructure getCommentStructure() { + return commentStructure; + } + + @Override + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } + + public String getFieldInitializer() { + return fieldInitializer; + } + + public JavaSymbolName getFieldName() { + return fieldName; + } + + public JavaType getFieldType() { + return fieldType; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("modifier", Modifier.toString(getModifier())); + builder.append("fieldType", fieldType); + builder.append("fieldName", fieldName); + builder.append("fieldInitializer", fieldInitializer); + builder.append("annotations", getAnnotations()); + builder.append("customData", getCustomData()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultImportMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultImportMetadata.java new file mode 100644 index 000000000..77971c015 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultImportMetadata.java @@ -0,0 +1,74 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.classpath.details.comments.CommentedJavaStructure; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Default implementation of {@link ImportMetadata}. + * + * @author James Tyrrell + * @since 1.1.1 + */ +public class DefaultImportMetadata extends + AbstractIdentifiableJavaStructureProvider implements ImportMetadata, + CommentedJavaStructure { + + private final JavaPackage importPackage; + private final JavaType importType; + private CommentStructure commentStructure; + private boolean isAsterisk = false; + private boolean isStatic = false; + + // Package protected to mandate the use of ImportMetadataBuilder + DefaultImportMetadata(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final JavaPackage importPackage, final JavaType importType, + final boolean isStatic, final boolean isAsterisk) { + super(customData, declaredByMetadataId, modifier); + this.importPackage = importPackage; + this.importType = importType; + this.isStatic = isStatic; + this.isAsterisk = isAsterisk; + } + + public JavaPackage getImportPackage() { + return importPackage; + } + + public JavaType getImportType() { + return importType; + } + + public boolean isAsterisk() { + return isAsterisk; + } + + public boolean isStatic() { + return isStatic; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("typePackage", importPackage); + builder.append("type", importType); + builder.append("isStatic", isStatic); + builder.append("isAsterisk", isAsterisk); + return builder.toString(); + } + + @Override + public CommentStructure getCommentStructure() { + return commentStructure; + } + + @Override + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultInitializerMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultInitializerMetadata.java new file mode 100644 index 000000000..3d778804f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultInitializerMetadata.java @@ -0,0 +1,48 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.model.CustomData; + +/** + * Default implementation of {@link InitializerMetadata}. + * + * @author James Tyrrell + * @since 1.1.1 + */ +public class DefaultInitializerMetadata extends + AbstractIdentifiableJavaStructureProvider implements + InitializerMetadata { + + private final String body; + private final boolean isStatic; + + // Package protected to mandate the use of InitializerMetadataBuilder + DefaultInitializerMetadata(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final boolean isStatic, final String body) { + super(customData, declaredByMetadataId, modifier); + this.isStatic = isStatic; + this.body = body; + } + + public final String getBody() { + return body; + } + + public boolean isStatic() { + return isStatic; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("modifier", Modifier.toString(getModifier())); + builder.append("customData", getCustomData()); + builder.append("isStatic", isStatic()); + builder.append("body", getBody()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultItdTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultItdTypeDetails.java new file mode 100644 index 000000000..4a5e8a9dc --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultItdTypeDetails.java @@ -0,0 +1,249 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdSourceFileComposer; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.CustomDataAccessor; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Default representation of an {@link ItdTypeDetails}. + *

    + * Provides a basic {@link #hashCode()} that is used for detecting significant + * changes in {@link AbstractItdMetadataProvider} and avoiding downstream + * notifications accordingly. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +public class DefaultItdTypeDetails extends AbstractMemberHoldingTypeDetails + implements ItdTypeDetails { + + static final PhysicalTypeCategory PHYSICAL_TYPE_CATEGORY = PhysicalTypeCategory.ITD; + + private final JavaType aspect; + private final List declaredConstructors = new ArrayList(); + private final List declaredFields = new ArrayList(); + private final List declaredMethods = new ArrayList(); + private final List extendsTypes = new ArrayList(); + private final List fieldAnnotations = new ArrayList(); + private final ClassOrInterfaceTypeDetails governor; + private final List implementsTypes = new ArrayList(); + private final List innerTypes = new ArrayList(); + private final List methodAnnotations = new ArrayList(); + private final boolean privilegedAspect; + private final Set registeredImports = new HashSet(); + private final Set declarePrecedence = new LinkedHashSet(); + + + /** + * Constructor (package protected to enforce the use of the corresponding + * builder) + * + * @param customData + * @param declaredByMetadataId + * @param modifier + * @param governor the type to receive the introductions (required) + * @param aspect (required) + * @param privilegedAspect + * @param registeredImports can be null + * @param declaredConstructors can be null + * @param declaredFields can be null + * @param declaredMethods can be null + * @param extendsTypes can be null + * @param implementsTypes can be null + * @param typeAnnotations can be null + * @param fieldAnnotations can be null + * @param methodAnnotations can be null + * @param innerTypes can be null + * @param declarePrecedence can be null + */ + DefaultItdTypeDetails( + final CustomData customData, + final String declaredByMetadataId, + final int modifier, + final ClassOrInterfaceTypeDetails governor, + final JavaType aspect, + final boolean privilegedAspect, + final Collection registeredImports, + final Collection declaredConstructors, + final Collection declaredFields, + final Collection declaredMethods, + final Collection extendsTypes, + final Collection implementsTypes, + final Collection typeAnnotations, + final Collection fieldAnnotations, + final Collection methodAnnotations, + final Collection innerTypes, + final Collection declarePrecedence) { + + super(customData, declaredByMetadataId, modifier, typeAnnotations); + Validate.notNull(aspect, "Aspect required"); + Validate.notNull(governor, + "Governor (to receive the introductions) required"); + + this.aspect = aspect; + this.governor = governor; + this.privilegedAspect = privilegedAspect; + + CollectionUtils.populate(this.declaredConstructors, + declaredConstructors); + CollectionUtils.populate(this.declaredFields, declaredFields); + CollectionUtils.populate(this.declaredMethods, declaredMethods); + CollectionUtils.populate(this.extendsTypes, extendsTypes); + CollectionUtils.populate(this.fieldAnnotations, fieldAnnotations); + CollectionUtils.populate(this.implementsTypes, implementsTypes); + CollectionUtils.populate(this.innerTypes, innerTypes); + CollectionUtils.populate(this.methodAnnotations, methodAnnotations); + CollectionUtils.populate(this.registeredImports, registeredImports); + CollectionUtils.populate(this.declarePrecedence, declarePrecedence); + } + + public boolean extendsType(final JavaType type) { + return extendsTypes.contains(type); + } + + public JavaType getAspect() { + return aspect; + } + + public List getDeclaredConstructors() { + return Collections.unmodifiableList(declaredConstructors); + } + + public List getDeclaredFields() { + return Collections.unmodifiableList(declaredFields); + } + + public List getDeclaredInitializers() { + return Collections.emptyList(); + } + + public List getDeclaredInnerTypes() { + return Collections.emptyList(); + } + + public List getDeclaredMethods() { + return Collections.unmodifiableList(declaredMethods); + } + + public List getDynamicFinderNames() { + return Collections.emptyList(); + } + + public List getExtendsTypes() { + return Collections.unmodifiableList(extendsTypes); + } + + public List getFieldAnnotations() { + return Collections.unmodifiableList(fieldAnnotations); + } + + public ClassOrInterfaceTypeDetails getGovernor() { + return governor; + } + + public List getImplementsTypes() { + return Collections.unmodifiableList(implementsTypes); + } + + public List getInnerTypes() { + return Collections.unmodifiableList(innerTypes); + } + + public List getMethodAnnotations() { + return Collections.unmodifiableList(methodAnnotations); + } + + public JavaType getName() { + return getType(); + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return PHYSICAL_TYPE_CATEGORY; + } + + public Set getRegisteredImports() { + return Collections.unmodifiableSet(registeredImports); + } + + public JavaType getType() { + return governor.getType(); + } + + @Override + public int hashCode() { + int hash = aspect.hashCode() * governor.getName().hashCode() + * governor.getModifier() * governor.getCustomData().hashCode() + * PHYSICAL_TYPE_CATEGORY.hashCode() + * (privilegedAspect ? 2 : 3); + hash *= includeCustomDataHash(declaredConstructors); + hash *= includeCustomDataHash(declaredFields); + hash *= includeCustomDataHash(declaredMethods); + hash *= new ItdSourceFileComposer(this).getOutput().hashCode(); + return hash; + } + + public boolean implementsAny(final JavaType... types) { + for (final JavaType type : types) { + if (implementsTypes.contains(type)) { + return true; + } + } + return false; + } + + private int includeCustomDataHash( + final Collection coll) { + int result = 1; + for (final CustomDataAccessor accessor : coll) { + result *= accessor.getCustomData().hashCode(); + } + return result; + } + + public boolean isPrivilegedAspect() { + return privilegedAspect; + } + + @Override + public Set getDeclarePrecedence() { + return Collections.unmodifiableSet(declarePrecedence); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("modifier", getModifier()); + builder.append("name", governor); + builder.append("aspect", aspect); + builder.append("physicalTypeCategory", PHYSICAL_TYPE_CATEGORY); + builder.append("privilegedAspect", privilegedAspect); + builder.append("registeredImports", registeredImports); + builder.append("declaredConstructors", declaredConstructors); + builder.append("declaredFields", declaredFields); + builder.append("declaredMethods", declaredMethods); + builder.append("extendsTypes", extendsTypes); + builder.append("fieldAnnotations", fieldAnnotations); + builder.append("methodAnnotations", methodAnnotations); + builder.append("typeAnnotations", getAnnotations()); + builder.append("innerTypes", innerTypes); + builder.append("customData", getCustomData()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultMethodMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultMethodMetadata.java new file mode 100644 index 000000000..11e28d2e2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultMethodMetadata.java @@ -0,0 +1,79 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Default implementation of {@link MethodMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultMethodMetadata extends AbstractInvocableMemberMetadata + implements MethodMetadata { + + private final JavaSymbolName methodName; + private final JavaType returnType; + + // Package protected to mandate the use of MethodMetadataBuilder + DefaultMethodMetadata(final CustomData customData, + final String declaredByMetadataId, final int modifier, + final List annotations, + final JavaSymbolName methodName, final JavaType returnType, + final List parameterTypes, + final List parameterNames, + final List throwsTypes, final String body) { + super(customData, declaredByMetadataId, modifier, annotations, + parameterTypes, parameterNames, throwsTypes, body); + Validate.notNull(methodName, "Method name required"); + Validate.notNull(returnType, "Return type required"); + this.methodName = methodName; + this.returnType = returnType; + } + + public JavaSymbolName getMethodName() { + return methodName; + } + + public final JavaType getReturnType() { + return returnType; + } + + public boolean hasSameName(final MethodMetadata... otherMethods) { + for (final MethodMetadata otherMethod : otherMethods) { + if (otherMethod != null + && methodName.equals(otherMethod.getMethodName())) { + return true; + } + } + return false; + } + + public boolean isStatic() { + return Modifier.isStatic(getModifier()); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("declaredByMetadataId", getDeclaredByMetadataId()); + builder.append("modifier", Modifier.toString(getModifier())); + builder.append("methodName", methodName); + builder.append("parameterTypes", getParameterTypes()); + builder.append("parameterNames", getParameterNames()); + builder.append("returnType", returnType); + builder.append("annotations", getAnnotations()); + builder.append("throwsTypes", getThrowsTypes()); + builder.append("customData", getCustomData()); + builder.append("body", getBody()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeDetails.java new file mode 100644 index 000000000..bbbb09c05 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeDetails.java @@ -0,0 +1,46 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeDetails; +import org.springframework.roo.model.AbstractCustomDataAccessorProvider; +import org.springframework.roo.model.CustomDataImpl; +import org.springframework.roo.model.JavaType; + +/** + * Simple implementation of {@link PhysicalTypeDetails} that is suitable for + * {@link PhysicalTypeCategory#OTHER} or sub-classing by category-specific + * implementations. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultPhysicalTypeDetails extends + AbstractCustomDataAccessorProvider implements PhysicalTypeDetails { + + private final JavaType javaType; + private final PhysicalTypeCategory physicalTypeCategory; + + public DefaultPhysicalTypeDetails( + final PhysicalTypeCategory physicalTypeCategory, + final JavaType javaType) { + super(CustomDataImpl.NONE); + Validate.notNull(javaType, "Java type required"); + Validate.notNull(physicalTypeCategory, + "Physical type category required"); + this.javaType = javaType; + this.physicalTypeCategory = physicalTypeCategory; + } + + public JavaType getName() { + return getType(); + } + + public PhysicalTypeCategory getPhysicalTypeCategory() { + return physicalTypeCategory; + } + + public JavaType getType() { + return javaType; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeMetadata.java new file mode 100644 index 000000000..db9142b80 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeMetadata.java @@ -0,0 +1,84 @@ +package org.springframework.roo.classpath.details; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.itd.ItdMetadataProvider; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.model.JavaType; + +/** + * The default {@link PhysicalTypeMetadata} implementation. + */ +public class DefaultPhysicalTypeMetadata extends AbstractMetadataItem implements + PhysicalTypeMetadata { + + private final ClassOrInterfaceTypeDetails cid; + private final String physicalLocationCanonicalPath; + + /** + * Constructor + * + * @param metadataIdentificationString the ID to assign this + * {@link org.springframework.roo.metadata.MetadataItem} (must + * satisfy {@link PhysicalTypeIdentifier#isValid(String)}) + * @param physicalLocationCanonicalPath the canonical path of the file + * containing this Java type (required) + * @param cid the details of this type (required) + */ + public DefaultPhysicalTypeMetadata( + final String metadataIdentificationString, + final String physicalLocationCanonicalPath, + final ClassOrInterfaceTypeDetails cid) { + super(metadataIdentificationString); + Validate.isTrue( + PhysicalTypeIdentifier.isValid(metadataIdentificationString), + "Metadata id '%s' is not a valid physical type identifier", + metadataIdentificationString); + Validate.notBlank(physicalLocationCanonicalPath, + "Physical location canonical path required"); + Validate.notNull(cid, "Class or interface type details required"); + this.cid = cid; + this.physicalLocationCanonicalPath = physicalLocationCanonicalPath; + } + + public String getItdCanoncialPath(final ItdMetadataProvider metadataProvider) { + // Delegate to the correctly spelled method + return getItdCanonicalPath(metadataProvider); + } + + public String getItdCanonicalPath(final ItdMetadataProvider metadataProvider) { + Validate.notNull(metadataProvider, "Metadata provider required"); + final int dropFrom = physicalLocationCanonicalPath.lastIndexOf(".java"); + Validate.isTrue(dropFrom > -1, "Unexpected governor filename format '" + + physicalLocationCanonicalPath + "'"); + return physicalLocationCanonicalPath.substring(0, dropFrom) + "_Roo_" + + metadataProvider.getItdUniquenessFilenameSuffix() + ".aj"; + } + + public JavaType getItdJavaType(final ItdMetadataProvider metadataProvider) { + Validate.notNull(metadataProvider, "Metadata provider required"); + return new JavaType(PhysicalTypeIdentifier.getJavaType(getId()) + .getFullyQualifiedTypeName() + + "_Roo_" + + metadataProvider.getItdUniquenessFilenameSuffix()); + } + + public ClassOrInterfaceTypeDetails getMemberHoldingTypeDetails() { + return cid; + } + + public String getPhysicalLocationCanonicalPath() { + return physicalLocationCanonicalPath; + } + + public JavaType getType() { + return cid.getName(); + } + + @Override + public String toString() { + // Used for example by the "metadata for id" command + return getClass().getSimpleName() + " for " + cid.getName(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/FieldMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/FieldMetadata.java new file mode 100644 index 000000000..ad8b11084 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/FieldMetadata.java @@ -0,0 +1,31 @@ +package org.springframework.roo.classpath.details; + +import org.springframework.roo.classpath.details.comments.CommentedJavaStructure; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Metadata concerning a particular field. + * + * @author Ben Alex + * @since 1.0 + */ +public interface FieldMetadata extends IdentifiableAnnotatedJavaStructure, + CommentedJavaStructure { + + /** + * @return the field initializer, if known (may be null if there is no + * initializer) + */ + String getFieldInitializer(); + + /** + * @return the name of the field (never null) + */ + JavaSymbolName getFieldName(); + + /** + * @return the type of field (never null) + */ + JavaType getFieldType(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/FieldMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/FieldMetadataBuilder.java new file mode 100644 index 000000000..df84ec1de --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/FieldMetadataBuilder.java @@ -0,0 +1,152 @@ +package org.springframework.roo.classpath.details; + +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.classpath.details.comments.JavadocComment; +import org.springframework.roo.classpath.operations.jsr303.FieldDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Builder for {@link FieldMetadata}. + * + * @author Ben Alex + * @since 1.1 + */ +public class FieldMetadataBuilder extends + AbstractIdentifiableAnnotatedJavaStructureBuilder { + + private String fieldInitializer; + private JavaSymbolName fieldName; + private JavaType fieldType; + private CommentStructure commentStructure; + + public FieldMetadataBuilder(final FieldMetadata existing) { + super(existing); + init(existing.getFieldName(), existing.getFieldType(), + existing.getFieldInitializer()); + + this.commentStructure = existing.getCommentStructure(); + } + + public FieldMetadataBuilder(final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + public FieldMetadataBuilder(final String declaredbyMetadataId, + final FieldMetadata existing) { + super(declaredbyMetadataId, existing); + init(existing.getFieldName(), existing.getFieldType(), + existing.getFieldInitializer()); + + this.commentStructure = existing.getCommentStructure(); + } + + /** + * Constructor for a builder with the given field values + * + * @param declaredbyMetadataId a MID for a specific instance + * @param modifier as per {@link java.lang.reflect.Modifier} + * @param fieldName the field name (required) + * @param fieldType the field type (required) + * @param fieldInitializer the Java expression for the field's initial value + * (can be null for none) + */ + public FieldMetadataBuilder(final String declaredbyMetadataId, + final int modifier, final JavaSymbolName fieldName, + final JavaType fieldType, final String fieldInitializer) { + this(declaredbyMetadataId); + setModifier(modifier); + init(fieldName, fieldType, fieldInitializer); + } + + /** + * Constructor + * + * @param declaredbyMetadataId + * @param modifier + * @param annotations + * @param fieldName + * @param fieldType + */ + public FieldMetadataBuilder(final String declaredbyMetadataId, + final int modifier, + final List annotations, + final JavaSymbolName fieldName, final JavaType fieldType) { + this(declaredbyMetadataId); + setModifier(modifier); + setAnnotations(annotations); + this.fieldName = fieldName; + this.fieldType = fieldType; + } + + /** + * Constructor + * + * @param fieldDetails + */ + public FieldMetadataBuilder(final FieldDetails fieldDetails) { + this(fieldDetails.getPhysicalTypeIdentifier(), fieldDetails + .getModifiers(), fieldDetails.getAnnotations(), fieldDetails + .getFieldName(), fieldDetails.getFieldType()); + + if (fieldDetails.getComment() != null) { + commentStructure = new CommentStructure(); + JavadocComment javadocComment = new JavadocComment( + fieldDetails.getComment()); + commentStructure.addComment(javadocComment, + CommentStructure.CommentLocation.BEGINNING); + } + } + + public FieldMetadata build() { + DefaultFieldMetadata md = new DefaultFieldMetadata(getCustomData() + .build(), getDeclaredByMetadataId(), getModifier(), + buildAnnotations(), getFieldName(), getFieldType(), + getFieldInitializer()); + md.setCommentStructure(getCommentStructure()); + + return md; + } + + public String getFieldInitializer() { + return fieldInitializer; + } + + public JavaSymbolName getFieldName() { + return fieldName; + } + + public JavaType getFieldType() { + return fieldType; + } + + private void init(final JavaSymbolName fieldName, final JavaType fieldType, + final String fieldInitializer) { + this.fieldName = fieldName; + this.fieldType = fieldType; + this.fieldInitializer = fieldInitializer; + } + + public void setFieldInitializer(final String fieldInitializer) { + this.fieldInitializer = fieldInitializer; + } + + public void setFieldName(final JavaSymbolName fieldName) { + this.fieldName = fieldName; + } + + public void setFieldType(final JavaType fieldType) { + this.fieldType = fieldType; + } + + public CommentStructure getCommentStructure() { + return commentStructure; + } + + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/IdentifiableAnnotatedJavaStructure.java b/classpath/src/main/java/org/springframework/roo/classpath/details/IdentifiableAnnotatedJavaStructure.java new file mode 100644 index 000000000..b40b32c5b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/IdentifiableAnnotatedJavaStructure.java @@ -0,0 +1,39 @@ +package org.springframework.roo.classpath.details; + +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.JavaType; + +/** + * Indicates an {@link IdentifiableJavaStructure} which can also be annotated. + * + * @author Ben Alex + * @since 1.1 + */ +public interface IdentifiableAnnotatedJavaStructure extends + IdentifiableJavaStructure { + + /** + * Locates the specified annotation on this structure. + * + * @param type to locate (required) + * @return the annotation, or null if not found + * @since 1.2.0 + */ + AnnotationMetadata getAnnotation(final JavaType type); + + /** + * @return annotations on this structure (never null, but may be empty) + */ + List getAnnotations(); + + /** + * Locates an annotation on this class and its superclasses. + * + * @param annotationType annotation to locate (required) + * @return the annotation, or null if not found + * @since 1.2.0 + */ + AnnotationMetadata getTypeAnnotation(final JavaType annotationType); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/IdentifiableJavaStructure.java b/classpath/src/main/java/org/springframework/roo/classpath/details/IdentifiableJavaStructure.java new file mode 100644 index 000000000..54acd1363 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/IdentifiableJavaStructure.java @@ -0,0 +1,29 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; + +import org.springframework.roo.model.CustomDataAccessor; + +/** + * Allows an identifiable Java structure (ie a member or a type) to be traced + * back to its declaring type. + * + * @author Ben Alex + * @since 1.0 + */ +public interface IdentifiableJavaStructure extends CustomDataAccessor { + + /** + * @return the ID of the metadata that declared this member (never null) + */ + String getDeclaredByMetadataId(); + + /** + * Indicates the access modifier of the member. The integer is formatted in + * accordance with {@link Modifier}. Returning 0 is acceptable the less + * common structures that don't support modifiers (eg static initializers). + * + * @return the modifier, if available (required) + */ + int getModifier(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ImportMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ImportMetadata.java new file mode 100644 index 000000000..018602bcd --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ImportMetadata.java @@ -0,0 +1,38 @@ +package org.springframework.roo.classpath.details; + +import org.springframework.roo.classpath.details.comments.CommentedJavaStructure; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Metadata concerning a particular import. + *

    + * As always with metadata types, instances of this class are immutable once + * constructed. + * + * @author James Tyrrell + * @since 1.1.1 + */ +public interface ImportMetadata extends IdentifiableJavaStructure, + CommentedJavaStructure { + + /** + * @return the import package (null if type import) + */ + JavaPackage getImportPackage(); + + /** + * @return the import type (null if package import) + */ + JavaType getImportType(); + + /** + * @return true if the import was a wildcard (eg "import com.foo.*;") + */ + boolean isAsterisk(); + + /** + * @return true if the import used the "static" keyword + */ + boolean isStatic(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ImportMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ImportMetadataBuilder.java new file mode 100644 index 000000000..cc5f77bd6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ImportMetadataBuilder.java @@ -0,0 +1,109 @@ +package org.springframework.roo.classpath.details; + +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Builder for {@link ImportMetadata}. + * + * @author James Tyrrell + * @since 1.1.1 + */ +public class ImportMetadataBuilder extends + AbstractIdentifiableJavaStructureBuilder { + + /** + * Builds an import of the given {@link JavaType} for use by the given + * caller. + * + * @param callerMID the metadata ID of the compilation unit to receive the + * import (required) + * @param typeToImport the type to import (required) + * @return a non-null, non-static, non-wildcard import + * @since 1.2.0 + */ + public static ImportMetadata getImport(final String callerMID, + final JavaType typeToImport) { + return new ImportMetadataBuilder(callerMID, 0, + typeToImport.getPackage(), typeToImport, false, false).build(); + } + + private JavaPackage importPackage; + private JavaType importType; + private boolean isAsterisk; + + private boolean isStatic; + private CommentStructure commentStructure; + + public ImportMetadataBuilder(final ImportMetadata existing) { + super(existing); + importPackage = existing.getImportPackage(); + importType = existing.getImportType(); + isStatic = existing.isStatic(); + isAsterisk = existing.isAsterisk(); + commentStructure = existing.getCommentStructure(); + } + + public ImportMetadataBuilder(final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + public ImportMetadataBuilder(final String declaredbyMetadataId, + final int modifier, final JavaPackage importPackage, + final JavaType importType, final boolean isStatic, + final boolean isAsterisk) { + this(declaredbyMetadataId); + setModifier(modifier); + this.importPackage = importPackage; + this.importType = importType; + this.isStatic = isStatic; + this.isAsterisk = isAsterisk; + } + + public ImportMetadata build() { + return new DefaultImportMetadata(getCustomData().build(), + getDeclaredByMetadataId(), getModifier(), importPackage, + importType, isStatic, isAsterisk); + } + + public JavaPackage getImportPackage() { + return importPackage; + } + + public JavaType getImportType() { + return importType; + } + + public boolean isAsterisk() { + return isAsterisk; + } + + public boolean isStatic() { + return isStatic; + } + + public void setAsterisk(final boolean asterisk) { + isAsterisk = asterisk; + } + + public void setImportPackage(final JavaPackage importPackage) { + this.importPackage = importPackage; + } + + public void setImportType(final JavaType importType) { + this.importType = importType; + } + + public void setStatic(final boolean aStatic) { + isStatic = aStatic; + } + + public CommentStructure getCommentStructure() { + return commentStructure; + } + + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/InitializerMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/InitializerMetadata.java new file mode 100644 index 000000000..e5cef6392 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/InitializerMetadata.java @@ -0,0 +1,14 @@ +package org.springframework.roo.classpath.details; + +/** + * Metadata concerning an initializer. + * + * @author James Tyrrell + * @since 1.1.1 + */ +public interface InitializerMetadata extends IdentifiableJavaStructure { + + String getBody(); + + boolean isStatic(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/InitializerMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/InitializerMetadataBuilder.java new file mode 100644 index 000000000..0358be6c2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/InitializerMetadataBuilder.java @@ -0,0 +1,70 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; + +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; + +/** + * Builder for {@link InitializerMetadata}. + * + * @author Ben Alex + * @since 1.1.1 + */ +public class InitializerMetadataBuilder extends + AbstractIdentifiableJavaStructureBuilder { + + private InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + private boolean isStatic; + + public InitializerMetadataBuilder(final InitializerMetadata existing) { + super(existing); + isStatic = existing.getModifier() == Modifier.STATIC + || existing.isStatic(); + bodyBuilder.append(existing.getBody()); + } + + public InitializerMetadataBuilder(final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + public InitializerMetadataBuilder(final String declaredbyMetadataId, + final int modifier, boolean isStatic, + final InvocableMemberBodyBuilder bodyBuilder) { + this(declaredbyMetadataId); + setModifier(modifier); + if (modifier == Modifier.STATIC) { + isStatic = true; + } + this.isStatic = isStatic; + this.bodyBuilder = bodyBuilder; + } + + public InitializerMetadata build() { + return new DefaultInitializerMetadata(getCustomData().build(), + getDeclaredByMetadataId(), getModifier(), isStatic, + getBodyBuilder().getOutput()); + } + + public String getBody() { + if (bodyBuilder != null) { + return bodyBuilder.getOutput(); + } + return null; + } + + public InvocableMemberBodyBuilder getBodyBuilder() { + return bodyBuilder; + } + + public boolean isStatic() { + return isStatic; + } + + public void setBodyBuilder(final InvocableMemberBodyBuilder bodyBuilder) { + this.bodyBuilder = bodyBuilder; + } + + public void setStatic(final boolean isStatic) { + this.isStatic = isStatic; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/InvocableMemberMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/InvocableMemberMetadata.java new file mode 100644 index 000000000..e092ae57e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/InvocableMemberMetadata.java @@ -0,0 +1,39 @@ +package org.springframework.roo.classpath.details; + +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.comments.CommentedJavaStructure; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Metadata concerning an invocable member, namely a method or constructor. + * + * @author Ben Alex + * @since 1.0 + */ +public interface InvocableMemberMetadata extends + IdentifiableAnnotatedJavaStructure, CommentedJavaStructure { + + /** + * @return the body of the method, if available (can be null if unavailable) + */ + String getBody(); + + /** + * @return the parameter names, if available (never null, but may be an + * empty) + */ + List getParameterNames(); + + /** + * @return the parameter types (never null, but may be an empty) + */ + List getParameterTypes(); + + /** + * @return a list of types that the invocable member can throw (never null) + */ + List getThrowsTypes(); +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ItdTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ItdTypeDetails.java new file mode 100644 index 000000000..b69dd144d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ItdTypeDetails.java @@ -0,0 +1,86 @@ +package org.springframework.roo.classpath.details; + +import java.util.List; +import java.util.Set; + +import org.springframework.roo.model.JavaType; + +/** + * Provides information about an ITD. + *

    + * For simplicity of implementation this is not a complete representation of all + * members and other information available via Java bytecode. For example, + * static initialisers and inner classes are unsupported. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +public interface ItdTypeDetails extends MemberHoldingTypeDetails { + + /** + * Returns the name of type which holds the aspect itself. + *

    + * Note that the type receiving the introductions can be determined via + * {@link #getName()}. + * + * @return the aspect {@link JavaType} (never null) + */ + JavaType getAspect(); + + /** + * Lists the field-level annotations. + *

    + * This includes those annotations declared on the field, together with + * those defined via the ITD "declare @field: DestinationType: @Annotation" + * feature. + * + * @return an unmodifiable representation of the field and the annotations + * declared on this field (may be empty, but never null) + */ + List getFieldAnnotations(); + + /** + * Returns the {@link ClassOrInterfaceTypeDetails} representing the governor + * of this ITD. + * + * @return the governor {@link ClassOrInterfaceTypeDetails} (never null) + */ + ClassOrInterfaceTypeDetails getGovernor(); + + /** + * Lists the inner types. + * + * @return an unmodifiable representation of the inner types (may be empty + * but never null) + */ + List getInnerTypes(); + + /** + * Lists the method-level annotations. + *

    + * This includes those annotations declared on the method, together with + * those defined via the ITD "declare @field: DestinationType: @Annotation" + * feature. + * + * @return an unmodifiable representation of the method and the annotations + * declared on this method (may be empty, but never null) + */ + List getMethodAnnotations(); + + /** + * @return the explicitly-registered imports this user wishes to have + * defined in the ITD (cannot be null, but may be empty) + */ + Set getRegisteredImports(); + + boolean isPrivilegedAspect(); + + /** + * Set of aspect declared on {@code declare precedence} + * AspectJ declaration. + * + * @return ordered aspect declared + */ + Set getDeclarePrecedence(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/ItdTypeDetailsBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/ItdTypeDetailsBuilder.java new file mode 100644 index 000000000..e2bb07f46 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/ItdTypeDetailsBuilder.java @@ -0,0 +1,299 @@ +package org.springframework.roo.classpath.details; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.ImportRegistrationResolver; +import org.springframework.roo.model.ImportRegistrationResolverImpl; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Assists in the building of an {@link ItdTypeDetails} instance. + *

    + * All methods on this class (which does NOT include the constructor) accept + * null arguments, and will automatically ignore any attempt to add an + * {@link IdentifiableJavaStructure} that is not use the same + * declaredByMetadataId as when the instance was constructed. + *

    + * In addition, any method on this class which accepts an + * {@link InvocableMemberMetadata} will verify a + * {@link InvocableMemberMetadata#getBody()} is provided. This therefore detects + * programming errors which result from requesting a member to be included in an + * ITD but without providing the actual executable body for that member. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +public class ItdTypeDetailsBuilder extends + AbstractMemberHoldingTypeDetailsBuilder { + + private final JavaType aspect; + private final List fieldAnnotations = new ArrayList(); + private final ClassOrInterfaceTypeDetails governor; + private final ImportRegistrationResolver importRegistrationResolver; + private final List methodAnnotations = new ArrayList(); + private final boolean privilegedAspect; + private final Set declarePrecedence; + + /** + * Constructor based on an existing ITD + * + * @param existing (required) + */ + public ItdTypeDetailsBuilder(final ItdTypeDetails existing) { + super(existing.getDeclaredByMetadataId(), existing); + aspect = existing.getAspect(); + governor = existing.getGovernor(); + importRegistrationResolver = new ImportRegistrationResolverImpl( + aspect.getPackage()); + privilegedAspect = existing.isPrivilegedAspect(); + declarePrecedence = existing.getDeclarePrecedence(); + } + + /** + * Constructor + * + * @param declaredByMetadataId + * @param governor (required) + * @param aspect (required) + * @param privilegedAspect + */ + public ItdTypeDetailsBuilder(final String declaredByMetadataId, + final ClassOrInterfaceTypeDetails governor, final JavaType aspect, + final boolean privilegedAspect) { + super(declaredByMetadataId); + Validate.notNull(governor, + "Name (to receive the introductions) required"); + Validate.notNull(aspect, "Aspect required"); + this.aspect = aspect; + this.governor = governor; + importRegistrationResolver = new ImportRegistrationResolverImpl( + aspect.getPackage()); + this.privilegedAspect = privilegedAspect; + this.declarePrecedence = new LinkedHashSet(); + } + + public void addFieldAnnotation( + final DeclaredFieldAnnotationDetails declaredFieldAnnotationDetails) { + if (declaredFieldAnnotationDetails == null) { + return; + } + final JavaType declaredBy = PhysicalTypeIdentifier + .getJavaType(declaredFieldAnnotationDetails.getField() + .getDeclaredByMetadataId()); + final boolean hasAnnotation = MemberFindingUtils.getAnnotationOfType( + declaredFieldAnnotationDetails.getField().getAnnotations(), + declaredFieldAnnotationDetails.getFieldAnnotation() + .getAnnotationType()) != null; + if (!declaredFieldAnnotationDetails.isRemoveAnnotation()) { + Validate.isTrue( + !hasAnnotation, + "Field annotation '@%s' is already present on the target field '%s.%s' (ITD target '%s')", + declaredFieldAnnotationDetails.getFieldAnnotation() + .getAnnotationType().getSimpleTypeName(), + declaredBy.getFullyQualifiedTypeName(), + declaredFieldAnnotationDetails.getField().getFieldName() + .getSymbolName(), + aspect.getFullyQualifiedTypeName()); + } + else { + Validate.isTrue( + hasAnnotation, + "Field annotation '@%s' cannot be removed as it is not present on the target field '%s.%s' (ITD target '%s')", + declaredFieldAnnotationDetails.getFieldAnnotation() + .getAnnotationType().getSimpleTypeName(), + declaredBy.getFullyQualifiedTypeName(), + declaredFieldAnnotationDetails.getField().getFieldName() + .getSymbolName(), + aspect.getFullyQualifiedTypeName()); + } + fieldAnnotations.add(declaredFieldAnnotationDetails); + } + + @Override + public void addImports(final Collection imports) { + if (imports != null) { + for (final ImportMetadata anImport : imports) { + importRegistrationResolver.addImport(anImport.getImportType()); + } + } + } + + public void addMethodAnnotation( + final DeclaredMethodAnnotationDetails declaredMethodAnnotationDetails) { + if (declaredMethodAnnotationDetails == null) { + return; + } + final JavaType declaredBy = PhysicalTypeIdentifier + .getJavaType(declaredMethodAnnotationDetails + .getMethodMetadata().getDeclaredByMetadataId()); + final boolean hasAnnotation = MemberFindingUtils.getAnnotationOfType( + declaredMethodAnnotationDetails.getMethodMetadata() + .getAnnotations(), declaredMethodAnnotationDetails + .getMethodAnnotation().getAnnotationType()) != null; + Validate.isTrue( + !hasAnnotation, + "Method annotation '@%s' is already present on the target field '%s.%s' (ITD target '%s')", + declaredMethodAnnotationDetails.getMethodAnnotation() + .getAnnotationType().getSimpleTypeName(), + declaredBy.getFullyQualifiedTypeName(), + declaredMethodAnnotationDetails.getMethodMetadata() + .getMethodName().getSymbolName(), + aspect.getFullyQualifiedTypeName()); + methodAnnotations.add(declaredMethodAnnotationDetails); + } + + @Deprecated + // Should use addAnnotation() instead + public void addTypeAnnotation(final AnnotationMetadata annotationMetadata) { + addAnnotation(annotationMetadata); + } + + /** + * Set the aspects to use on {@code declare precedence} + * AspectJ declaration. + * + * @param aspects + */ + public void setDeclarePrecedence(JavaType...aspects) { + if (aspects != null && aspects.length > 0 ){ + Validate.isTrue(aspects.length > 1,"precedence must contain, at least, 2 aspects"); + } + CollectionUtils.populate(declarePrecedence, Arrays.asList(aspects)); + } + + public ItdTypeDetails build() { + return new DefaultItdTypeDetails(getCustomData().build(), + getDeclaredByMetadataId(), getModifier(), governor, aspect, + privilegedAspect, + importRegistrationResolver.getRegisteredImports(), + buildConstructors(), buildFields(), buildMethods(), + getExtendsTypes(), getImplementsTypes(), buildAnnotations(), + fieldAnnotations, methodAnnotations, buildInnerTypes(), + declarePrecedence); + } + + public ImportRegistrationResolver getImportRegistrationResolver() { + return importRegistrationResolver; + } + + @Override + protected void onAddAnnotation(final AnnotationMetadataBuilder md) { + Validate.isTrue( + governor.getAnnotation(md.getAnnotationType()) == null, + "Type annotation '%s' already defined in target type '%s' (ITD target '%s')", + md.getAnnotationType(), governor.getName() + .getFullyQualifiedTypeName(), aspect + .getFullyQualifiedTypeName()); + Validate.isTrue( + build().getAnnotation(md.getAnnotationType()) == null, + "Type annotation '%s' already defined in ITD (ITD target '%s')", + md.getAnnotationType(), aspect.getFullyQualifiedTypeName()); + } + + @Override + protected void onAddConstructor(final ConstructorMetadataBuilder md) { + Validate.isTrue( + governor.getDeclaredConstructor(AnnotatedJavaType + .convertFromAnnotatedJavaTypes(md.getParameterTypes())) == null, + "Constructor with %d parameters already defined in target type '%s' (ITD target '%s')", + md.getParameterTypes().size(), governor.getName() + .getFullyQualifiedTypeName(), aspect + .getFullyQualifiedTypeName()); + Validate.isTrue( + build().getDeclaredConstructor( + AnnotatedJavaType.convertFromAnnotatedJavaTypes(md + .getParameterTypes())) == null, + "Constructor with %d parameters already defined in ITD (ITD target '%s')", + md.getParameterTypes().size(), aspect + .getFullyQualifiedTypeName()); + Validate.notBlank( + md.getBody(), + "Constructor '%s' failed to provide a body, despite being identified for ITD inclusion", + md); + } + + @Override + protected void onAddExtendsTypes(final JavaType type) { + Validate.isTrue( + !governor.getExtendsTypes().contains(type), + "Type '%s' already declared in extends types list in target type '%s' (ITD target '%s')", + type, governor.getName().getFullyQualifiedTypeName(), + aspect.getFullyQualifiedTypeName()); + Validate.isTrue( + !getExtendsTypes().contains(type), + "Type '%s' already declared in extends types list in ITD (ITD target '%s')", + type, aspect.getFullyQualifiedTypeName()); + } + + @Override + protected void onAddField(final FieldMetadataBuilder md) { + Validate.isTrue( + governor.getDeclaredField(md.getFieldName()) == null, + "Field '%s' already defined in target type '%s' (ITD target '%s')", + md.getFieldName(), governor.getName() + .getFullyQualifiedTypeName(), aspect + .getFullyQualifiedTypeName()); + Validate.isTrue(build().getDeclaredField(md.getFieldName()) == null, + "Field '%s' already defined in ITD (ITD target '%s')", + md.getFieldName(), aspect.getFullyQualifiedTypeName()); + } + + @Override + protected void onAddImplementType(final JavaType type) { + Validate.isTrue( + !governor.getImplementsTypes().contains(type), + "Type '%s' already declared in implements types list in target type '%s' (ITD target '%s')", + type, governor.getName().getFullyQualifiedTypeName(), + aspect.getFullyQualifiedTypeName()); + Validate.isTrue( + !getImplementsTypes().contains(type), + "Type '%s' already declared in implements types list in ITD (ITD target '%s')", + type, aspect.getFullyQualifiedTypeName()); + } + + @Override + public void onAddInnerType(final ClassOrInterfaceTypeDetailsBuilder cid) { + if (cid == null) { + return; + } + Validate.isTrue(Modifier.isStatic(cid.getModifier()), + "Currently only static inner types are supported by AspectJ"); + } + + @Override + protected void onAddMethod(final MethodMetadataBuilder md) { + Validate.isTrue( + MemberFindingUtils.getDeclaredMethod(governor, md + .getMethodName(), AnnotatedJavaType + .convertFromAnnotatedJavaTypes(md.getParameterTypes())) == null, + "Method '%s' already defined in target type '%s' (ITD target '%s')", + md.getMethodName(), governor.getName() + .getFullyQualifiedTypeName(), aspect + .getFullyQualifiedTypeName()); + Validate.isTrue( + MemberFindingUtils.getDeclaredMethod(build(), md + .getMethodName(), AnnotatedJavaType + .convertFromAnnotatedJavaTypes(md.getParameterTypes())) == null, + "Method '%s' already defined in ITD (ITD target '%s')", md + .getMethodName(), aspect.getFullyQualifiedTypeName()); + if (!Modifier.isAbstract(md.getModifier())) { + Validate.notBlank( + md.getBody(), + "Method '%s' failed to provide a body, despite being identified for ITD inclusion", + md); + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/MemberFindingUtils.java b/classpath/src/main/java/org/springframework/roo/classpath/details/MemberFindingUtils.java new file mode 100644 index 000000000..6db4040b8 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/MemberFindingUtils.java @@ -0,0 +1,602 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Utility methods for finding members in {@link MemberHoldingTypeDetails} + * instances. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.0 + */ +public final class MemberFindingUtils { + + /** + * Locates the metadata for an annotation of the specified type from within + * the given list. + * + * @param annotations the set of annotations to search (may be + * null) + * @param annotationType the annotation to locate (may be null) + * @return the annotation, or null if not found + */ + public static AnnotationMetadata getAnnotationOfType( + final List annotations, + final JavaType annotationType) { + if (annotations == null) { + return null; + } + for (final AnnotationMetadata annotation : annotations) { + if (annotation.getAnnotationType().equals(annotationType)) { + return annotation; + } + } + return null; + } + + /** + * Returns the metadata for the annotation of the given type from within the + * given metadata + * + * @param metadata the metadata to search; can be null + * @param annotationType the type of annotation for which to return the + * metadata; can be null + * @return null if not found + * @since 1.2.0 + */ + public static AnnotationMetadata getAnnotationOfType( + final MemberHoldingTypeDetailsMetadataItem metadata, + final JavaType annotationType) { + if (metadata == null || metadata.getMemberHoldingTypeDetails() == null) { + return null; + } + return getAnnotationOfType(metadata.getMemberHoldingTypeDetails() + .getAnnotations(), annotationType); + } + + /** + * Searches all {@link MemberDetails} and returns all constructors. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @return zero or more constructors (never null) + * @deprecated use {@link MemberDetails#getConstructors()} instead + */ + @Deprecated + public static List getConstructors( + final MemberDetails memberDetails) { + return memberDetails.getConstructors(); + } + + /** + * Locates the specified constructor. + * + * @param memberHoldingTypeDetails the {@link MemberHoldingTypeDetails} to + * search (required) + * @param parameters to locate (can be null if there are no parameters) + * @return the constructor, or null if not found + * @deprecated use + * {@link MemberHoldingTypeDetails#getDeclaredConstructor(List)} + * instead + */ + @Deprecated + public static ConstructorMetadata getDeclaredConstructor( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final List parameters) { + return memberHoldingTypeDetails.getDeclaredConstructor(parameters); + } + + /** + * Locates the specified field of the given {@link MemberHoldingTypeDetails} + * . + * + * @param memberHoldingTypeDetails the {@link MemberHoldingTypeDetails} to + * search (required) + * @param fieldName to locate (required) + * @return the field, or null if not found + * @deprecated call + * {@link MemberHoldingTypeDetails#getDeclaredField(JavaSymbolName)} + * instead + */ + @Deprecated + public static FieldMetadata getDeclaredField( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaSymbolName fieldName) { + return memberHoldingTypeDetails.getDeclaredField(fieldName); + } + + /** + * Locates an inner type with the specified name. + * + * @param MemberDetails to search (required) + * @param typeName to locate (required) + * @deprecated use + * {@link MemberHoldingTypeDetails#getDeclaredInnerType(JavaType)} + * instead + */ + @Deprecated + public static ClassOrInterfaceTypeDetails getDeclaredInnerType( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaType typeName) { + return memberHoldingTypeDetails.getDeclaredInnerType(typeName); + } + + /** + * Locates a method on the specified {@link MemberHoldingTypeDetails} based + * on the method name. + * + * @param memberHoldingTypeDetails the {@link MemberHoldingTypeDetails} to + * search; can be null + * @param methodName to locate; can be null + * @return the method, or null if the given name was + * null or it was simply not found + */ + public static MethodMetadata getDeclaredMethod( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaSymbolName methodName) { + if (memberHoldingTypeDetails == null) { + return null; + } + for (final MethodMetadata method : memberHoldingTypeDetails + .getDeclaredMethods()) { + if (method.getMethodName().equals(methodName)) { + return method; + } + } + return null; + } + + /** + * Locates the specified method. + * + * @param memberHoldingTypeDetails the {@link MemberHoldingTypeDetails} to + * search; can be null + * @param methodName to locate; can be null + * @param parameters to locate (can be null if there are no parameters) + * @return the method, or null if the given name was + * null or it was simply not found + */ + public static MethodMetadata getDeclaredMethod( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaSymbolName methodName, List parameters) { + if (memberHoldingTypeDetails == null) { + return null; + } + if (parameters == null) { + parameters = new ArrayList(); + } + for (final MethodMetadata method : memberHoldingTypeDetails + .getDeclaredMethods()) { + if (method.getMethodName().equals(methodName)) { + final List parameterTypes = AnnotatedJavaType + .convertFromAnnotatedJavaTypes(method + .getParameterTypes()); + if (parameterTypes.equals(parameters)) { + return method; + } + } + } + return null; + } + + /** + * Locates the specified type-level annotation. + * + * @param builder the {@link MemberHoldingTypeDetails} to search (required) + * @param type to locate (required) + * @return the annotation, or null if not found + * @deprecated use + * {@link AbstractIdentifiableAnnotatedJavaStructureBuilder#getDeclaredTypeAnnotation(JavaType)} + * instead + */ + @Deprecated + public static AnnotationMetadataBuilder getDeclaredTypeAnnotation( + final AbstractIdentifiableAnnotatedJavaStructureBuilder builder, + final JavaType type) { + return builder.getDeclaredTypeAnnotation(type); + } + + /** + * Locates the specified type-level annotation. + * + * @param memberHoldingTypeDetails the {@link MemberHoldingTypeDetails} to + * search (required) + * @param type to locate (required) + * @return the annotation, or null if not found + * @deprecated use + * {@link IdentifiableAnnotatedJavaStructure#getAnnotation(JavaType)} + * instead + */ + @Deprecated + public static AnnotationMetadata getDeclaredTypeAnnotation( + final IdentifiableAnnotatedJavaStructure memberHoldingTypeDetails, + final JavaType type) { + return memberHoldingTypeDetails.getAnnotation(type); + } + + /** + * Locates the specified type-level annotation. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @param type to locate (required) + * @return the annotation, or null if not found + * @deprecated use {@link MemberDetails#getAnnotation(JavaType)} instead + */ + @Deprecated + public static AnnotationMetadata getDeclaredTypeAnnotation( + final MemberDetails memberDetails, final JavaType type) { + return memberDetails.getAnnotation(type); + } + + /** + * Searches up the inheritance hierarchy until the first field with the + * specified name is located. + * + * @param memberHoldingTypeDetails to search (required) + * @param fieldName to locate (required) + * @return the field, or null if not found + * @deprecated use {@link MemberHoldingTypeDetails#getField(JavaSymbolName)} + * instead + */ + @Deprecated + public static FieldMetadata getField( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaSymbolName fieldName) { + return memberHoldingTypeDetails.getField(fieldName); + } + + /** + * Searches all {@link MemberDetails} and returns all fields. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @return zero or more fields (never null) + * @deprecated use {@link MemberDetails#getFields()} instead + */ + @Deprecated + public static List getFields( + final MemberDetails memberDetails) { + return memberDetails.getFields(); + } + + /** + * Searches up the inheritance hierarchy and locates all declared fields + * which are annotated with the specified annotation. + * + * @param memberHoldingTypeDetails to search (required) + * @param annotation to locate (required) + * @return all the located fields (never null, but may be empty) + * @deprecated use + * {@link MemberHoldingTypeDetails#getFieldsWithAnnotation(JavaType)} + * instead + */ + @Deprecated + public static List getFieldsWithAnnotation( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaType annotation) { + return memberHoldingTypeDetails.getFieldsWithAnnotation(annotation); + } + + /** + * Returns all fields within the given {@link MemberDetails} that contain + * the given {@link CustomData} tag. + * + * @param memberDetails the {@link MemberDetails} to search (can be + * null) + * @param tagKey the {@link CustomData} key to search for + * @return zero or more fields (never null) + */ + public static List getFieldsWithTag( + final MemberDetails memberDetails, final Object tagKey) { + Validate.notNull(tagKey, "Custom data key required"); + final List fields = new ArrayList(); + if (memberDetails != null) { + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails + .getDetails()) { + for (final FieldMetadata field : memberHoldingTypeDetails + .getDeclaredFields()) { + if (field.getCustomData().keySet().contains(tagKey)) { + fields.add(field); + } + } + } + } + return fields; + } + + /** + * Returns the first of the given types of annotation on the given class or + * interface + * + * @param cid the class or interface to check (can be null) + * @param annotationTypes the types of annotation to look for, in order (can + * be null) + * @return null if the given type or array of annotations is + * null, or none were found + */ + public static AnnotationMetadata getFirstAnnotation( + final ClassOrInterfaceTypeDetails cid, + final JavaType... annotationTypes) { + if (cid != null && annotationTypes != null) { + for (final JavaType annotationType : annotationTypes) { + final AnnotationMetadata annotation = MemberFindingUtils + .getAnnotationOfType(cid.getAnnotations(), + annotationType); + if (annotation != null) { + return annotation; + } + } + } + return null; + } + + /** + * Searches all {@link MemberDetails} and returns all + * {@link MemberHoldingTypeDetails} which contains a given + * {@link CustomData} tag. + * + * @param memberDetails the {@link MemberDetails} to search (can be + * null) + * @param tagKey the {@link CustomData} key to search for (required) + * @return zero or more {@link MemberHoldingTypeDetails} (never null) + */ + public static List getMemberHoldingTypeDetailsWithTag( + final MemberDetails memberDetails, final Object tagKey) { + Validate.notNull(tagKey, "Custom data tag required"); + final List result = new ArrayList(); + if (memberDetails != null) { + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails + .getDetails()) { + if (memberHoldingTypeDetails.getCustomData().keySet() + .contains(tagKey)) { + result.add(memberHoldingTypeDetails); + } + } + } + return result; + } + + /** + * Locates a method with the name presented. Searches all + * {@link MemberDetails} until the first such method is located or none can + * be found. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @param methodName the method name to locate (can be null) + * @return the first located method, or null if the method name + * is null or such a method cannot be found + * @deprecated use {@link MemberDetails#getMethod(JavaSymbolName)} instead + */ + @Deprecated + public static MethodMetadata getMethod(final MemberDetails memberDetails, + final JavaSymbolName methodName) { + return memberDetails.getMethod(methodName); + } + + /** + * Locates a method with the name and parameter signature presented. + * Searches all {@link MemberDetails} until the first such method is located + * or none can be found. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @param methodName the method name to locate (can be null) + * @param parameters the method parameter signature to locate (can be null + * if no parameters are required) + * @return the first located method, or null if the method name + * is null or such a method cannot be found + * @deprecated use {@link MemberDetails#getMethod(JavaSymbolName, List)} + * instead + */ + @Deprecated + public static MethodMetadata getMethod(final MemberDetails memberDetails, + final JavaSymbolName methodName, final List parameters) { + return memberDetails.getMethod(methodName, parameters); + } + + /** + * Locates a method with the name and parameter signature presented that is + * not declared by the presented MID. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @param methodName the method name to locate (can be null) + * @param parameters the method parameter signature to locate (can be null + * if no parameters are required) + * @param excludingMid the MID that a found method cannot be declared by + * @return the first located method, or null if the method name + * is null or such a method cannot be found + * @deprecated use + * {@link MemberDetails#getMethod(JavaSymbolName, List, String)} + * instead + */ + @Deprecated + public static MethodMetadata getMethod(final MemberDetails memberDetails, + final JavaSymbolName methodName, final List parameters, + final String excludingMid) { + return memberDetails.getMethod(methodName, parameters, excludingMid); + } + + /** + * Convenience method which converts a String method name to a + * {@link JavaSymbolName} for use by the standard + * {@link #getMethod(MemberDetails, org.springframework.roo.model.JavaSymbolName)} + * . + * + * @param memberHoldingTypeDetails to search (required) + * @param methodName to locate (required) + * @return the method, or null if not found + */ + public static MethodMetadata getMethod(final MemberDetails memberDetails, + final String methodName) { + Validate.notNull(methodName, "Method name required"); + return memberDetails.getMethod(new JavaSymbolName(methodName)); + } + + /** + * Searches up the inheritance hierarchy until the first method with the + * specified name is located, method parameters are not taken into account. + * + * @param memberHoldingTypeDetails to search (required) + * @param methodName to locate (required) + * @return the method, or null if not found + * @deprecated use + * {@link MemberHoldingTypeDetails#getMethod(JavaSymbolName)} + * instead + */ + @Deprecated + public static MethodMetadata getMethod( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaSymbolName methodName) { + return memberHoldingTypeDetails.getMethod(methodName); + } + + /** + * Searches up the inheritance hierarchy until the first method with the + * specified name and parameters is located. + * + * @param memberHoldingTypeDetails to search (required) + * @param methodName to locate (required) + * @param parameters to locate (can be null if there are no parameters) + * @return the method, or null if not found + * @deprecated use + * {@link MemberHoldingTypeDetails#getMethod(JavaSymbolName, List)} + * instead + */ + @Deprecated + public static MethodMetadata getMethod( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaSymbolName methodName, final List parameters) { + return memberHoldingTypeDetails.getMethod(methodName, parameters); + } + + /** + * Convenience method which converts a String method name to a + * {@link JavaSymbolName} for use by the standard + * {@link #getMethod(MemberHoldingTypeDetails, org.springframework.roo.model.JavaSymbolName)} + * . + * + * @param memberHoldingTypeDetails to search (required) + * @param methodName to locate (required) + * @return the method, or null if not found + */ + public static MethodMetadata getMethod( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final String methodName) { + return memberHoldingTypeDetails + .getMethod(new JavaSymbolName(methodName)); + } + + /** + * Searches all {@link MemberDetails} and returns all methods. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @return zero or more methods (never null) + * @deprecated use {@link MemberDetails#getMethods()} instead + */ + @Deprecated + public static List getMethods( + final MemberDetails memberDetails) { + return memberDetails.getMethods(); + } + + /** + * Locates all methods on this class and its superclasses. + * + * @param memberHoldingTypeDetails to search (required) + * @return zero or more methods (never null) + * @deprecated use {@link MemberHoldingTypeDetails#getMethods()} instead + */ + @Deprecated + public static List getMethods( + final MemberHoldingTypeDetails memberHoldingTypeDetails) { + return memberHoldingTypeDetails.getMethods(); + } + + /** + * Searches all {@link MemberDetails} and returns all methods which contain + * a given {@link CustomData} tag. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @param tagKey the {@link CustomData} key to search for + * @return zero or more methods (never null) + * @deprecated use {@link MemberDetails#getMethodsWithTag(Object)} instead + */ + @Deprecated + public static List getMethodsWithTag( + final MemberDetails memberDetails, final Object tagKey) { + return memberDetails.getMethodsWithTag(tagKey); + } + + /** + * Determines the most concrete {@link MemberHoldingTypeDetails} in cases + * where multiple matches are found for a given tag. + * + * @param memberDetails the {@link MemberDetails} to search (can be + * null) + * @param tag the {@link CustomData} key to search for (required) + * @return the most concrete tagged type or null if not found + */ + public static MemberHoldingTypeDetails getMostConcreteMemberHoldingTypeDetailsWithTag( + final MemberDetails memberDetails, final Object tag) { + Validate.notNull(tag, "Custom data tag required"); + final List memberHoldingTypeDetailsList = getMemberHoldingTypeDetailsWithTag( + memberDetails, tag); + if (memberHoldingTypeDetailsList.isEmpty()) { + return null; + } + return memberHoldingTypeDetailsList.get(memberHoldingTypeDetailsList + .size() - 1); + } + + /** + * Determines the most concrete {@link MemberHoldingTypeDetails} in cases + * where multiple matches are found for a given tag. + * + * @param memberDetails the {@link MemberDetails} to search (can be + * null) + * @param tagKey the {@link CustomData} key to search for (required) + * @return the most concrete tagged method or null if not found + */ + public static MethodMetadata getMostConcreteMethodWithTag( + final MemberDetails memberDetails, final Object tagKey) { + if (memberDetails == null) { + return null; + } + return memberDetails.getMostConcreteMethodWithTag(tagKey); + } + + /** + * Locates an annotation on this class and its superclasses. + * + * @param memberHoldingTypeDetails to search (required) + * @param annotationType annotation to locate (required) + * @return the annotation, or null if not found + * @deprecated use + * {@link IdentifiableAnnotatedJavaStructure#getTypeAnnotation(JavaType)} + * instead + */ + @Deprecated + public static AnnotationMetadata getTypeAnnotation( + final IdentifiableAnnotatedJavaStructure memberHoldingTypeDetails, + final JavaType annotationType) { + return memberHoldingTypeDetails.getTypeAnnotation(annotationType); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private MemberFindingUtils() { + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/MemberHoldingTypeDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/details/MemberHoldingTypeDetails.java new file mode 100644 index 000000000..0b8065706 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/MemberHoldingTypeDetails.java @@ -0,0 +1,191 @@ +package org.springframework.roo.classpath.details; + +import java.util.List; + +import org.springframework.roo.classpath.PhysicalTypeDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Immutable representation of the members of a class, interface, enum or + * aspect. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MemberHoldingTypeDetails extends PhysicalTypeDetails, + IdentifiableAnnotatedJavaStructure { + + /** + * Indicates whether this type extends the given type. Equivalent to calling + * {@link #getExtendsTypes()} and checking whether the given type is in the + * returned list. + * + * @param type the supertype being checked for (required) + * @return see above + */ + boolean extendsType(JavaType type); + + /** + * Locates the constructor with the specified parameter types. + * + * @param parameters to locate (can be null if there are no parameters) + * @return the constructor, or null if not found + * @since 1.2.0 + */ + ConstructorMetadata getDeclaredConstructor(List parameters); + + List getDeclaredConstructors(); + + /** + * Locates the specified field. + * + * @param fieldName to locate (can be null) + * @return the field, or null if not found + * @since 1.2.0 + */ + FieldMetadata getDeclaredField(JavaSymbolName fieldName); + + List getDeclaredFields(); + + List getDeclaredInitializers(); + + /** + * Locates an inner type with the specified name. + * + * @param typeName to locate (required) + * @since 1.2.0 + */ + ClassOrInterfaceTypeDetails getDeclaredInnerType(JavaType typeName); + + List getDeclaredInnerTypes(); + + List getDeclaredMethods(); + + /** + * Returns the names of any dynamic finders + * + * @return a non-null list + * @since 1.2.0 + */ + List getDynamicFinderNames(); + + /** + * Lists the classes this type extends. This may be empty. Always empty in + * the case of an enum. + *

    + * While a {@link List} is used, normally in Java a class will only extend a + * single other class. A {@link List} is used to support interfaces, as well + * as support the special + * "declare parents: DestinationType extends SuperclassType" feature of ITDs + * which permits effectively multiple inheritance. + * + * @return an unmodifiable representation of classes this type extends (may + * be empty, but never null) + */ + List getExtendsTypes(); + + /** + * Searches up the inheritance hierarchy until the first field with the + * specified name is located. + * + * @param fieldName to locate (required) + * @return the field, or null if not found + * @since 1.2.0 + */ + FieldMetadata getField(JavaSymbolName fieldName); + + /** + * Searches up the inheritance hierarchy and locates all declared fields + * which are annotated with the specified annotation. + * + * @param annotation to locate (required) + * @return all the located fields (never null, but may be empty) + * @since 1.2.0 + */ + List getFieldsWithAnnotation(JavaType annotation); + + /** + * Lists the classes this type implements. Always empty in the case of an + * interface. + *

    + * A {@link List} is used to support interfaces, as well as support the + * special "declare parents: DestinationType implements SomeInterfaceType" + * feature of ITDs. + * + * @return an unmodifiable representation of classes this type implements + * (may be empty, but never null) + */ + List getImplementsTypes(); + + /** + * If this is a layering component, for example a service or repository, + * returns the domain entities managed by this component, otherwise returns + * an empty list. + * + * @return a non-null list (may be empty) + * @since 1.2.0 + */ + List getLayerEntities(); + + /** + * Searches up the inheritance hierarchy until the first method with the + * specified name is located; method parameters are not taken into account. + * + * @param methodName to locate (required) + * @return the method, or null if not found + * @since 1.2.0 + */ + MethodMetadata getMethod(JavaSymbolName methodName); + + /** + * Searches up the inheritance hierarchy until the first method with the + * specified name and parameters is located. + * + * @param methodName to locate (required) + * @param parameters to locate (can be null if there are no parameters) + * @return the method, or null if not found + * @since 1.2.0 + */ + MethodMetadata getMethod(JavaSymbolName methodName, + List parameters); + + /** + * Locates all methods on this class and its superclasses. + * + * @return zero or more methods (never null) + */ + List getMethods(); + + /** + * Generates a unique name for a field, starting from the given proposed + * name and adding underscores until it's unique. + * + * @param proposedName the proposed field name (required) + * @return a non-null name that's unique within the governor + * @see MemberFindingUtils#getField(org.springframework.roo.classpath.details.MemberHoldingTypeDetails, + * JavaSymbolName) + * @since 1.2.0 + */ + JavaSymbolName getUniqueFieldName(final String proposedName); + + /** + * Indicates whether this type implements the given types. Equivalent to + * calling {@link #getImplementsTypes()} and checking whether the given + * types are in the returned list. + * + * @param type the interfaces being checked for (required) + * @return see above + */ + boolean implementsAny(JavaType... type); + + /** + * Indicates whether this type implements the given interface. Equivalent to + * calling {@link #getImplementsTypes()} and checking whether the given type + * is in the returned list. + * + * @param type the interface being checked for (required) + * @return see above + */ + boolean implementsType(JavaType interfaceType); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/MethodMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/MethodMetadata.java new file mode 100644 index 000000000..f860e3cf9 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/MethodMetadata.java @@ -0,0 +1,42 @@ +package org.springframework.roo.classpath.details; + +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Metadata concerning a particular method. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MethodMetadata extends InvocableMemberMetadata { + + /** + * @return the name of the method (never null) + */ + JavaSymbolName getMethodName(); + + /** + * @return the return type (never null, even if void) + */ + JavaType getReturnType(); + + /** + * Indicates whether this method has the same name (case-sensitive) as any + * of the given methods + * + * @param otherMethods the methods to check against; can be empty or contain + * null elements, which will be ignored + * @return see above + * @since 1.2.0 + */ + boolean hasSameName(final MethodMetadata... otherMethods); + + /** + * Indicates whether this method is static + * + * @return see above + * @since 1.2.0 + */ + boolean isStatic(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/MethodMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/MethodMetadataBuilder.java new file mode 100644 index 000000000..0f5417d1e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/MethodMetadataBuilder.java @@ -0,0 +1,133 @@ +package org.springframework.roo.classpath.details; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Builder for {@link MethodMetadata}. + * + * @author Ben Alex + * @since 1.1 + */ +public final class MethodMetadataBuilder extends + AbstractInvocableMemberMetadataBuilder { + + private JavaSymbolName methodName; + private JavaType returnType; + private CommentStructure commentStructure; + + public MethodMetadataBuilder(final MethodMetadata existing) { + super(existing); + init(existing.getMethodName(), existing.getReturnType()); + } + + public MethodMetadataBuilder(final String declaredbyMetadataId) { + super(declaredbyMetadataId); + } + + /** + * Constructor for a method with no parameters + * + * @param declaredbyMetadataId + * @param modifier + * @param methodName + * @param returnType + * @param bodyBuilder + */ + public MethodMetadataBuilder(final String declaredbyMetadataId, + final int modifier, final JavaSymbolName methodName, + final JavaType returnType, + final InvocableMemberBodyBuilder bodyBuilder) { + this(declaredbyMetadataId, modifier, methodName, returnType, + new ArrayList(), + new ArrayList(), bodyBuilder); + } + + /** + * Constructor for a method with parameters + * + * @param declaredbyMetadataId + * @param modifier + * @param methodName + * @param returnType + * @param parameterTypes + * @param parameterNames + * @param bodyBuilder + */ + public MethodMetadataBuilder(final String declaredbyMetadataId, + final int modifier, final JavaSymbolName methodName, + final JavaType returnType, + final List parameterTypes, + final List parameterNames, + final InvocableMemberBodyBuilder bodyBuilder) { + this(declaredbyMetadataId); + setModifier(modifier); + setParameterTypes(parameterTypes); + setParameterNames(parameterNames); + init(methodName, returnType); + setBodyBuilder(bodyBuilder); + } + + public MethodMetadataBuilder(final String declaredbyMetadataId, + final MethodMetadata existing) { + super(declaredbyMetadataId, existing); + init(existing.getMethodName(), existing.getReturnType()); + } + + public MethodMetadata build() { + DefaultMethodMetadata methodMetadata = new DefaultMethodMetadata( + getCustomData().build(), getDeclaredByMetadataId(), + getModifier(), buildAnnotations(), getMethodName(), + getReturnType(), getParameterTypes(), getParameterNames(), + getThrowsTypes(), getBodyBuilder().getOutput()); + + methodMetadata.setCommentStructure(this.commentStructure); + + return methodMetadata; + } + + public JavaSymbolName getMethodName() { + return methodName; + } + + public JavaType getReturnType() { + return returnType; + } + + private void init(final JavaSymbolName methodName, final JavaType returnType) { + this.methodName = methodName; + this.returnType = returnType; + } + + public void setMethodName(final JavaSymbolName methodName) { + this.methodName = methodName; + } + + public void setReturnType(final JavaType returnType) { + this.returnType = returnType; + } + + public CommentStructure getCommentStructure() { + return commentStructure; + } + + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + // Append the parts of the method that make up the Java + // signature + .append("methodName", methodName) + .append("parameterTypes", getParameterTypes()).toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AbstractAnnotationAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AbstractAnnotationAttributeValue.java new file mode 100644 index 000000000..a751f657d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AbstractAnnotationAttributeValue.java @@ -0,0 +1,71 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Abstract base class for annotation attribute values. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractAnnotationAttributeValue + implements AnnotationAttributeValue { + + private final JavaSymbolName name; + + /** + * Constructor + * + * @param name the attribute name (required) + */ + protected AbstractAnnotationAttributeValue(final JavaSymbolName name) { + Validate.notNull(name, "Annotation attribute name required"); + this.name = name; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof AbstractAnnotationAttributeValue)) { + return false; + } + final AbstractAnnotationAttributeValue other = (AbstractAnnotationAttributeValue) obj; + if (getValue() == null) { + if (other.getValue() != null) { + return false; + } + } + else if (!getValue().equals(other.getValue())) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + public JavaSymbolName getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (getValue() == null ? 0 : getValue().hashCode()); + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotatedJavaType.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotatedJavaType.java new file mode 100644 index 000000000..b3906b261 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotatedJavaType.java @@ -0,0 +1,169 @@ +package org.springframework.roo.classpath.details.annotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.AnnotationMetadataUtils; +import org.springframework.roo.model.JavaType; + +/** + * Represents a {@link JavaType} with zero or more annotations. + * + * @author Ben Alex + * @since 1.0 + */ +public class AnnotatedJavaType { + + /** + * Converts a non-null {@link AnnotatedJavaType} into an equivalent + * {@link JavaType}. Note the annotation metadata will be discarded, as it + * cannot be stored inside a {@link JavaType}. + * + * @param annotatedJavaType to convert (required) + * @return the equivalent {@link AnnotatedJavaType}, but without any actual + * annotations (never returns null) + */ + public static JavaType convertFromAnnotatedJavaType( + final AnnotatedJavaType annotatedJavaType) { + Validate.notNull(annotatedJavaType, "Annotated Java types required"); + return annotatedJavaType.getJavaType(); + } + + /** + * Converts a non-null {@link List} of {@link AnnotatedJavaType}s into a + * {@link List} of equivalent {@link JavaType}s. Note the annotation + * metadata will be discarded, as it cannot be stored inside a + * {@link JavaType}. + * + * @param annotatedJavaTypes to convert (required) + * @return the equivalent {@link AnnotatedJavaType}s, but without any actual + * annotations (never returns null) + */ + public static List convertFromAnnotatedJavaTypes( + final List annotatedJavaTypes) { + Validate.notNull(annotatedJavaTypes, "Annotated Java types required"); + final List result = new ArrayList(); + for (final AnnotatedJavaType annotatedJavaType : annotatedJavaTypes) { + result.add(convertFromAnnotatedJavaType(annotatedJavaType)); + } + return result; + } + + /** + * Converts a {@link JavaType} into an equivalent {@link AnnotatedJavaType}. + * Note that each returned {@link AnnotatedJavaType}will have no annotation + * metadata, as the input {@link JavaType} cannot store any such metadata. + * + * @param javaType to convert (required) + * @return the equivalent {@link AnnotatedJavaType} (never returns null) + */ + public static AnnotatedJavaType convertFromJavaType(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + return new AnnotatedJavaType(javaType); + } + + /** + * Converts a non-null bag of {@link JavaType}s into a {@link List} of + * equivalent {@link AnnotatedJavaType}s. Note that each returned + * {@link AnnotatedJavaType} will have no annotation metadata, as the input + * {@link JavaType}s cannot store any such metadata. + * + * @param javaTypes to convert (can be null for none) + * @return the equivalent {@link AnnotatedJavaType}s (never returns null) + */ + public static List convertFromJavaTypes( + final Iterable javaTypes) { + final List result = new ArrayList(); + if (javaTypes != null) { + for (final JavaType javaType : javaTypes) { + result.add(convertFromJavaType(javaType)); + } + } + return result; + } + + /** + * Converts a non-null bag of {@link JavaType}s into a {@link List} of + * equivalent {@link AnnotatedJavaType}s. Note that each returned + * {@link AnnotatedJavaType} will have no annotation metadata, as the input + * {@link JavaType}s cannot store any such metadata. + * + * @param javaTypes to convert + * @return the equivalent {@link AnnotatedJavaType}s (never returns null) + * @since 1.2.0 + */ + public static List convertFromJavaTypes( + final JavaType... javaTypes) { + return convertFromJavaTypes(Arrays.asList(javaTypes)); + } + + private final List annotations = new ArrayList(); + private boolean isVarArgs; + private final JavaType javaType; + + /** + * Constructor that accepts a vararg array of annotations + * + * @param javaType the type (required) + * @param annotations can be none + * @since 1.2.0 + */ + public AnnotatedJavaType(final JavaType javaType, + final AnnotationMetadata... annotations) { + this(javaType, Arrays.asList(annotations)); + } + + /** + * Constructor that accepts an optional list of annotations + * + * @param javaType the type (required) + * @param annotations any annotations for the type (defensively copied, + * null is acceptable) + */ + public AnnotatedJavaType(final JavaType javaType, + final Collection annotations) { + Validate.notNull(javaType, "Java type required"); + this.javaType = javaType; + if (annotations != null) { + this.annotations.addAll(annotations); + } + } + + /** + * Returns the annotations on this type + * + * @return a copy of this list (never null, but may be empty) + */ + public List getAnnotations() { + return new ArrayList(annotations); + } + + /** + * @return the type (never returns null) + */ + public JavaType getJavaType() { + return javaType; + } + + public boolean isVarArgs() { + return isVarArgs; + } + + public void setVarArgs(final boolean varArgs) { + isVarArgs = varArgs; + } + + @Override + public final String toString() { + final StringBuilder sb = new StringBuilder(); + for (final AnnotationMetadata annotation : annotations) { + sb.append(AnnotationMetadataUtils.toSourceForm(annotation)); + sb.append(" "); + } + sb.append(javaType.getNameIncludingTypeParameters()); + return sb.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationAttributeValue.java new file mode 100644 index 000000000..e94b594aa --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationAttributeValue.java @@ -0,0 +1,27 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represent an annotation attribute value. + *

    + * Implementations must correctly meet the contractual requirements of + * {@link #equals(Object)} and {@link #hashCode()}. + * + * @author Ben Alex + * @since 1.0 + * @param the type of value this attribute contains + */ +public interface AnnotationAttributeValue { + + /** + * @return the name of the attribute (never null; often the name will be + * "value") + */ + JavaSymbolName getName(); + + /** + * @return the value (never null) + */ + T getValue(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadata.java new file mode 100644 index 000000000..a819f425f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadata.java @@ -0,0 +1,45 @@ +package org.springframework.roo.classpath.details.annotations; + +import java.util.List; + +import org.springframework.roo.classpath.details.comments.CommentedJavaStructure; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Metadata concerning a particular annotation appearing on a member. + * + * @author Ben Alex + * @author Andrew Swan + * @since 1.0 + */ +public interface AnnotationMetadata extends CommentedJavaStructure { + + /** + * @return the annotation type (never null) + */ + JavaType getAnnotationType(); + + /** + * Acquires an attribute value for the requested name. + * + * @param attributeName + * @return the requested attribute (or null if not found) + */ + AnnotationAttributeValue getAttribute(JavaSymbolName attributeName); + + /** + * Returns the value of the given attribute + * + * @param attributeName + * @return the requested attribute (or null if not found) + * @since 1.2.0 + */ + AnnotationAttributeValue getAttribute(String attributeName); + + /** + * @return the attribute names, preferably in the order they are declared in + * the annotation (never null, but may be empty) + */ + List getAttributeNames(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadataBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadataBuilder.java new file mode 100644 index 000000000..c75691ee4 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadataBuilder.java @@ -0,0 +1,279 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.springframework.roo.model.JpaJavaType.COLUMN; +import static org.springframework.roo.model.JpaJavaType.EMBEDDED; +import static org.springframework.roo.model.JpaJavaType.EMBEDDED_ID; +import static org.springframework.roo.model.JpaJavaType.ENUMERATED; +import static org.springframework.roo.model.JpaJavaType.ID; +import static org.springframework.roo.model.JpaJavaType.LOB; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.TRANSIENT; +import static org.springframework.roo.model.JpaJavaType.VERSION; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.model.Builder; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Builder for {@link AnnotationMetadata}. + *

    + * The "add" method will replace any existing annotation attribute with the same + * name, taking care to preserve its location. + * + * @author Ben Alex + * @author Andrew Swan + * @since 1.1 + */ +public class AnnotationMetadataBuilder implements Builder { + + public static final AnnotationMetadata JPA_COLUMN_ANNOTATION = getInstance(COLUMN); + public static final AnnotationMetadata JPA_EMBEDDED_ANNOTATION = getInstance(EMBEDDED); + public static final AnnotationMetadata JPA_EMBEDDED_ID_ANNOTATION = getInstance(EMBEDDED_ID); + public static final AnnotationMetadata JPA_ENUMERATED_ANNOTATION = getInstance(ENUMERATED); + public static final AnnotationMetadata JPA_ID_ANNOTATION = getInstance(ID); + public static final AnnotationMetadata JPA_LOB_ANNOTATION = getInstance(LOB); + public static final AnnotationMetadata JPA_MANY_TO_MANY_ANNOTATION = getInstance(MANY_TO_MANY); + public static final AnnotationMetadata JPA_MANY_TO_ONE_ANNOTATION = getInstance(MANY_TO_ONE); + public static final AnnotationMetadata JPA_ONE_TO_MANY_ANNOTATION = getInstance(ONE_TO_MANY); + public static final AnnotationMetadata JPA_ONE_TO_ONE_ANNOTATION = getInstance(ONE_TO_ONE); + public static final AnnotationMetadata JPA_TRANSIENT_ANNOTATION = getInstance(TRANSIENT); + public static final AnnotationMetadata JPA_VERSION_ANNOTATION = getInstance(VERSION); + + /** + * Returns the metadata for the existing annotation, with no attribute + * values + * + * @param annotationType the annotation type (required) + * @return a non-null instance + * @since 1.2.0 + */ + public static AnnotationMetadata getInstance(final Class annotationType) { + return new AnnotationMetadataBuilder(annotationType).build(); + } + + public static AnnotationMetadata getInstance(final JavaType annotationType) { + return new AnnotationMetadataBuilder(annotationType).build(); + } + + public static AnnotationMetadataBuilder getInstance( + final JavaType annotationType, + final Collection> attributeValues) { + return new AnnotationMetadataBuilder(annotationType, attributeValues); + } + + /** + * Returns the metadata for the existing annotation, with no attribute + * values + * + * @param annotationType the fully-qualified name of the annotation type + * (required) + * @return a non-null instance + * @since 1.2.0 + */ + public static AnnotationMetadata getInstance(final String annotationType) { + return new AnnotationMetadataBuilder(annotationType).build(); + } + + private JavaType annotationType; + private final Map> attributeValues = new LinkedHashMap>(); + private CommentStructure commentStructure; + + /** + * Constructor. The caller must set the annotation type via + * {@link #setAnnotationType(JavaType)} before calling {@link #build()} + */ + public AnnotationMetadataBuilder() { + } + + /** + * Constructor for using an existing {@link AnnotationMetadata} as a + * baseline for building a new instance. + * + * @param existing required + */ + public AnnotationMetadataBuilder(final AnnotationMetadata existing) { + Validate.notNull(existing); + annotationType = existing.getAnnotationType(); + for (final JavaSymbolName attributeName : existing.getAttributeNames()) { + attributeValues.put(attributeName.getSymbolName(), + existing.getAttribute(attributeName)); + } + this.setCommentStructure(existing.getCommentStructure()); + } + + /** + * Constructor for no initial attribute values + * + * @param annotationType the annotation class (required) + * @since 1.2.0 + */ + public AnnotationMetadataBuilder(final Class annotationType) { + this(new JavaType(annotationType)); + } + + /** + * Constructor for no initial attribute values + * + * @param annotationType + */ + public AnnotationMetadataBuilder(final JavaType annotationType) { + this.annotationType = annotationType; + } + + /** + * Constructor that accepts an optional list of values + * + * @param annotationType + * @param attributeValues can be null + */ + public AnnotationMetadataBuilder(final JavaType annotationType, + final Collection> attributeValues) { + this.annotationType = annotationType; + setAttributes(attributeValues); + } + + /** + * Constructor for no initial attribute values + * + * @param annotationType the fully-qualified name of the annotation type + * (required) + */ + public AnnotationMetadataBuilder(final String annotationType) { + this(new JavaType(annotationType)); + } + + public void addAttribute(final AnnotationAttributeValue value) { + // Locate existing attribute with this key and replace it + attributeValues.put(value.getName().getSymbolName(), value); + } + + public void addBooleanAttribute(final String key, final boolean value) { + addAttribute(new BooleanAttributeValue(new JavaSymbolName(key), value)); + } + + public void addCharAttribute(final String key, final char value) { + addAttribute(new CharAttributeValue(new JavaSymbolName(key), value)); + } + + /** + * Adds an attribute with the given {@link JavaType} as its value + * + * @param key the attribute name (required) + * @param javaType the value (required) + */ + public void addClassAttribute(final String key, final JavaType javaType) { + addAttribute(new ClassAttributeValue(new JavaSymbolName(key), javaType)); + } + + public void addClassAttribute(final String key, + final String fullyQualifiedTypeName) { + addAttribute(new ClassAttributeValue(new JavaSymbolName(key), + new JavaType(fullyQualifiedTypeName))); + } + + public void addDoubleAttribute(final String key, final double value, + final boolean floatingPrecisionOnly) { + addAttribute(new DoubleAttributeValue(new JavaSymbolName(key), value, + floatingPrecisionOnly)); + } + + public void addEnumAttribute(final String key, final EnumDetails details) { + addAttribute(new EnumAttributeValue(new JavaSymbolName(key), details)); + } + + public void addEnumAttribute(final String key, final JavaType javaType, + final JavaSymbolName enumConstant) { + final EnumDetails details = new EnumDetails(javaType, enumConstant); + addAttribute(new EnumAttributeValue(new JavaSymbolName(key), details)); + } + + public void addEnumAttribute(final String key, final JavaType javaType, + final String enumConstant) { + final EnumDetails details = new EnumDetails(javaType, + new JavaSymbolName(enumConstant)); + addAttribute(new EnumAttributeValue(new JavaSymbolName(key), details)); + } + + public void addEnumAttribute(final String key, + final String fullyQualifiedTypeName, final String enumConstant) { + final EnumDetails details = new EnumDetails(new JavaType( + fullyQualifiedTypeName), new JavaSymbolName(enumConstant)); + addAttribute(new EnumAttributeValue(new JavaSymbolName(key), details)); + } + + public void addIntegerAttribute(final String key, final int value) { + addAttribute(new IntegerAttributeValue(new JavaSymbolName(key), value)); + } + + public void addLongAttribute(final String key, final long value) { + addAttribute(new LongAttributeValue(new JavaSymbolName(key), value)); + } + + public void addStringAttribute(final String key, final String value) { + addAttribute(new StringAttributeValue(new JavaSymbolName(key), value)); + } + + public AnnotationMetadata build() { + + DefaultAnnotationMetadata annotationMetadata = new DefaultAnnotationMetadata( + getAnnotationType(), + new ArrayList>(getAttributes() + .values())); + + annotationMetadata.setCommentStructure(commentStructure); + + return annotationMetadata; + } + + public JavaType getAnnotationType() { + return annotationType; + } + + public Map> getAttributes() { + return attributeValues; + } + + public void removeAttribute(final String key) { + // Locate existing attribute with this key and replace it + attributeValues.remove(key); + } + + public void setAnnotationType(final JavaType annotationType) { + this.annotationType = annotationType; + } + + /** + * Sets the attribute values + * + * @param attributeValues the values to set; can be null for + * none + */ + public void setAttributes( + final Collection> attributeValues) { + this.attributeValues.clear(); + if (attributeValues != null) { + for (final AnnotationAttributeValue attributeValue : attributeValues) { + addAttribute(attributeValue); + } + } + } + + public CommentStructure getCommentStructure() { + return commentStructure; + } + + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/ArrayAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/ArrayAttributeValue.java new file mode 100644 index 000000000..151807c18 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/ArrayAttributeValue.java @@ -0,0 +1,45 @@ +package org.springframework.roo.classpath.details.annotations; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents an array of annotation attribute values. + * + * @author Ben Alex + * @since 1.0 + * @param the type of each {@link AnnotationAttributeValue} + */ +public class ArrayAttributeValue> extends + AbstractAnnotationAttributeValue> { + + private final List value; + + /** + * Constructor + * + * @param name the attribute name (required) + * @param value the attribute values (required) + */ + public ArrayAttributeValue(final JavaSymbolName name, final List value) { + super(name); + Validate.notNull(value, "Value required"); + this.value = value; + } + + /** + * Returns an unmodifiable copy of the array values + */ + public List getValue() { + return Collections.unmodifiableList(value); + } + + @Override + public String toString() { + return getName() + " -> {" + StringUtils.join(value, ",") + "}"; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/BooleanAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/BooleanAttributeValue.java new file mode 100644 index 000000000..de5313126 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/BooleanAttributeValue.java @@ -0,0 +1,35 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents a boolean annotation attribute value. + * + * @author Ben Alex + * @since 1.0 + */ +public class BooleanAttributeValue extends + AbstractAnnotationAttributeValue { + + private final boolean value; + + /** + * Constructor + * + * @param name + * @param value + */ + public BooleanAttributeValue(final JavaSymbolName name, final boolean value) { + super(name); + this.value = value; + } + + public Boolean getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/CharAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/CharAttributeValue.java new file mode 100644 index 000000000..417abaf05 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/CharAttributeValue.java @@ -0,0 +1,35 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents a char annotation attribute value. + * + * @author Ben Alex + * @since 1.0 + */ +public class CharAttributeValue extends + AbstractAnnotationAttributeValue { + + private final char value; + + /** + * Constructor + * + * @param name + * @param value + */ + public CharAttributeValue(final JavaSymbolName name, final char value) { + super(name); + this.value = value; + } + + public Character getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value; + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/ClassAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/ClassAttributeValue.java new file mode 100644 index 000000000..84052bdb1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/ClassAttributeValue.java @@ -0,0 +1,42 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Represents a {@link Class} annotation attribute value. + *

    + * Source code parsers should treat any non-quoted string ending in ".class" as + * a class name, and then use normal package resolution techniques to determine + * the fully-qualified class. + * + * @author Ben Alex + * @since 1.0 + */ +public class ClassAttributeValue extends + AbstractAnnotationAttributeValue { + + private final JavaType value; + + /** + * Constructor + * + * @param name the attribute name (required) + * @param value the value (required) + */ + public ClassAttributeValue(final JavaSymbolName name, final JavaType value) { + super(name); + Validate.notNull(value, "Value required"); + this.value = value; + } + + public JavaType getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value.getNameIncludingTypeParameters(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/DefaultAnnotationMetadata.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/DefaultAnnotationMetadata.java new file mode 100644 index 000000000..27c019de8 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/DefaultAnnotationMetadata.java @@ -0,0 +1,87 @@ +package org.springframework.roo.classpath.details.annotations; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.comments.CommentStructure; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Default implementation of {@link AnnotationMetadata}. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultAnnotationMetadata implements AnnotationMetadata { + + private final JavaType annotationType; + private final Map> attributeMap; + private final List> attributes; + private CommentStructure commentStructure; + + /** + * Constructor + * + * @param annotationType the type of annotation for which these are the + * metadata (required) + * @param attributeValues the given annotation's values; can be + * null + */ + DefaultAnnotationMetadata(final JavaType annotationType, + final List> attributeValues) { + Validate.notNull(annotationType, "Annotation type required"); + this.annotationType = annotationType; + attributes = new ArrayList>(); + attributeMap = new HashMap>(); + if (attributeValues != null) { + attributes.addAll(attributeValues); + for (final AnnotationAttributeValue value : attributeValues) { + attributeMap.put(value.getName(), value); + } + } + } + + public JavaType getAnnotationType() { + return annotationType; + } + + public AnnotationAttributeValue getAttribute( + final JavaSymbolName attributeName) { + Validate.notNull(attributeName, "Attribute name required"); + return attributeMap.get(attributeName); + } + + @SuppressWarnings("unchecked") + public AnnotationAttributeValue getAttribute(final String attributeName) { + return getAttribute(new JavaSymbolName(attributeName)); + } + + public List getAttributeNames() { + final List result = new ArrayList(); + for (final AnnotationAttributeValue value : attributes) { + result.add(value.getName()); + } + return result; + } + + public CommentStructure getCommentStructure() { + return commentStructure; + } + + public void setCommentStructure(CommentStructure commentStructure) { + this.commentStructure = commentStructure; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("annotationType", annotationType); + builder.append("attributes", attributes); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/DoubleAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/DoubleAttributeValue.java new file mode 100644 index 000000000..da30f2788 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/DoubleAttributeValue.java @@ -0,0 +1,36 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents a double annotation attribute value. + * + * @author Ben Alex + * @since 1.0 + */ +public class DoubleAttributeValue extends + AbstractAnnotationAttributeValue { + + private boolean floatingPrecisionOnly = false; + private final double value; + + public DoubleAttributeValue(final JavaSymbolName name, final double value, + final boolean floatingPrecisionOnly) { + super(name); + this.value = value; + this.floatingPrecisionOnly = floatingPrecisionOnly; + } + + public Double getValue() { + return value; + } + + public boolean isFloatingPrecisionOnly() { + return floatingPrecisionOnly; + } + + @Override + public String toString() { + return getName() + " -> " + new Double(value).toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/EnumAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/EnumAttributeValue.java new file mode 100644 index 000000000..a89fff38f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/EnumAttributeValue.java @@ -0,0 +1,48 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents an enumeration annotation attribute value. + *

    + * Source code parsers should treat any non-quoted string NOT ending in ".class" + * as an enumeration, using the segment appearing after the final period in the + * string as the field name. Anything to the left of that final period is + * treated as representing the enumeration type, and normal package resolution + * techniques should be used to resolve the enumeration type. + * + * @author Ben Alex + * @since 1.0 + */ +public class EnumAttributeValue extends + AbstractAnnotationAttributeValue { + private final EnumDetails value; + + public EnumAttributeValue(final JavaSymbolName name, final EnumDetails value) { + super(name); + Validate.notNull(value, "Value required"); + this.value = value; + } + + @SuppressWarnings("all") + public Enum getAsEnum() throws ClassNotFoundException { + final Class enumType = getClass().getClassLoader().loadClass( + value.getType().getFullyQualifiedTypeName()); + Validate.isTrue(enumType.isEnum(), + "Should have obtained an Enum but failed for type '%s'", + enumType.getName()); + final String name = value.getField().getSymbolName(); + return Enum.valueOf((Class) enumType, name); + } + + public EnumDetails getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/IntegerAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/IntegerAttributeValue.java new file mode 100644 index 000000000..d0b652ae5 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/IntegerAttributeValue.java @@ -0,0 +1,35 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents an integer annotation attribute value. + * + * @author Ben Alex + * @since 1.0 + */ +public class IntegerAttributeValue extends + AbstractAnnotationAttributeValue { + + private final int value; + + /** + * Constructor + * + * @param name + * @param value + */ + public IntegerAttributeValue(final JavaSymbolName name, final int value) { + super(name); + this.value = value; + } + + public Integer getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/LongAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/LongAttributeValue.java new file mode 100644 index 000000000..1953d0c73 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/LongAttributeValue.java @@ -0,0 +1,34 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents a long annotation attribute value. + * + * @author Ben Alex + * @since 1.0 + */ +public class LongAttributeValue extends AbstractAnnotationAttributeValue { + + private final long value; + + /** + * Constructor + * + * @param name + * @param value + */ + public LongAttributeValue(final JavaSymbolName name, final long value) { + super(name); + this.value = value; + } + + public Long getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/NestedAnnotationAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/NestedAnnotationAttributeValue.java new file mode 100644 index 000000000..1d6705d07 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/NestedAnnotationAttributeValue.java @@ -0,0 +1,31 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents a nested annotation attribute value. + * + * @author Ben Alex + * @since 1.0 + */ +public class NestedAnnotationAttributeValue extends + AbstractAnnotationAttributeValue { + private final AnnotationMetadata value; + + public NestedAnnotationAttributeValue(final JavaSymbolName name, + final AnnotationMetadata value) { + super(name); + Validate.notNull(value, "Value required"); + this.value = value; + } + + public AnnotationMetadata getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/StringAttributeValue.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/StringAttributeValue.java new file mode 100644 index 000000000..faa32ad35 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/StringAttributeValue.java @@ -0,0 +1,33 @@ +package org.springframework.roo.classpath.details.annotations; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Represents a {@link String} annotation attribute value. + *

    + * Source code parsers should treat any quoted string as a + * {@link StringAttributeValue}. + * + * @author Ben Alex + * @since 1.0 + */ +public class StringAttributeValue extends + AbstractAnnotationAttributeValue { + private final String value; + + public StringAttributeValue(final JavaSymbolName name, final String value) { + super(name); + Validate.notNull(value, "Value required"); + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return getName() + " -> " + value; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AbstractAnnotationValues.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AbstractAnnotationValues.java new file mode 100644 index 000000000..5c80171c2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AbstractAnnotationValues.java @@ -0,0 +1,115 @@ +package org.springframework.roo.classpath.details.annotations.populator; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.itd.MemberHoldingTypeDetailsMetadataItem; +import org.springframework.roo.model.JavaType; + +/** + * Abstract class that provides a convenience parser and holder for annotation + * values. Useful if an add-on needs to share annotation parsing outcomes + * between its provider and metadata instances. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractAnnotationValues { + + protected AnnotationMetadata annotationMetadata; + + /** + * Indicates whether the class was able to be parsed at all (ie the metadata + * was properly formed) + */ + protected boolean classParsed; + + protected ClassOrInterfaceTypeDetails governorTypeDetails; + + protected AbstractAnnotationValues( + final MemberHoldingTypeDetails memberHoldingTypeDetails, + final JavaType annotationType) { + Validate.notNull(annotationType, "Annotation to locate is required"); + + if (memberHoldingTypeDetails instanceof ClassOrInterfaceTypeDetails) { + classParsed = true; + + // We have reliable physical type details + governorTypeDetails = (ClassOrInterfaceTypeDetails) memberHoldingTypeDetails; + + // Process values from the annotation, if present + annotationMetadata = governorTypeDetails + .getAnnotation(annotationType); + } + } + + /** + * Convenience constructor that takes a {@link Class} for the annotation + * type + * + * @param governorMetadata to parse (can be null) + * @param annotationType the annotation class (required) + */ + protected AbstractAnnotationValues( + final MemberHoldingTypeDetailsMetadataItem governorMetadata, + final Class annotationType) { + this(governorMetadata, new JavaType(annotationType)); + } + + /** + * Parses the governor's metadata for the requested annotation + * {@link JavaType}. If found, makes the annotation available via the + * {@link #annotationMetadata} field. Subclasses will then generally use + * {@link AutoPopulationUtils#populate(Object, AnnotationMetadata)} to + * complete the configuration of the subclass (we don't invoke + * {@link AutoPopulationUtils} from this constructor because the subclass is + * likely to have set default values for each field, and these will be + * overwritten when the control flow returns to the subclass constructor). + *

    + * If the {@link PhysicalTypeMetadata} cannot be parsed or does not + * internally contain a {@link ClassOrInterfaceTypeDetails}, no attempt will + * be made to populate the values. + * + * @param governorMetadata to parse (can be null) + * @param annotationType to locate and parse (can be null) + */ + protected AbstractAnnotationValues( + final MemberHoldingTypeDetailsMetadataItem governorMetadata, + final JavaType annotationType) { + Validate.notNull(annotationType, "Annotation to locate is required"); + + if (governorMetadata != null) { + final Object governorDetails = governorMetadata + .getMemberHoldingTypeDetails(); + + if (governorDetails instanceof ClassOrInterfaceTypeDetails) { + classParsed = true; + + // We have reliable physical type details + governorTypeDetails = (ClassOrInterfaceTypeDetails) governorDetails; + + // Process values from the annotation, if present + annotationMetadata = governorTypeDetails + .getAnnotation(annotationType); + } + } + } + + /** + * @return the type which declared the annotation (ie the governor; never + * returns null) + */ + public ClassOrInterfaceTypeDetails getGovernorTypeDetails() { + return governorTypeDetails; + } + + public boolean isAnnotationFound() { + return annotationMetadata != null; + } + + public boolean isClassParsed() { + return classParsed; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AnnotationValuesTestCase.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AnnotationValuesTestCase.java new file mode 100644 index 000000000..3def224b8 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AnnotationValuesTestCase.java @@ -0,0 +1,77 @@ +package org.springframework.roo.classpath.details.annotations.populator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.apache.commons.lang3.Validate; +import org.junit.Test; + +/** + * Convenience superclass for unit-testing subclasses of + * {@link AbstractAnnotationValues} + * + * @author Andrew Swan + * @since 1.2.0 + * @param the annotation type + * @param the annotation values class + */ +public abstract class AnnotationValuesTestCase { + + /** + * Subclasses must return the class of annotation whose value class is being + * tested + * + * @return a non-null annotation type + */ + protected abstract Class getAnnotationClass(); + + /** + * Subclasses must return the values class being tested + * + * @return a non-null class + */ + protected abstract Class getValuesClass(); + + @Test + public void testAllAnnotationAttributesHaveAValueField() { + final Class annotationClass = getAnnotationClass(); + assertTrue("Invalid annotation class " + annotationClass, + annotationClass.isAnnotation()); + for (final Method method : annotationClass.getDeclaredMethods()) { + // Look for a field of this name in the values class or any + // superclass + final Field valueField = findField(getValuesClass(), + method.getName()); + assertNotNull("No value field found for annotation attribute " + + method, valueField); + final int fieldModifiers = valueField.getModifiers(); + assertFalse("Value field " + valueField + " is final", + Modifier.isFinal(fieldModifiers)); + assertNotNull("Value field " + valueField + + " is not auto-populated", + valueField.getAnnotation(AutoPopulate.class)); + } + } + + private Field findField(final Class clazz, final String name) { + Validate.notNull(clazz, "Class must not be null"); + Validate.notNull(name, + "Either name or type of the field must be specified"); + Class searchType = clazz; + while (!Object.class.equals(searchType) && searchType != null) { + final Field[] fields = searchType.getDeclaredFields(); + for (final Field field : fields) { + if (name.equals(field.getName())) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AutoPopulate.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AutoPopulate.java new file mode 100644 index 000000000..309828507 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AutoPopulate.java @@ -0,0 +1,25 @@ +package org.springframework.roo.classpath.details.annotations.populator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; + +/** + * Identifies a field that can be automatically populated from + * {@link AnnotationAttributeValue}s. + * + * @author Ben Alex + * @since 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface AutoPopulate { + /** + * @return the name of the annotation value to read (defaults to an empty + * string, which denotes the name of the field should be used) + */ + String value() default ""; +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AutoPopulationUtils.java b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AutoPopulationUtils.java new file mode 100644 index 000000000..c3ef88819 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/annotations/populator/AutoPopulationUtils.java @@ -0,0 +1,202 @@ +package org.springframework.roo.classpath.details.annotations.populator; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.CharAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.classpath.details.annotations.DoubleAttributeValue; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.classpath.details.annotations.LongAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Automatically populates the {@link AutoPopulate} annotated fields on a given + * {@link Object}. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AutoPopulationUtils { + private static final Map attributeNameForEachField = new HashMap(); + private static final Map, List> cachedIntrospections = new HashMap, List>(); + + /** + * Introspects the target {@link Object} for its declared fields, locating + * all {@link AutoPopulate} annotated fields. For each field, an attempt + * will be made to locate the value from the passed + * {@link AnnotationMetadata}. The annotation value will be converted into + * the required field type, or silently skipped if this is not possible (eg + * the user edited source code and made a formatting error). As such it is + * important that the caller. + * + * @param target to put values into (mandatory, cannot be null) + * @param annotation to obtain values from (can be null, for convenience of + * the caller) + */ + public static void populate(final Object target, + final AnnotationMetadata annotation) { + Validate.notNull(target, "Target required"); + + if (annotation == null) { + return; + } + + List fields = cachedIntrospections.get(target.getClass()); + if (fields == null) { + // Go and cache them + fields = new ArrayList(); + + for (final Field field : target.getClass().getDeclaredFields()) { + // Determine if this field even contains the necessary + // annotation + final AutoPopulate ap = field.getAnnotation(AutoPopulate.class); + if (ap == null) { + continue; + } + + // Determine attribute name we should be looking for in the + // annotation + String attribute = ap.value(); + if ("".equals(ap.value())) { + attribute = field.getName(); + } + + // Ensure field is accessible + if (!field.isAccessible()) { + field.setAccessible(true); + } + + final JavaSymbolName attributeName = new JavaSymbolName( + attribute); + + // Store the info + fields.add(field); + attributeNameForEachField.put(field, attributeName); + + } + + cachedIntrospections.put(target.getClass(), fields); + } + + for (final Field field : fields) { + // Lookup whether this attribute name was provided + final JavaSymbolName attributeName = attributeNameForEachField + .get(field); + if (attributeName == null) { + throw new IllegalStateException( + "Expected cached attribute name lookup"); + } + + if (annotation.getAttributeNames().contains(attributeName)) { + // Get the value + final AnnotationAttributeValue value = annotation + .getAttribute(attributeName); + + // Assign the value to the target object + try { + final Class fieldType = field.getType(); + if (value instanceof BooleanAttributeValue + && (fieldType.equals(Boolean.class) || fieldType + .equals(Boolean.TYPE))) { + field.set(target, value.getValue()); + } + else if (value instanceof CharAttributeValue + && (fieldType.equals(Character.class) || fieldType + .equals(Character.TYPE))) { + field.set(target, value.getValue()); + } + else if (value instanceof ClassAttributeValue + && fieldType.equals(JavaType.class)) { + field.set(target, value.getValue()); + } + else if (value instanceof DoubleAttributeValue + && (fieldType.equals(Double.class) || fieldType + .equals(Double.TYPE))) { + field.set(target, value.getValue()); + } + else if (value instanceof EnumAttributeValue + && Enum.class.isAssignableFrom(fieldType)) { + field.set(target, + ((EnumAttributeValue) value).getAsEnum()); + } + else if (value instanceof IntegerAttributeValue + && (fieldType.equals(Integer.class) || fieldType + .equals(Integer.TYPE))) { + field.set(target, value.getValue()); + } + else if (value instanceof LongAttributeValue + && (fieldType.equals(Long.class) || fieldType + .equals(Long.TYPE))) { + field.set(target, value.getValue()); + } + else if (value instanceof StringAttributeValue + && fieldType.equals(String.class)) { + field.set(target, value.getValue()); + } + else if (value instanceof StringAttributeValue + && fieldType.getComponentType() != null + && fieldType.getComponentType() + .equals(String.class)) { + // ROO-618 + final Object newValue = Array.newInstance(String.class, + 1); + Array.set(newValue, 0, value.getValue()); + field.set(target, newValue); + } + else if (value instanceof ArrayAttributeValue + && fieldType.isArray()) { + // The field is a string array, the attribute is an + // array, so let's hope it's a string array + final ArrayAttributeValue castValue = (ArrayAttributeValue) value; + final List result = new ArrayList(); + final List result1 = new ArrayList(); + for (final AnnotationAttributeValue val : castValue + .getValue()) { + // For now we'll only support arrays of strings + if (fieldType.getComponentType().equals( + String.class) + && val instanceof StringAttributeValue) { + final StringAttributeValue stringValue = (StringAttributeValue) val; + result.add(stringValue.getValue()); + } + else if (fieldType.getComponentType().equals( + JavaType.class) + && val instanceof ClassAttributeValue) { + final ClassAttributeValue classValue = (ClassAttributeValue) val; + result1.add(classValue.getValue()); + } + } + if (result.size() > 0) { + // We had at least one string array, so we change + // the field + field.set(target, result.toArray(new String[] {})); + } + if (result1.size() > 0) { + // We had at least one string array, so we change + // the field + field.set(target, + result1.toArray(new JavaType[] {})); + } + } + // If not in the above list, it's unsupported so we silently + // skip + } + catch (final Throwable ignoreFailures) { + } + } + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/AbstractComment.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/AbstractComment.java new file mode 100644 index 000000000..5c539ad48 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/AbstractComment.java @@ -0,0 +1,24 @@ +package org.springframework.roo.classpath.details.comments; + +/** + * @author Mike De Haan + */ +public abstract class AbstractComment { + + private String comment; + + protected AbstractComment() { + } + + protected AbstractComment(String comment) { + this.comment = comment; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/BlockComment.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/BlockComment.java new file mode 100644 index 000000000..0c1620491 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/BlockComment.java @@ -0,0 +1,14 @@ +package org.springframework.roo.classpath.details.comments; + +/** + * @author Mike De Haan + */ +public class BlockComment extends AbstractComment { + + public BlockComment() { + } + + public BlockComment(String comment) { + super(comment); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentFormatter.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentFormatter.java new file mode 100644 index 000000000..9c4ab76f7 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentFormatter.java @@ -0,0 +1,90 @@ +package org.springframework.roo.classpath.details.comments; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Mike De Haan + */ +public class CommentFormatter { + + private static Pattern commentFormattingRegex = Pattern + .compile("[\\s]*(.+)\\r?\\n?"); + + /** + * Format a given comment string with the indent level specified. + * + * @param comment + * @param indentLevel + * @return + */ + public String format(String comment, int indentLevel) { + + // Return if there's nothing to do + if (comment == null) { + return null; + } + + final StringBuilder indentString = new StringBuilder(); + for (int i = 0; i < indentLevel; i++) { + indentString.append(" "); + } + + // Comment ends with newline + boolean endsWithNewline = (comment.endsWith("\r\n") || comment + .endsWith("\n")); + + List matchList = new ArrayList(); + Matcher regexMatcher = commentFormattingRegex.matcher(comment); + while (regexMatcher.find()) { + matchList.add(regexMatcher.group()); + } + + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < matchList.size(); i++) { + + // We need to handle the last newline + if (i == matchList.size() - 1) { + builder.append(indentString.toString() + " " + + matchList.get(i).trim() + + (endsWithNewline ? "\n" : "")); + } + else if (i == 0) { + builder.append(indentString.toString() + + matchList.get(i).trim() + "\n"); + } + else { + builder.append(indentString.toString() + " " + + matchList.get(i).trim() + "\n"); + } + } + + return builder.toString(); + } + + /** + * Formats a plain string (including newlines) and formats it as Javadoc. + * + * @param input Plain string (including newlines) to be formatted as Javadoc + * @return Formatted Javadoc + */ + public String formatStringAsJavadoc(String input) { + + if (input == null) { + return null; + } + + final StringBuilder finalComment = new StringBuilder("/**\n"); + Matcher regexMatcher = commentFormattingRegex.matcher(input); + while (regexMatcher.find()) { + finalComment.append("* "); + finalComment.append(regexMatcher.group()); + } + finalComment.append("\n*/\n"); + + return finalComment.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentStructure.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentStructure.java new file mode 100644 index 000000000..6e724baa4 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentStructure.java @@ -0,0 +1,80 @@ +package org.springframework.roo.classpath.details.comments; + +import org.apache.commons.lang3.Validate; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author Mike De Haan + */ +public class CommentStructure { + + public static enum CommentLocation { + BEGINNING, INTERNAL, END + } + + private List beginComments; + private List endComments; + private List internalComments; + + /** + * Helper method to assist in adding comments to structures. + * + * @param comment The comment to add (LineComment, BlockComment, + * JavadocComment) + * @param commentLocation Where the comment should be added. + */ + public void addComment(AbstractComment comment, + CommentLocation commentLocation) { + + Validate.notNull(comment, "Comment must not be null"); + Validate.notNull(comment, "Comment location must be specified"); + + if (commentLocation.equals(CommentLocation.BEGINNING)) { + if (beginComments == null) { + beginComments = new LinkedList(); + } + + beginComments.add(comment); + } + else if (commentLocation.equals(CommentLocation.INTERNAL)) { + if (internalComments == null) { + internalComments = new LinkedList(); + } + + internalComments.add(comment); + } + else { + if (endComments == null) { + endComments = new LinkedList(); + } + + endComments.add(comment); + } + } + + public List getBeginComments() { + return beginComments; + } + + public List getEndComments() { + return endComments; + } + + public List getInternalComments() { + return internalComments; + } + + public void setBeginComments(final List beginComments) { + this.beginComments = beginComments; + } + + public void setEndComments(final List endComments) { + this.endComments = endComments; + } + + public void setInternalComments(final List internalComments) { + this.internalComments = internalComments; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentedJavaStructure.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentedJavaStructure.java new file mode 100644 index 000000000..e9fd83dcc --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/CommentedJavaStructure.java @@ -0,0 +1,15 @@ +package org.springframework.roo.classpath.details.comments; + +/** + * Metadata concerning comments + * + * @author Mike De Haan + * @since 1.3 + */ +public interface CommentedJavaStructure { + + CommentStructure getCommentStructure(); + + void setCommentStructure(CommentStructure commentStructure); + +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/JavadocComment.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/JavadocComment.java new file mode 100644 index 000000000..f7f15fadc --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/JavadocComment.java @@ -0,0 +1,14 @@ +package org.springframework.roo.classpath.details.comments; + +/** + * @author Mike De Haan + */ +public class JavadocComment extends AbstractComment { + + public JavadocComment() { + } + + public JavadocComment(String comment) { + super(comment); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/details/comments/LineComment.java b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/LineComment.java new file mode 100644 index 000000000..d5b29b127 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/details/comments/LineComment.java @@ -0,0 +1,15 @@ +package org.springframework.roo.classpath.details.comments; + +/** + * @author Mike De Haan + */ +public class LineComment extends AbstractComment { + + public LineComment() { + + } + + public LineComment(String comment) { + super(comment); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractItdMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractItdMetadataProvider.java new file mode 100644 index 000000000..9c6b804f5 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractItdMetadataProvider.java @@ -0,0 +1,821 @@ +package org.springframework.roo.classpath.itd; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.springframework.roo.classpath.ItdDiscoveryService; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.IdentifiableJavaStructure; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; + +import java.util.logging.Logger; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Provides common functionality used by ITD-based generators. + *

    + * This abstract class assumes: + *

    + *

    + * Put differently, this abstract class assumes every ITD will have a + * corresponding "governor". A "governor" is defined as the type which will + * eventually receive the introduction. The abstract class assumes all metadata + * identification strings represent the name of the governor, albeit with a + * metadata class specific to the add-on. When an instance-specific metadata + * identification request is received, the governor will be obtained and in turn + * introspected for one of the trigger annotations. If these are detected, or if + * there is already an ITD file of the same name as would normally be created + * had a trigger annotation been found, the metadata will be created. The + * metadata creation method is expected to create, update or delete the ITD file + * as appropriate. + * + * @author Ben Alex + * @since 1.0 + */ +@Component(componentAbstract = true) +public abstract class AbstractItdMetadataProvider extends + AbstractHashCodeTrackingMetadataNotifier implements + ItdTriggerBasedMetadataProvider, MetadataNotificationListener { + + protected final static Logger LOGGER = HandlerUtils.getLogger(AbstractItdMetadataProvider.class); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + /** + * Requires the governor to be a {@link PhysicalTypeCategory#CLASS} (as + * opposed to an interface etc) + */ + // TODO change the type of this field to PhysicalTypeCategory and allow + // subclasses to pass it via a new constructor + private boolean dependsOnGovernorBeingAClass = true; + /** + * Cancel production if the governor type details are required, but aren't + * available + */ + private boolean dependsOnGovernorTypeDetailAvailability = true; + protected FileManager fileManager; + /** We don't care about trigger annotations; we always produce metadata */ + private boolean ignoreTriggerAnnotations = false; + protected ItdDiscoveryService itdDiscoveryService; + + protected MemberDetailsScanner memberDetailsScanner; + + /** + * The annotations which, if present on a class or interface, will cause + * metadata to be created + */ + private final List metadataTriggers = new ArrayList(); + + protected PersistenceMemberLocator persistenceMemberLocator; + + protected TypeLocationService typeLocationService; + + /** + * Registers an additional {@link JavaType} that will trigger metadata + * registration. + * + * @param javaType the type-level annotation to detect that will cause + * metadata creation (required) + */ + public void addMetadataTrigger(final JavaType javaType) { + Validate.notNull(javaType, + "Java type required for metadata trigger registration"); + metadataTriggers.add(javaType); + } + + /** + * Registers the given {@link JavaType}s as triggering metadata + * registration. + * + * @param triggerTypes the type-level annotations to detect that will cause + * metadata creation + * @since 1.2.0 + */ + public void addMetadataTriggers(final JavaType... triggerTypes) { + for (final JavaType triggerType : triggerTypes) { + addMetadataTrigger(triggerType); + } + } + + /** + * Called whenever there is a requirement to produce a local identifier (ie + * an instance identifier consistent with {@link #getProvidesType()}) for + * the indicated {@link JavaType} and {@link Path}. + * + * @param javaType the type (required) + * @param path the path (required) + * @return an instance-specific identifier that is compatible with + * {@link #getProvidesType()} (never null or empty) + */ + protected abstract String createLocalIdentifier(JavaType javaType, + LogicalPath path); + + /** + * Deletes the given ITD, either now or later. + * + * @param metadataIdentificationString the ITD's metadata ID + * @param itdFilename the ITD's filename + * @param reason the reason for deletion; ignored if now is + * false + * @param now whether to delete the ITD immediately; false + * schedules it for later deletion; this is preferable when it's + * possible that the ITD might need to be re-created in the + * meantime (e.g. because some ancestor metadata has changed to + * that effect), otherwise there will be spurious console + * messages about the ITD being deleted and created + */ + private void deleteItd(final String metadataIdentificationString, + final String itdFilename, final String reason, final boolean now) { + + if (now) { + getFileManager().delete(itdFilename, reason); + } + else { + getFileManager() + .createOrUpdateTextFileIfRequired(itdFilename, "", false); + } + getItdDiscoveryService().removeItdTypeDetails(metadataIdentificationString); + // TODO do we need to notify downstream dependencies that this ITD has + // gone away? + } + + public final MetadataItem get(final String metadataIdentificationString) { + + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + metadataIdentificationString).equals( + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Unexpected request for '%s' to this provider (which uses '%s')", + metadataIdentificationString, getProvidesType()); + + // Remove the upstream dependencies for this instance (we'll be + // recreating them later, if needed) + getMetadataDependencyRegistry() + .deregisterDependencies(metadataIdentificationString); + + // Compute the identifier for the Physical Type Metadata we're + // correlated with + final String governorPhysicalTypeIdentifier = getGovernorPhysicalTypeIdentifier(metadataIdentificationString); + + // Obtain the physical type + final PhysicalTypeMetadata governorPhysicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService() + .get(governorPhysicalTypeIdentifier); + if (governorPhysicalTypeMetadata == null + || !governorPhysicalTypeMetadata.isValid()) { + // We can't get even basic information about the physical type, so + // abort (the ITD will be deleted by ItdFileDeletionService) + return null; + } + + // Flag to indicate whether we'll even try to create this metadata + boolean produceMetadata = false; + + // Determine if we should generate the metadata on the basis of it + // containing a trigger annotation + final ClassOrInterfaceTypeDetails cid = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (cid != null) { + // Only create metadata if the type is annotated with one of the + // metadata triggers + for (final JavaType trigger : metadataTriggers) { + if (cid.getAnnotation(trigger) != null) { + produceMetadata = true; + break; + } + } + } + + // Fall back to ignoring trigger annotations + if (ignoreTriggerAnnotations) { + produceMetadata = true; + } + + // Cancel production if the governor type details are required, but + // aren't available + if (dependsOnGovernorTypeDetailAvailability && cid == null) { + produceMetadata = false; + } + + // Cancel production if the governor is not a class, and the subclass + // only wants to know about classes + if (cid != null && dependsOnGovernorBeingAClass + && cid.getPhysicalTypeCategory() != PhysicalTypeCategory.CLASS) { + produceMetadata = false; + } + + final String itdFilename = governorPhysicalTypeMetadata + .getItdCanonicalPath(this); + if (!produceMetadata && isGovernor(cid) + && getFileManager().exists(itdFilename)) { + // We don't seem to want metadata anymore, yet the ITD physically + // exists, so get rid of it + // This might be because the trigger annotation has been removed, + // the governor is missing a class declaration, etc. + deleteItd(metadataIdentificationString, itdFilename, + "not required for governor " + cid.getName(), true); + return null; + } + + if (produceMetadata) { + // This type contains an annotation we were configured to detect, or + // there is an ITD (which may need deletion), so we need to produce + // the metadata + final JavaType aspectName = governorPhysicalTypeMetadata + .getItdJavaType(this); + final ItdTypeDetailsProvidingMetadataItem metadata = getMetadata( + metadataIdentificationString, aspectName, + governorPhysicalTypeMetadata, itdFilename); + + // There is no requirement to register a direct connection with the + // physical type and this metadata because changes will + // trickle down via the class-level notification registered by + // convention by AbstractItdMetadataProvider subclasses (BPA 10 Dec + // 2010) + + if (metadata == null || !metadata.isValid()) { + // The metadata couldn't be created properly + deleteItd(metadataIdentificationString, itdFilename, "", false); + return null; + } + + // By this point we have a valid MetadataItem, but it might not + // contain any members for the resulting ITD etc + + // Handle the management of the ITD file + boolean deleteItdFile = false; + final ItdTypeDetails itdTypeDetails = metadata + .getMemberHoldingTypeDetails(); + + if (itdTypeDetails == null) { + // The ITD has no members + deleteItdFile = true; + } + + if (!deleteItdFile) { + // We have some members in the ITD, so decide if we're to write + // something to disk + final ItdSourceFileComposer itdSourceFileComposer = new ItdSourceFileComposer( + metadata.getMemberHoldingTypeDetails()); + + // Decide whether the get an ITD on-disk based on whether there + // is physical content to write + if (itdSourceFileComposer.isContent()) { + // We have content to write + getItdDiscoveryService().addItdTypeDetails(itdTypeDetails); + final String itd = itdSourceFileComposer.getOutput(); + getFileManager().createOrUpdateTextFileIfRequired(itdFilename, + itd, false); + } + else { + // We don't have content to write + deleteItdFile = true; + } + } + + if (deleteItdFile) { + deleteItd(metadataIdentificationString, itdFilename, null, + false); + } + + // Eagerly notify that the metadata has been updated; this also + // registers the metadata hash code in the superclass' cache to + // avoid + // unnecessary subsequent notifications if it hasn't changed + notifyIfRequired(metadata); + + return metadata; + } + return null; + } + + /** + * Called whenever there is a requirement to convert a local metadata + * identification string (ie an instance identifier consistent with + * {@link #getProvidesType()}) into the corresponding governor physical type + * identifier. + * + * @param metadataIdentificationString the local identifier (required) + * @return the physical type identifier of the governor (required) + */ + protected abstract String getGovernorPhysicalTypeIdentifier( + String metadataIdentificationString); + + public final String getIdForPhysicalJavaType( + final String physicalJavaTypeIdentifier) { + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + physicalJavaTypeIdentifier).equals( + MetadataIdentificationUtils + .getMetadataClass(PhysicalTypeIdentifier + .getMetadataIdentiferType())), + "Expected a valid physical Java type instance identifier (not '%s')", + physicalJavaTypeIdentifier); + final JavaType javaType = PhysicalTypeIdentifier + .getJavaType(physicalJavaTypeIdentifier); + final LogicalPath path = PhysicalTypeIdentifier + .getPath(physicalJavaTypeIdentifier); + return createLocalIdentifier(javaType, path); + } + + /** + * Assists creating a local metadata identification string (MID) from any + * presented {@link MemberHoldingTypeDetails} implementation. This is + * achieved by extracting the + * {@link IdentifiableJavaStructure#getDeclaredByMetadataId()} and + * converting it into a {@link JavaType} and {@link Path}, then calling + * {@link #createLocalIdentifier(JavaType, Path)}. + * + * @param memberHoldingTypeDetails the member holder from which the + * declaring type information should be extracted (required) + * @return a MID produced by {@link #createLocalIdentifier(JavaType, Path)} + * for the extracted Java type in the extract Path (never null) + */ + protected String getLocalMid( + final MemberHoldingTypeDetails memberHoldingTypeDetails) { + final JavaType governorType = memberHoldingTypeDetails.getName(); + + // Extract out the metadata provider class (we need this later to + // extract just the Path it is located in) + final String providesType = MetadataIdentificationUtils + .getMetadataClass(memberHoldingTypeDetails + .getDeclaredByMetadataId()); + final LogicalPath path = PhysicalTypeIdentifierNamingUtils.getPath( + providesType, + memberHoldingTypeDetails.getDeclaredByMetadataId()); + // Produce the local MID we're going to use to make the request + return createLocalIdentifier(governorType, path); + } + + /** + * Returns details of the given class or interface type's members + * + * @param cid the physical type for which to get the members (can be + * null) + * @return null if the member details are unavailable + */ + protected MemberDetails getMemberDetails( + final ClassOrInterfaceTypeDetails cid) { + + if(memberDetailsScanner == null){ + memberDetailsScanner = getMemberDetailsScanner(); + } + Validate.notNull(memberDetailsScanner, "MemberDetailsScanner is required"); + + if (cid == null) { + return null; + } + return memberDetailsScanner.getMemberDetails(getClass().getName(), cid); + } + + /** + * Returns details of the given Java type's members + * + * @param type the type for which to get the members (required) + * @return null if the member details are unavailable + */ + protected MemberDetails getMemberDetails(final JavaType type) { + + if(typeLocationService == null){ + typeLocationService = getTypeLocationService(); + } + Validate.notNull(typeLocationService, "TypeLocationService is required"); + + final String physicalTypeIdentifier = typeLocationService + .getPhysicalTypeIdentifier(type); + if (physicalTypeIdentifier == null) { + return null; + } + // We need to lookup the metadata we depend on + final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService() + .get(physicalTypeIdentifier); + return getMemberDetails(physicalTypeMetadata); + } + + /** + * Returns details of the given physical type's members + * + * @param physicalTypeMetadata the physical type for which to get the + * members (can be null) + * @return null if the member details are unavailable + */ + protected MemberDetails getMemberDetails( + final PhysicalTypeMetadata physicalTypeMetadata) { + + if(memberDetailsScanner == null){ + memberDetailsScanner = getMemberDetailsScanner(); + } + Validate.notNull(memberDetailsScanner, "MemberDetailsScanner is required"); + + // We need to abort if we couldn't find dependent metadata + if (physicalTypeMetadata == null || !physicalTypeMetadata.isValid()) { + return null; + } + + final ClassOrInterfaceTypeDetails cid = physicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (cid == null) { + // Abort if the type's class details aren't available (parse error + // etc) + return null; + } + return memberDetailsScanner.getMemberDetails(getClass().getName(), cid); + } + + /** + * Called when it is time to create the actual metadata instance. + * + * @param metadataIdentificationString the local identifier (non-null and + * consistent with {@link #getProvidesType()}) + * @param aspectName the Java type name for the ITD (non-null and obtained + * via + * {@link PhysicalTypeMetadata#getItdJavaType(ItdMetadataProvider)} + * ) + * @param governorPhysicalTypeMetadata the governor metadata (non-null and + * obtained via + * {@link #getGovernorPhysicalTypeIdentifier(String)}) + * @param itdFilename the canonical filename for the ITD (non-null and + * obtained via + * {@link PhysicalTypeMetadata#getItdCanoncialPath(ItdMetadataProvider)} + * ) + * @return the new metadata (may return null if there is a problem + * processing) + */ + protected abstract ItdTypeDetailsProvidingMetadataItem getMetadata( + String metadataIdentificationString, JavaType aspectName, + PhysicalTypeMetadata governorPhysicalTypeMetadata, + String itdFilename); + + /** + * Looks up the given type's inheritance hierarchy for metadata of the given + * type, starting with the given type's parent and going upwards until the + * first such instance is found (i.e. lower level metadata takes priority + * over higher level metadata) + * + * @param the type of metadata to look for + * @param child the child type whose parents to search (required) + * @return null if there is no such metadata + */ + @SuppressWarnings("unchecked") + protected T getParentMetadata( + final ClassOrInterfaceTypeDetails child) { + T parentMetadata = null; + ClassOrInterfaceTypeDetails superCid = child.getSuperclass(); + while (parentMetadata == null && superCid != null) { + final String superCidPhysicalTypeIdentifier = superCid + .getDeclaredByMetadataId(); + final LogicalPath path = PhysicalTypeIdentifier + .getPath(superCidPhysicalTypeIdentifier); + final String superCidLocalIdentifier = createLocalIdentifier( + superCid.getName(), path); + parentMetadata = (T) getMetadataService().get(superCidLocalIdentifier); + superCid = superCid.getSuperclass(); + } + return parentMetadata; // Could be null + } + + /** + * Indicates whether the given type is the governor for this provider. This + * implementation simply checks whether the given type is either a class or + * an interface, based on the value of {@link #dependsOnGovernorBeingAClass} + * . A more sophisticated implementation could check for the presence of + * particular annotations or the implementation of particular interfaces. + * + * @param type can be null + * @return false if the given type is null + */ + protected boolean isGovernor(final ClassOrInterfaceTypeDetails type) { + if (type == null) { + return false; + } + if (dependsOnGovernorBeingAClass) { + return type.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS; + } + return type.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE; + } + + protected boolean isIgnoreTriggerAnnotations() { + return ignoreTriggerAnnotations; + } + + private boolean isNotificationForJavaType(final String mid) { + return MetadataIdentificationUtils.getMetadataClass(mid).equals( + MetadataIdentificationUtils + .getMetadataClass(PhysicalTypeIdentifier + .getMetadataIdentiferType())); + } + + public final void notify(final String upstreamDependency, + String downstreamDependency) { + if (downstreamDependency == null) { + notifyForGenericListener(upstreamDependency); + return; + } + + // Handle if the downstream dependency is "class level", meaning we need + // to figure out the specific downstream MID this metadata provider + // wants to update/refresh. + if (MetadataIdentificationUtils + .isIdentifyingClass(downstreamDependency)) { + // We have not identified an instance-specific downstream MID, so + // we'll need to calculate an instance-specific downstream MID to + // retrieve. + downstreamDependency = resolveDownstreamDependencyIdentifier(upstreamDependency); + + // We skip if the resolution method returns null, as it doesn't want + // to continue for some reason + if (downstreamDependency == null) { + return; + } + + Validate.isTrue( + MetadataIdentificationUtils + .isIdentifyingInstance(downstreamDependency), + "An instance-specific downstream MID was required by '%s' (not '%s')", + getClass().getName(), downstreamDependency); + + // We only need to proceed if the downstream dependency relationship + // is not already registered. + // It is unusual to register a direct downstream relationship given + // it costs dependency registration memory and class-level + // notifications will always occur anyway. + if (getMetadataDependencyRegistry().getDownstream(upstreamDependency) + .contains(downstreamDependency)) { + return; + } + } + + // We should now have an instance-specific "downstream dependency" that + // can be processed by this class + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass( + downstreamDependency).equals( + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Unexpected downstream notification for '%s' to this provider (which uses '%s')", + downstreamDependency, getProvidesType()); + + // We no longer notify downstreams here, as the "get" operation with + // eviction will ensure the main get(String) method below will be fired + // and it + // directly notified downstreams as part of that method (BPA 10 Dec + // 2010) + getMetadataService().evictAndGet(downstreamDependency); + } + + /** + * Designed to handle events originating from a + * {@link MetadataDependencyRegistry#addNotificationListener(MetadataNotificationListener)} + * registration. Such events are always presented with a non-null upstream + * dependency indicator and a null downstream dependency indicator. These + * events differ from events related to {@link PhysicalTypeIdentifier} + * registrations, as in those cases the downstream dependency indicator will + * be the class-level {@link #getProvidesType()}. + *

    + * This method allows subclasses to specially handle generic + * {@link MetadataDependencyRegistry} events. + * + * @param upstreamDependency the upstream which was modified (guaranteed to + * be non-null, but could be class-level or instance-level) + */ + protected void notifyForGenericListener(final String upstreamDependency) { + } + + /** + * Removes a {@link JavaType} metadata trigger registration. If the type was + * never registered, the method returns without an error. + * + * @param javaType to remove (required) + */ + public void removeMetadataTrigger(final JavaType javaType) { + Validate.notNull(javaType, + "Java type required for metadata trigger deregistration"); + metadataTriggers.remove(javaType); + } + + /** + * Removes the given {@link JavaType}s as triggering metadata registration. + * + * @param triggerTypes the type-level annotations to remove as triggers + * @since 1.2.0 + */ + public void removeMetadataTriggers(final JavaType... triggerTypes) { + for (final JavaType triggerType : triggerTypes) { + removeMetadataTrigger(triggerType); + } + } + + /** + * Invoked whenever a "class-level" downstream dependency identifier is + * presented in a metadata notification. An "instance-specific" downstream + * dependency identifier is required so that a metadata request can + * ultimately be made. This method is responsible for evaluating the + * upstream dependency identifier and converting it into a valid downstream + * dependency identifier. The downstream dependency identifier must be of + * the same type as this metadata provider's {@link #getProvidesType()}. The + * downstream dependency identifier must also be instance-specific. + *

    + * The basic implementation offered in this class will only convert a + * {@link PhysicalTypeIdentifier}. If a subclass registers a dependency on + * an upstream (other than + * {@link PhysicalTypeIdentifier#getMetadataIdentiferType()}) and presents + * their {@link #getProvidesType()} as the downstream (thus meaning only + * class-level downstream dependency identifiers will be presented), they + * must override this method and appropriately handle instance-specific + * downstream dependency identifier resolution. + *

    + * This method may also return null if it wishes to abort processing of the + * notification. This may be appropriate if a determination cannot be made + * at this time for whatever reason (eg too early in a lifecycle etc). + * + * @param upstreamDependency the upstream (never null) + * @return an instance-specific MID of type {@link #getProvidesType()} (or + * null if the metadata notification should be aborted) + */ + protected String resolveDownstreamDependencyIdentifier( + final String upstreamDependency) { + // We only support analysis of a PhysicalTypeIdentifier upstream MID to + // convert this to a downstream MID. + // In any other case the downstream metadata should have registered an + // instance-specific downstream dependency on a given upstream. + Validate.isTrue(isNotificationForJavaType(upstreamDependency), + "Expected class-level notifications only for physical Java types (not '" + + upstreamDependency + "') for metadata provider " + + getClass().getName()); + + // A physical Java type has changed, and determine what the + // corresponding local metadata identification string would have been + final JavaType javaType = PhysicalTypeIdentifier + .getJavaType(upstreamDependency); + final LogicalPath path = PhysicalTypeIdentifier + .getPath(upstreamDependency); + return createLocalIdentifier(javaType, path); + } + + /** + * If set to true (default is true), ensures the governor type details + * represent a class. Note that + * {@link #setDependsOnGovernorTypeDetailAvailability(boolean)} must also be + * true to ensure this can be relied upon. + * + * @param dependsOnGovernorBeingAClass true means governor type detail must + * represent a class + */ + public void setDependsOnGovernorBeingAClass( + final boolean dependsOnGovernorBeingAClass) { + this.dependsOnGovernorBeingAClass = dependsOnGovernorBeingAClass; + } + + /** + * If set to true (default is true), ensures subclass not called unless the + * governor type details are available. + * + * @param dependsOnGovernorTypeDetailAvailability true means governor type + * details must be available + */ + public void setDependsOnGovernorTypeDetailAvailability( + final boolean dependsOnGovernorTypeDetailAvailability) { + this.dependsOnGovernorTypeDetailAvailability = dependsOnGovernorTypeDetailAvailability; + } + + protected void setIgnoreTriggerAnnotations( + final boolean ignoreTriggerAnnotations) { + this.ignoreTriggerAnnotations = ignoreTriggerAnnotations; + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on AbstractIdMetadataProvider."); + return null; + } + }else{ + return fileManager; + } + } + + public ItdDiscoveryService getItdDiscoveryService(){ + if(itdDiscoveryService == null){ + // Get all Services implement ItdDiscoveryService interface + try { + ServiceReference[] references = context.getAllServiceReferences(ItdDiscoveryService.class.getName(), null); + + for(ServiceReference ref : references){ + return (ItdDiscoveryService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ItdDiscoveryService on AbstractIdMetadataProvider."); + return null; + } + }else{ + return itdDiscoveryService; + } + + } + + public MemberDetailsScanner getMemberDetailsScanner(){ + // Get all Services implement MemberDetailsScanner interface + try { + ServiceReference[] references = context.getAllServiceReferences(MemberDetailsScanner.class.getName(), null); + + for(ServiceReference ref : references){ + return (MemberDetailsScanner) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MemberDetailsScanner on AbstractIdMetadataProvider."); + return null; + } + } + + public TypeLocationService getTypeLocationService(){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = context.getAllServiceReferences(TypeLocationService.class.getName(), null); + + for(ServiceReference ref : references){ + return (TypeLocationService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load TypeLocationService on AbstractIdMetadataProvider."); + return null; + } + } + + public PersistenceMemberLocator getPersistenceMemberLocator(){ + if(persistenceMemberLocator == null){ + // Get all Services implement TypeLocationService interface + try { + ServiceReference[] references = context.getAllServiceReferences(PersistenceMemberLocator.class.getName(), null); + + for(ServiceReference ref : references){ + return (PersistenceMemberLocator) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PersistenceMemberLocator on AbstractIdMetadataProvider."); + return null; + } + }else{ + return persistenceMemberLocator; + } + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractItdTypeDetailsProvidingMetadataItem.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractItdTypeDetailsProvidingMetadataItem.java new file mode 100644 index 000000000..8121b141f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractItdTypeDetailsProvidingMetadataItem.java @@ -0,0 +1,380 @@ +package org.springframework.roo.classpath.itd; + +import static java.lang.reflect.Modifier.PRIVATE; +import static java.lang.reflect.Modifier.PUBLIC; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; + +/** + * Abstract implementation of {@link ItdTypeDetailsProvidingMetadataItem}, which + * assumes the subclass will require a non-null + * {@link ClassOrInterfaceTypeDetails} representing the governor and wishes to + * build an ITD via the {@link ItdTypeDetailsBuilder} mechanism. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractItdTypeDetailsProvidingMetadataItem extends + AbstractMetadataItem implements ItdTypeDetailsProvidingMetadataItem { + + protected JavaType aspectName; + protected ItdTypeDetailsBuilder builder; + protected JavaType destination; + protected PhysicalTypeMetadata governorPhysicalTypeMetadata; + protected ClassOrInterfaceTypeDetails governorTypeDetails; + protected ItdTypeDetails itdTypeDetails; + + /** + * Validates input and constructs a superclass that implements + * {@link ItdTypeDetailsProvidingMetadataItem}. + *

    + * Exposes the {@link ClassOrInterfaceTypeDetails} of the governor, if + * available. If they are not available, ensures {@link #isValid()} returns + * false. + *

    + * Subclasses should generally return immediately if {@link #isValid()} is + * false. Subclasses should also attempt to set the {@link #itdTypeDetails} + * to contain the output of their ITD where {@link #isValid()} is true. + * + * @param identifier the identifier for this item of metadata (required) + * @param aspectName the Java type of the ITD (required) + * @param governorPhysicalTypeMetadata the governor, which is expected to + * contain a {@link ClassOrInterfaceTypeDetails} (required) + */ + protected AbstractItdTypeDetailsProvidingMetadataItem( + final String identifier, final JavaType aspectName, + final PhysicalTypeMetadata governorPhysicalTypeMetadata) { + super(identifier); + Validate.notNull(aspectName, "Aspect name required"); + Validate.notNull(governorPhysicalTypeMetadata, + "Governor physical type metadata required"); + + this.aspectName = aspectName; + this.governorPhysicalTypeMetadata = governorPhysicalTypeMetadata; + + final Object physicalTypeDetails = governorPhysicalTypeMetadata + .getMemberHoldingTypeDetails(); + if (physicalTypeDetails instanceof ClassOrInterfaceTypeDetails) { + // We have reliable physical type details + governorTypeDetails = (ClassOrInterfaceTypeDetails) physicalTypeDetails; + } + else { + // There is a problem + valid = false; + } + + destination = governorTypeDetails.getName(); + + // Provide the subclass a builder, to make preparing an ITD even easier + builder = new ItdTypeDetailsBuilder(getId(), governorTypeDetails, + aspectName, true); + } + + private void addToImports(final List parameterTypes) { + if (parameterTypes != null) { + final List typesToImport = new ArrayList(); + for (final JavaType parameterType : parameterTypes) { + if (!JdkJavaType.isPartOfJavaLang(parameterType)) { + typesToImport.add(parameterType); + } + } + builder.getImportRegistrationResolver().addImports(typesToImport); + } + } + + /** + * Generates the {@link ItdTypeDetails} from the current contents of this + * instance's {@link ItdTypeDetailsBuilder}. + * + * @since 1.2.0 + */ + protected void buildItd() { + itdTypeDetails = builder.build(); + } + + /** + * Ensures that the governor extends the given type, i.e. introduces that + * type as a supertype iff it's not already one + * + * @param javaType the type to extend (required) + * @since 1.2.0 + */ + protected final void ensureGovernorExtends(final JavaType javaType) { + if (!governorTypeDetails.extendsType(javaType)) { + builder.addExtendsTypes(javaType); + } + } + + /** + * Ensures that the governor implements the given type. + * + * @param javaType the type to implement (required) + * @since 1.2.0 + */ + protected final void ensureGovernorImplements(final JavaType javaType) { + if (!governorTypeDetails.implementsType(javaType)) { + builder.addImplementsType(javaType); + } + } + + protected MethodMetadataBuilder getAccessorMethod(final FieldMetadata field) { + return getAccessorMethod( + field, + InvocableMemberBodyBuilder.getInstance().appendFormalLine( + "return " + field.getFieldName().getSymbolName() + ";")); + } + + protected MethodMetadataBuilder getAccessorMethod( + final FieldMetadata field, + final InvocableMemberBodyBuilder bodyBuilder) { + return getMethod(PUBLIC, BeanInfoUtils.getAccessorMethodName(field), + field.getFieldType(), null, null, bodyBuilder); + } + + protected MethodMetadataBuilder getAccessorMethod( + final JavaSymbolName fieldName, final JavaType fieldType) { + return getAccessorMethod( + fieldName, + fieldType, + InvocableMemberBodyBuilder.getInstance().appendFormalLine( + "return " + fieldName + ";")); + } + + protected MethodMetadataBuilder getAccessorMethod( + final JavaSymbolName fieldName, final JavaType fieldType, + final InvocableMemberBodyBuilder bodyBuilder) { + return getMethod(PUBLIC, + BeanInfoUtils.getAccessorMethodName(fieldName, fieldType), + fieldType, null, null, bodyBuilder); + } + + /** + * Convenience method for returning a simple private field based on the + * field name, type, and initializer. + * + * @param fieldName the field name + * @param fieldType the field type + * @param fieldInitializer the string to initialize the field with + * @return null if the field exists on the governor, otherwise a new field + * with the given field name and type + */ + protected FieldMetadataBuilder getField(final int modifier, + final JavaSymbolName fieldName, final JavaType fieldType, + final String fieldInitializer) { + if (governorTypeDetails.getField(fieldName) != null) { + return null; + } + + addToImports(Arrays.asList(fieldType)); + return new FieldMetadataBuilder(getId(), modifier, fieldName, + fieldType, fieldInitializer); + } + + protected FieldMetadataBuilder getField(final JavaSymbolName fieldName, + final JavaType fieldType) { + return getField(PRIVATE, fieldName, fieldType, null); + } + + /** + * Returns the given method of the governor. + * + * @param methodName the name of the method for which to search + * @param parameterTypes the method's parameter types + * @return null if there was no such method + * @see MemberFindingUtils#getDeclaredMethod(org.springframework.roo.classpath.details.MemberHoldingTypeDetails, + * JavaSymbolName, List) + * @since 1.2.0 + */ + protected MethodMetadata getGovernorMethod(final JavaSymbolName methodName, + final JavaType... parameterTypes) { + return getGovernorMethod(methodName, Arrays.asList(parameterTypes)); + } + + /** + * Returns the given method of the governor. + * + * @param methodName the name of the method for which to search + * @param parameterTypes the method's parameter types + * @return null if there was no such method + * @see MemberFindingUtils#getDeclaredMethod(org.springframework.roo.classpath.details.MemberHoldingTypeDetails, + * JavaSymbolName, List) + * @since 1.2.0 (previously called methodExists) + */ + protected MethodMetadata getGovernorMethod(final JavaSymbolName methodName, + final List parameterTypes) { + return MemberFindingUtils.getDeclaredMethod(governorTypeDetails, + methodName, parameterTypes); + } + + public final ItdTypeDetails getMemberHoldingTypeDetails() { + return itdTypeDetails; + } + + /** + * Returns a public method given the method name, return type, parameter + * types, parameter names, and method body. + * + * @param methodName the method name + * @param returnType the return type + * @param parameterTypes a list of parameter types + * @param parameterNames a list of parameter names + * @param bodyBuilder the method body + * @return null if the method exists on the governor, otherwise a new method + * is returned + */ + protected MethodMetadataBuilder getMethod(final int modifier, + final JavaSymbolName methodName, final JavaType returnType, + final List parameterTypes, + final List parameterNames, + final InvocableMemberBodyBuilder bodyBuilder) { + final MethodMetadata method = getGovernorMethod(methodName, + parameterTypes); + if (method != null) { + return null; + } + + addToImports(parameterTypes); + return new MethodMetadataBuilder(getId(), modifier, methodName, + returnType, + AnnotatedJavaType.convertFromJavaTypes(parameterTypes), + parameterNames, bodyBuilder); + } + + protected MethodMetadataBuilder getMutatorMethod( + final JavaSymbolName fieldName, final JavaType parameterType) { + return getMutatorMethod( + fieldName, + parameterType, + InvocableMemberBodyBuilder.getInstance().appendFormalLine( + "this." + fieldName.getSymbolName() + " = " + + fieldName.getSymbolName() + ";")); + } + + protected MethodMetadataBuilder getMutatorMethod( + final JavaSymbolName fieldName, final JavaType parameterType, + final InvocableMemberBodyBuilder bodyBuilder) { + return getMethod(PUBLIC, BeanInfoUtils.getMutatorMethodName(fieldName), + JavaType.VOID_PRIMITIVE, Arrays.asList(parameterType), + Arrays.asList(fieldName), bodyBuilder); + } + + /** + * Returns the metadata for an annotation of the given type if the governor + * does not already have one. + * + * @param annotationType the type of annotation to generate (required) + * @return null if the governor already has that annotation + */ + protected AnnotationMetadata getTypeAnnotation(final JavaType annotationType) { + if (governorTypeDetails.getAnnotation(annotationType) != null) { + return null; + } + return new AnnotationMetadataBuilder(annotationType).build(); + } + + /** + * Indicates whether the governor has a method with the given signature. + * + * @param methodName the name of the method for which to search + * @param parameterTypes the method's parameter types + * @return see above + * @since 1.2.0 + */ + protected boolean governorHasMethod(final JavaSymbolName methodName, + final JavaType... parameterTypes) { + return getGovernorMethod(methodName, parameterTypes) != null; + } + + /** + * Indicates whether the governor has a method with the given signature. + * + * @param methodName the name of the method for which to search + * @param parameterTypes the method's parameter types + * @return see above + * @since 1.2.1 + */ + protected boolean governorHasMethod(final JavaSymbolName methodName, + final List parameterTypes) { + return getGovernorMethod(methodName, parameterTypes) != null; + } + + /** + * Indicates whether the governor has a method with the given method name + * regardless of method parameters. + * + * @param methodName the name of the method for which to search + * @return see above + * @since 1.2.0 + */ + protected boolean governorHasMethodWithSameName( + final JavaSymbolName methodName) { + return MemberFindingUtils.getDeclaredMethod(governorTypeDetails, + methodName) != null; + } + + @Override + public int hashCode() { + return builder.build().hashCode(); + } + + /** + * Determines if the presented class (or any of its superclasses) implements + * the target interface. + * + * @param clazz the cid to search + * @param interfaceTarget the interface to locate + * @return true if the class or any of its superclasses contains the + * specified interface + */ + protected boolean isImplementing(final ClassOrInterfaceTypeDetails clazz, + final JavaType interfaceTarget) { + if (clazz.getImplementsTypes().contains(interfaceTarget)) { + return true; + } + if (clazz.getSuperclass() != null) { + return isImplementing(clazz.getSuperclass(), interfaceTarget); + } + return false; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", valid); + builder.append("aspectName", aspectName); + builder.append("governor", governorPhysicalTypeMetadata.getId()); + builder.append("itdTypeDetails", itdTypeDetails); + return builder.toString(); + } + + /** + * Return current aspect name + * @return + */ + public JavaType getAspectName() { + return aspectName; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractMemberDiscoveringItdMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractMemberDiscoveringItdMetadataProvider.java new file mode 100644 index 000000000..457dfa454 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/AbstractMemberDiscoveringItdMetadataProvider.java @@ -0,0 +1,120 @@ +package org.springframework.roo.classpath.itd; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; + +/** + * Simplifies the development of {@link ItdMetadataProvider}s that wish to + * automatically discover new {@link ItdTypeDetails} that become available + * elsewhere in the system, even if the {@link ItdMetadataProvider} is not + * registered as a downstream dependency. + *

    + * This class helps solves the common requirement of not knowing which other + * add-ons might be providing metadata you are interested in. While + * {@link MemberDetailsScanner} will locate all metadata presently available in + * the system, this is a snapshot of metadata at that moment in time. While it's + * simple (and normal practice) to register as a downstream dependency of + * scanned metadata that you wish to monitor, this approach is insufficient to + * discover new metadata that subsequently becomes available (or even discover + * metadata that previously was available but did not contain members of + * interest at that moment in time and were therefore not registered as a + * dependency to monitor). + *

    + * The practical solution to these problems is to subclass this class, implement + * the abstract {@link #getLocalMidToRequest(ItdTypeDetails)}, and in the + * activate method register a generic listener as described in the documentation + * for {@link #notifyForGenericListener(String)}. This class will then receive + * all generic notifications, determine if they relate to an ITD, extract the + * {@link ItdTypeDetails}, and present it to + * {@link #getLocalMidToRequest(ItdTypeDetails)}. The latter method allows the + * subclass to decide if they want to be formally asked for new metadata, in + * which case this class will do so. Importantly the subclass can decide the + * exact metadata identification string to request, allowing flexibility in + * implementation (eg a subclass could monitor for new ITD metadata related to + * types other than simply their normal governor types). + * + * @author Ben Alex + * @since 1.1.1 + */ +@Component(componentAbstract = true) +public abstract class AbstractMemberDiscoveringItdMetadataProvider extends + AbstractItdMetadataProvider { + + /** + * Allows a subclass to assess a recently updated {@link ItdTypeDetails} and + * decide whether they are interested in a metadata request being made in + * response. Subclasses will generally iterate over the passed ITD details + * and search for members of interest. If any members of interest are + * located, subclasses will return an instance-specific metadata + * identification string (MID) consistent with the subclass' + * {@link #getProvidesType()} The requested MID will be cleared from the + * cache and formally requested. This process allows subclasses to + * effectively discover new ITD members that appear over time without + * needing to process every request themselves. + * + * @param itdTypeDetails a valid {@link ItdTypeDetails} from which member + * information is available (never null) + * @return null if the subclass is not interested in the type, or a MID if + * it is + */ + protected abstract String getLocalMidToRequest(ItdTypeDetails itdTypeDetails); + + /** + * Receives generic notification events arising from our calling of + * MetadataDependencyRegistry.addNotificationListener(this). You must still + * register in the activate method to receive these events, as described in + * the JavaDocs for the superclass method of the same name. + * + * @see AbstractItdMetadataProvider#notifyForGenericListener(String) + */ + @Override + protected final void notifyForGenericListener( + final String upstreamDependency) { + if (MetadataIdentificationUtils.isIdentifyingClass(upstreamDependency)) { + // It's just a class-specific notification (i.e. no instance), so we + // don't care about it + return; + } + + // We have an instance-specific identifier; try to get its metadata + final MetadataItem metadata = getMetadataService().get(upstreamDependency); + + // We don't have to worry about physical type metadata, as we monitor + // the relevant .java once the DOD governor is first detected + if (!(metadata instanceof ItdTypeDetailsProvidingMetadataItem) + || !metadata.isValid()) { + // It's not for an ITD, or there's something wrong with it + return; + } + + // Get the details of the ITD + final ItdTypeDetails itdTypeDetails = ((ItdTypeDetailsProvidingMetadataItem) metadata) + .getMemberHoldingTypeDetails(); + if (itdTypeDetails == null) { + return; + } + + // Ask the subclass if they'd like us to request a MetadataItem in + // response + final String localMid = getLocalMidToRequest(itdTypeDetails); + if (localMid != null) { + Validate.isTrue( + MetadataIdentificationUtils.isIdentifyingInstance(localMid), + "Metadata identification string '%s' should identify a specific instance to request", + localMid); + Validate.isTrue( + MetadataIdentificationUtils.getMetadataClass(localMid) + .equals(MetadataIdentificationUtils + .getMetadataClass(getProvidesType())), + "Metadata identification string '%s' is incompatible with this metadata provider's class '%s'", + MetadataIdentificationUtils.getMetadataClass(localMid), + MetadataIdentificationUtils + .getMetadataClass(getProvidesType())); + getMetadataService().evictAndGet(localMid); + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/InvocableMemberBodyBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/InvocableMemberBodyBuilder.java new file mode 100644 index 000000000..21ded2a79 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/InvocableMemberBodyBuilder.java @@ -0,0 +1,123 @@ +package org.springframework.roo.classpath.itd; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.InvocableMemberMetadata; + +/** + * A simple way of producing method bodies for + * {@link InvocableMemberMetadata#getBody()}. + *

    + * Method bodies immediately assume they are indented two levels. + * + * @author Ben Alex + * @since 1.0 + */ +public class InvocableMemberBodyBuilder { + + public static InvocableMemberBodyBuilder getInstance() { + return new InvocableMemberBodyBuilder(); + } + + private int indentLevel; + private boolean reset; + private final StringBuilder stringBuilder = new StringBuilder(); + + /** + * Constructor for an empty body + */ + public InvocableMemberBodyBuilder() { + indentLevel++; + indentLevel++; + } + + /** + * Prints the message, WITHOUT ANY INDENTATION. + */ + public InvocableMemberBodyBuilder append(final String message) { + if (message != null && !"".equals(message)) { + stringBuilder.append(message); + } + return this; + } + + /** + * Prints the message, after adding indents and returns to a new line. This + * is the most commonly used method. + */ + public InvocableMemberBodyBuilder appendFormalLine(final String message) { + appendIndent(); + if (message != null && !"".equals(message)) { + stringBuilder.append(message); + } + return newLine(false); + } + + /** + * Prints the relevant number of indents. + */ + public InvocableMemberBodyBuilder appendIndent() { + for (int i = 0; i < indentLevel; i++) { + stringBuilder.append(" "); + } + return this; + } + + public String getOutput() { + if (reset) { + Validate.isTrue( + indentLevel == 0, + "Indent level must be 0 (not %d) to terminate following a reset", + indentLevel); + } + else { + Validate.isTrue( + indentLevel == 2, + "Indent level must be 2 (not %d) to terminate (use reset to indent to level 0)", + indentLevel); + } + return stringBuilder.toString(); + } + + /** + * Increases the indent by one level. + */ + public InvocableMemberBodyBuilder indent() { + indentLevel++; + return this; + } + + /** + * Decreases the indent by one level. + */ + public InvocableMemberBodyBuilder indentRemove() { + indentLevel--; + return this; + } + + public InvocableMemberBodyBuilder newLine() { + newLine(true); + return this; + } + + /** + * Prints a blank line, ensuring any indent is included before doing so. + */ + public InvocableMemberBodyBuilder newLine(final boolean indentBefore) { + if (indentBefore) { + appendIndent(); + } + // We use \n for consistency with JavaParser's DumpVisitor, which always + // uses \n + stringBuilder.append("\n"); + return this; + } + + /** + * Resets the indent to zero. + */ + public InvocableMemberBodyBuilder reset() { + indentLevel = 0; + reset = true; + return this; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdFileDeletionService.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdFileDeletionService.java new file mode 100644 index 000000000..32c844bd5 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdFileDeletionService.java @@ -0,0 +1,93 @@ +package org.springframework.roo.classpath.itd; + +import java.io.File; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.file.monitor.event.FileEvent; +import org.springframework.roo.file.monitor.event.FileEventListener; +import org.springframework.roo.process.manager.FileManager; + +/** + * Automatically deletes any ITD that should not exist due to the non-existence + * of the {@link PhysicalTypeMetadata} that the ITD would have been introduced + * into. + *

    + * This service will only delete files matching the syntax *_Roo_*.aj, and then + * only if the leftmost wildcard represents a filename that does not have a + * .java file in the same directory. For example, if the file + * src/main/java/com/foo/Bar_Roo_Hello.aj was detected as existing, it will be + * deleted unless src/main/java/com/foo/Bar.java also presently exists. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class ItdFileDeletionService implements FileEventListener { + + private static String ANT_PATH_ALL_ITD_SOURCE = "**" + File.separator + + "*_Roo_*.aj"; + private static String ANT_PATH_ALL_JAVA_SOURCE = "**" + File.separator + + "*.java"; + + static { + if ("/".equals(File.separator)) { + // This is a *nix box and thus starts all paths with a slash + // (ROO-34) + ANT_PATH_ALL_ITD_SOURCE = File.separator + ANT_PATH_ALL_ITD_SOURCE; + ANT_PATH_ALL_JAVA_SOURCE = File.separator + + ANT_PATH_ALL_JAVA_SOURCE; + } + } + + @Reference private FileManager fileManager; + + public void onFileEvent(final FileEvent fileEvent) { + Validate.notNull(fileEvent, "File event required"); + if (fileEvent.getFileDetails().matchesAntPath(ANT_PATH_ALL_ITD_SOURCE)) { + // It's a ROO ITD, but check it really exists + if (!fileEvent.getFileDetails().getFile().exists()) { + return; + } + // It exists, so compute the governor filename + final String path = fileEvent.getFileDetails().getCanonicalPath(); + final int lastIndex = path.lastIndexOf("_Roo_"); + final String governorName = path.substring(0, lastIndex) + ".java"; + if (!fileManager.exists(governorName)) { + // We just checked the disk, and the governor does not exist, so + // blow away the ITD + fileManager.delete(fileEvent.getFileDetails() + .getCanonicalPath(), fileEvent.getFileDetails() + .getFile().getName() + + " not found"); + } + } + else if (fileEvent.getFileDetails().matchesAntPath( + ANT_PATH_ALL_JAVA_SOURCE)) { + // It's a Java file, but we only cleanup on deletion in this case + if (!fileEvent.getFileDetails().getFile().exists()) { + // Java file was deleted, so let's get rid of any ITDs that are + // laying around + final String governorName = fileEvent.getFileDetails() + .getCanonicalPath(); + final int lastIndex = governorName.lastIndexOf(".java"); + final String itdAntPath = governorName.substring(0, lastIndex) + + "_Roo_*.aj"; + for (final FileDetails itd : fileManager + .findMatchingAntPath(itdAntPath)) { + final String itdCanonicalPath = itd.getCanonicalPath(); + if (fileManager.exists(itdCanonicalPath)) { + fileManager.delete(itdCanonicalPath, fileEvent + .getFileDetails().getFile().getName() + + " not found"); + } + } + } + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdMetadataProvider.java new file mode 100644 index 000000000..7d6db14b3 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdMetadataProvider.java @@ -0,0 +1,95 @@ +package org.springframework.roo.classpath.itd; + +import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.PhysicalTypeMetadataProvider; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.project.Path; + +/** + * Indicates a {@link MetadataProvider} that supports ITDs. + *

    + * ITD usage in ROO adopts some conventions to facilitate ease of use. + *

    + * The first requirement is that metadata identification is by way of the + * {@link PhysicalTypeIdentifierNamingUtils} if the type that will receive any + * potential introduction. It is important to recognize that implementations may + * not actually introduce members via a traditional ITD but instead may place + * the methods directly in the physical type. This is a supported usage pattern + * and is highly compatible with using the aforementioned metadata + * identification approach. + *

    + * The second requirement is that if an implementation wishes to create an ITD + * it does so strictly in accordance with the following compilation unit file + * placement and naming convention. The convention is that all ITD compilation + * units must be placed in the same source directory as the + * {@link PhysicalTypeIdentifierNamingUtils} used for metadata identification. + * Secondly, the ITD compilation unit must adopt an identical filename as that + * used by the {@link PhysicalTypeIdentifierNamingUtils}, except that the + * extension must be ".aj" and there must be a suffix immediately following the + * file name (but prior to the extension). The suffix is composed of "_Roo_" + * plus an implementation-specific string, as returned by + * {@link #getItdUniquenessFilenameSuffix()}. For example, consider a + * {@link PhysicalTypeIdentifierNamingUtils} for com/foo/Bar.java within + * {@link Path#SRC_MAIN_JAVA} and a result of + * {@link #getItdUniquenessFilenameSuffix()} being "Jpa". This would indicate an + * ITD filename within {@link Path#SRC_MAIN_JAVA} of "com/foo/Bar_Roo_Jpa.aj". + *

    + * The third requirement is that implementations can assume + * {@link ItdFileDeletionService} will automatically eliminate any unnecessary + * ITDs that no longer have a {@link PhysicalTypeIdentifierNamingUtils} that + * could potentially receive them. Conversely, if a + * {@link PhysicalTypeIdentifierNamingUtils} does exist, it is required that the + * implementation will delete any unnecessary ITDs if the ITD should no longer + * exist and also monitor that ITD for changes. + *

    + * A recommendation is that implementations listen to + * {@link PhysicalTypeMetadataProvider} so as to be notified of any new + * {@link PhysicalTypeMetadata} that becomes available. Implementations should + * consider whether the {@link PhysicalTypeMetadata} represents an instance that + * should have ITD-specific metadata created. If so, the implementation should + * create a metadata instance and cause that instance to monitor the + * {@link PhysicalTypeMetadata} directly. The {@link ItdMetadataProvider} should + * instantiate an ITD metadata instance with both the + * {@link PhysicalTypeMetadata} it is monitoring, plus + * {@link org.springframework.roo.file.monitor.polling.PollingFileMonitorService} + * for the .aj it should monitor (even if the .aj does not yet exist, because + * the metadata will create it). + * + * @author Ben Alex + * @since 1.0 + */ +public interface ItdMetadataProvider extends MetadataProvider { + + /** + * Obtains an identifier that would be validly recognized by this + * {@link ItdMetadataProvider} instance. The identifier must represent the + * presented physical Java type identifier. + *

    + * The presented physical Java type identifier need not presently exist in + * the {@link MetadataService}. Implementations must not rely on the + * metadata being available at this moment. Implementations by returning a + * value from this method do not guarantee that metadata for the returned + * identifier will subsequently made available. As such this method is a + * basic conversion method and shouldn't perform any analysis. + * + * @param physicalJavaTypeIdentifier to convert into a local metadata + * identifier (required) + * @return an identifier acceptable to this provider (must not return null + * or an empty string) + */ + String getIdForPhysicalJavaType(String physicalJavaTypeIdentifier); + + /** + * Returns the suffix that makes filenames unique for this implementation. + * This suffix is appended to the end of the + * {@link PhysicalTypeIdentifierNamingUtils} filename + "_Roo_" portion. + * This suffix should not contain any periods and as such does not represent + * the filename's extension. + * + * @return the filename suffix that makes ITDs produced by this + * implementation unique (cannot be null or an empty string) + */ + String getItdUniquenessFilenameSuffix(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdSourceFileComposer.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdSourceFileComposer.java new file mode 100644 index 000000000..d5ee8e6cd --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdSourceFileComposer.java @@ -0,0 +1,841 @@ +package org.springframework.roo.classpath.itd; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.details.AnnotationMetadataUtils; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.DeclaredFieldAnnotationDetails; +import org.springframework.roo.classpath.details.DeclaredMethodAnnotationDetails; +import org.springframework.roo.classpath.details.DefaultClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.model.ImportRegistrationResolver; +import org.springframework.roo.model.ImportRegistrationResolverImpl; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * A simple way of producing an inter-type declaration source file. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +public class ItdSourceFileComposer { + + private final JavaType aspect; + private boolean content; + private int indentLevel = 0; + private final JavaType introductionTo; + private final ItdTypeDetails itdTypeDetails; + private StringBuilder pw = new StringBuilder(); + private final ImportRegistrationResolver resolver; + + /** + * Constructs an {@link ItdSourceFileComposer} containing the members that + * were requested in the passed object. + * + * @param itdTypeDetails to construct (required) + */ + public ItdSourceFileComposer(final ItdTypeDetails itdTypeDetails) { + Validate.notNull(itdTypeDetails, "ITD type details required"); + Validate.notNull(itdTypeDetails.getName(), + "Introduction to is required"); + + this.itdTypeDetails = itdTypeDetails; + introductionTo = itdTypeDetails.getName(); + aspect = itdTypeDetails.getAspect(); + + // Create my own resolver, so we can add items to it as we process + resolver = new ImportRegistrationResolverImpl(itdTypeDetails + .getAspect().getPackage()); + resolver.addImport(introductionTo); // ROO-2932 + + for (final JavaType registeredImport : itdTypeDetails + .getRegisteredImports()) { + // Do a sanity check in case the user misused it + if (resolver.isAdditionLegal(registeredImport)) { + resolver.addImport(registeredImport); + } + } + + appendTypeDeclaration(); + appendDeclarePrecedence(); + appendExtendsTypes(); + appendImplementsTypes(); + appendTypeAnnotations(); + appendFieldAnnotations(); + appendMethodAnnotations(); + appendFields(); + appendConstructors(); + appendMethods(itdTypeDetails.getGovernor().getPhysicalTypeCategory() + .equals(PhysicalTypeCategory.INTERFACE)); + appendInnerTypes(); + appendTerminator(); + + // Now prepend the package declaration and any imports + // We need to do this ** at the end ** so we can ensure our compilation + // unit imports are correct, as they're built as we traverse over the + // other members + prependCompilationUnitDetails(); + } + + /** + * Prints the message, WITHOUT ANY INDENTATION. + */ + private ItdSourceFileComposer append(final String message) { + if (message != null && !"".equals(message)) { + pw.append(message); + content = true; + } + return this; + } + + private void appendConstructors() { + final List constructors = itdTypeDetails + .getDeclaredConstructors(); + if (constructors == null || constructors.isEmpty()) { + return; + } + + content = true; + + for (final ConstructorMetadata constructor : constructors) { + Validate.isTrue( + constructor.getParameterTypes().size() == constructor + .getParameterNames().size(), + "Mismatched parameter names against parameter types"); + + // Append annotations + for (final AnnotationMetadata annotation : constructor + .getAnnotations()) { + appendIndent(); + outputAnnotation(annotation); + this.newLine(false); + } + + // Append " .new" portion + appendIndent(); + if (constructor.getModifier() != 0) { + append(Modifier.toString(constructor.getModifier())); + append(" "); + } + append(introductionTo.getSimpleTypeName()); + append("."); + append("new"); + + // Append parameter types and names + append("("); + final List parameterTypes = constructor + .getParameterTypes(); + final List parameterNames = constructor + .getParameterNames(); + for (int i = 0; i < parameterTypes.size(); i++) { + final AnnotatedJavaType paramType = parameterTypes.get(i); + final JavaSymbolName paramName = parameterNames.get(i); + for (final AnnotationMetadata methodParameterAnnotation : paramType + .getAnnotations()) { + append(AnnotationMetadataUtils + .toSourceForm(methodParameterAnnotation)); + append(" "); + } + append(paramType.getJavaType().getNameIncludingTypeParameters( + false, resolver)); + append(" "); + append(paramName.getSymbolName()); + if (i < parameterTypes.size() - 1) { + append(", "); + } + } + append(") {"); + this.newLine(false); + indent(); + + // Add body + append(constructor.getBody()); + indentRemove(); + appendFormalLine("}"); + this.newLine(false); + } + } + + private void appendExtendsTypes() { + final List extendsTypes = itdTypeDetails.getExtendsTypes(); + if (extendsTypes == null || extendsTypes.isEmpty()) { + return; + } + + content = true; + + for (final JavaType extendsType : extendsTypes) { + appendIndent(); + append("declare parents: "); + append(introductionTo.getSimpleTypeName()); + append(" extends "); + if (resolver + .isFullyQualifiedFormRequiredAfterAutoImport(extendsType)) { + append(extendsType.getNameIncludingTypeParameters()); + } + else { + append(extendsType.getNameIncludingTypeParameters(false, + resolver)); + } + append(";"); + this.newLine(false); + this.newLine(); + } + } + + private void appendDeclarePrecedence() { + final Set aspects = itdTypeDetails.getDeclarePrecedence(); + if (aspects == null || aspects.isEmpty()) { + return; + } + + content = true; + + appendIndent(); + append("declare precedence: "); + + List aspectNames = new ArrayList(aspects.size()); + + for (final JavaType aspect : aspects) { + if (resolver + .isFullyQualifiedFormRequiredAfterAutoImport(aspect)) { + aspectNames.add(aspect.getNameIncludingTypeParameters()); + } + else { + aspectNames.add(aspect.getNameIncludingTypeParameters(false, + resolver)); + } + } + append(StringUtils.join(aspectNames, ", ")); + append(";"); + this.newLine(false); + this.newLine(); + } + + private void appendFieldAnnotations() { + final List fieldAnnotations = itdTypeDetails + .getFieldAnnotations(); + if (fieldAnnotations == null || fieldAnnotations.isEmpty()) { + return; + } + + content = true; + + for (final DeclaredFieldAnnotationDetails fieldDetails : fieldAnnotations) { + appendIndent(); + append("declare @field: * "); + append(introductionTo.getSimpleTypeName()); + append("."); + append(fieldDetails.getField().getFieldName().getSymbolName()); + append(": "); + if (fieldDetails.isRemoveAnnotation()) { + append("-"); + } + outputAnnotation(fieldDetails.getFieldAnnotation()); + append(";"); + this.newLine(false); + this.newLine(); + } + } + + private void appendFields() { + final List fields = itdTypeDetails + .getDeclaredFields(); + if (fields == null || fields.isEmpty()) { + return; + } + + content = true; + for (final FieldMetadata field : fields) { + // Append annotations + for (final AnnotationMetadata annotation : field.getAnnotations()) { + appendIndent(); + outputAnnotation(annotation); + this.newLine(false); + } + + // Append " " portion + appendIndent(); + if (field.getModifier() != 0) { + append(Modifier.toString(field.getModifier())); + append(" "); + } + append(field.getFieldType().getNameIncludingTypeParameters(false, + resolver)); + append(" "); + append(introductionTo.getSimpleTypeName()); + append("."); + append(field.getFieldName().getSymbolName()); + + // Append initializer, if present + if (field.getFieldInitializer() != null) { + append(" = "); + append(field.getFieldInitializer()); + } + + // Complete the field declaration + append(";"); + this.newLine(false); + this.newLine(); + } + } + + /** + * Prints the message, after adding indents and returns to a new line. This + * is the most commonly used method. + */ + private ItdSourceFileComposer appendFormalLine(final String message) { + appendIndent(); + if (message != null && !"".equals(message)) { + pw.append(message); + content = true; + } + return newLine(false); + } + + private void appendImplementsTypes() { + final List implementsTypes = itdTypeDetails + .getImplementsTypes(); + if (implementsTypes == null || implementsTypes.isEmpty()) { + return; + } + + content = true; + + for (final JavaType extendsType : implementsTypes) { + appendIndent(); + append("declare parents: "); + append(introductionTo.getSimpleTypeName()); + append(" implements "); + if (resolver + .isFullyQualifiedFormRequiredAfterAutoImport(extendsType)) { + append(extendsType.getNameIncludingTypeParameters()); + } + else { + append(extendsType.getNameIncludingTypeParameters(false, + resolver)); + } + append(";"); + this.newLine(false); + this.newLine(); + } + } + + /** + * Prints the relevant number of indents. + */ + private ItdSourceFileComposer appendIndent() { + for (int i = 0; i < indentLevel; i++) { + pw.append(" "); + } + return this; + } + + /** + * supports static inner types with static field definitions only at this + * point + */ + private void appendInnerTypes() { + final List innerTypes = itdTypeDetails + .getInnerTypes(); + + for (final ClassOrInterfaceTypeDetails innerType : innerTypes) { + content = true; + appendIndent(); + if (innerType.getModifier() != 0) { + append(Modifier.toString(innerType.getModifier())); + append(" "); + } + append("class "); + append(introductionTo.getNameIncludingTypeParameters()); + append("."); + append(innerType.getName().getSimpleTypeName()); + if (innerType.getExtendsTypes().size() > 0) { + append(" extends "); + // There should only be one extends type for inner classes + final JavaType extendsType = innerType.getExtendsTypes().get(0); + if (resolver + .isFullyQualifiedFormRequiredAfterAutoImport(extendsType)) { + append(extendsType.getNameIncludingTypeParameters()); + } + else { + append(extendsType.getNameIncludingTypeParameters(false, + resolver)); + } + append(" "); + } + final List implementsTypes = innerType + .getImplementsTypes(); + if (implementsTypes.size() > 0) { + append(" implements "); + for (int i = 0; i < implementsTypes.size(); i++) { + final JavaType implementsType = implementsTypes.get(i); + if (resolver + .isFullyQualifiedFormRequiredAfterAutoImport(implementsType)) { + append(implementsType.getNameIncludingTypeParameters()); + } + else { + append(implementsType.getNameIncludingTypeParameters( + false, resolver)); + } + if (i != implementsTypes.size() - 1) { + append(", "); + } + else { + append(" "); + } + } + } + append("{"); + this.newLine(false); + + // Write out fields + for (final FieldMetadata field : innerType.getDeclaredFields()) { + indent(); + this.newLine(false); + + // Append annotations + for (final AnnotationMetadata annotation : field + .getAnnotations()) { + appendIndent(); + outputAnnotation(annotation); + this.newLine(false); + } + appendIndent(); + if (field.getModifier() != 0) { + append(Modifier.toString(field.getModifier())); + append(" "); + } + append(field.getFieldType().getNameIncludingTypeParameters( + false, resolver)); + append(" "); + append(field.getFieldName().getSymbolName()); + + // Append initializer, if present + if (field.getFieldInitializer() != null) { + append(" = "); + append(field.getFieldInitializer()); + } + + // Complete the field declaration + append(";"); + this.newLine(false); + indentRemove(); + } + this.newLine(false); + + // Write out constructors + indent(); + writeInnerTypeConstructors(innerType.getName(), innerType.getDeclaredConstructors(), false, false); + indentRemove(); + + // Write out methods + indent(); + writeMethods(innerType.getDeclaredMethods(), false, false); + indentRemove(); + + appendIndent(); + append("}"); + this.newLine(false); + this.newLine(); + } + } + + private void appendMethodAnnotations() { + final List methodAnnotations = itdTypeDetails + .getMethodAnnotations(); + if (methodAnnotations == null || methodAnnotations.isEmpty()) { + return; + } + + content = true; + + for (final DeclaredMethodAnnotationDetails methodDetails : methodAnnotations) { + appendIndent(); + append("declare @method: "); + append(Modifier.toString(methodDetails.getMethodMetadata() + .getModifier())); + append(" "); + append(methodDetails.getMethodMetadata().getReturnType() + .getNameIncludingTypeParameters()); + append(" "); + append(introductionTo.getSimpleTypeName()); + append("."); + append(methodDetails.getMethodMetadata().getMethodName() + .getSymbolName()); + append("("); + for (int i = 0; i < methodDetails.getMethodMetadata() + .getParameterTypes().size(); i++) { + append(methodDetails.getMethodMetadata().getParameterTypes() + .get(i).getJavaType() + .getNameIncludingTypeParameters(false, resolver)); + if (i != methodDetails.getMethodMetadata().getParameterTypes() + .size() - 1) { + append(","); + } + } + append("): "); + outputAnnotation(methodDetails.getMethodAnnotation()); + append(";"); + this.newLine(false); + this.newLine(); + } + } + + private void appendMethods(final boolean interfaceMethod) { + final List methods = itdTypeDetails + .getDeclaredMethods(); + if (methods == null || methods.isEmpty()) { + return; + } + + content = true; + writeMethods(methods, true, interfaceMethod); + } + + private void appendTerminator() { + Validate.isTrue(indentLevel == 1, + "Indent level must be 1 (not %d) to conclude", indentLevel); + indentRemove(); + + // Ensure we present the content flag, as it will be set true during the + // formal line append + final boolean contentBefore = content; + appendFormalLine("}"); + content = contentBefore; + + } + + private void appendTypeAnnotations() { + final List typeAnnotations = itdTypeDetails + .getAnnotations(); + if (typeAnnotations == null || typeAnnotations.isEmpty()) { + return; + } + + content = true; + + for (final AnnotationMetadata typeAnnotation : typeAnnotations) { + appendIndent(); + append("declare @type: "); + append(introductionTo.getSimpleTypeName()); + append(": "); + outputAnnotation(typeAnnotation); + append(";"); + this.newLine(false); + this.newLine(); + } + } + + private void appendTypeDeclaration() { + Validate.isTrue( + introductionTo.getPackage().equals(aspect.getPackage()), + "Aspect and introduction must be in identical packages"); + + appendIndent(); + if (itdTypeDetails.isPrivilegedAspect()) { + append("privileged "); + } + append("aspect " + aspect.getSimpleTypeName() + " {"); + this.newLine(false); + indent(); + this.newLine(); + + // Set to false, as it was set true during the above operations + content = false; + } + + private String getNewLine() { + // We use \n for consistency with JavaParser's DumpVisitor, which always + // uses \n + return "\n"; + } + + public String getOutput() { + return pw.toString(); + } + + /** + * Increases the indent by one level. + */ + private ItdSourceFileComposer indent() { + indentLevel++; + return this; + } + + /** + * Decreases the indent by one level. + */ + private ItdSourceFileComposer indentRemove() { + indentLevel--; + return this; + } + + /** + * Indicates whether any content was added to the ITD, aside from the formal + * ITD declaration. + * + * @return true if there is actual content in the ITD, false otherwise + */ + public boolean isContent() { + return content; + } + + /** + * Prints a blank line, ensuring any indent is included before doing so. + */ + private ItdSourceFileComposer newLine() { + return newLine(true); + } + + /** + * Prints a blank line, ensuring any indent is included before doing so. + */ + private ItdSourceFileComposer newLine(final boolean indent) { + if (indent) { + appendIndent(); + } + // We use \n for consistency with JavaParser's DumpVisitor, which always + // uses \n + pw.append(getNewLine()); + // pw.append(StringUtils.LINE_SEPARATOR); + return this; + } + + private void outputAnnotation(final AnnotationMetadata annotation) { + append(AnnotationMetadataUtils.toSourceForm(annotation, resolver)); + } + + private void prependCompilationUnitDetails() { + final StringBuilder topOfFile = new StringBuilder(); + + topOfFile + .append("// WARNING: DO NOT EDIT THIS FILE. THIS FILE IS MANAGED BY SPRING ROO.") + .append(getNewLine()); + topOfFile + .append("// You may push code into the target .java compilation unit if you wish to edit any member(s).") + .append(getNewLine()).append(getNewLine()); + + // Note we're directly interacting with the top of file string builder + if (!aspect.isDefaultPackage()) { + topOfFile.append("package ") + .append(aspect.getPackage().getFullyQualifiedPackageName()) + .append(";").append(getNewLine()); + topOfFile.append(getNewLine()); + } + + // Ordered to ensure consistency of output + final SortedSet types = new TreeSet(); + types.addAll(resolver.getRegisteredImports()); + if (!types.isEmpty()) { + for (final JavaType importType : types) { + if (introductionTo.equals(importType.getEnclosingType())) { + // We don't "import" types defined within governor, as they + // already have scope and this causes AJDT warnings (see + // ROO-1686) + continue; + } + topOfFile.append("import ") + .append(importType.getFullyQualifiedTypeName()) + .append(";").append(getNewLine()); + } + + topOfFile.append(getNewLine()); + } + + // Now append the normal file to the bottom + topOfFile.append(pw.toString()); + + // Replace the old writer with out new writer + pw = topOfFile; + } + + private void writeMethods(final List methods, + final boolean defineTarget, final boolean isInterfaceMethod) { + for (final MethodMetadata method : methods) { + Validate.isTrue( + method.getParameterTypes().size() == method + .getParameterNames().size(), + "Method %s has mismatched parameter names against parameter types", + method.getMethodName().getSymbolName()); + + // Append annotations + for (final AnnotationMetadata annotation : method.getAnnotations()) { + appendIndent(); + outputAnnotation(annotation); + this.newLine(false); + } + + // Append " " portion + appendIndent(); + if (method.getModifier() != 0) { + append(Modifier.toString(method.getModifier())); + append(" "); + } + + // return type + final boolean staticMethod = Modifier + .isStatic(method.getModifier()); + append(method.getReturnType().getNameIncludingTypeParameters( + staticMethod, resolver)); + append(" "); + if (defineTarget) { + append(introductionTo.getSimpleTypeName()); + append("."); + } + append(method.getMethodName().getSymbolName()); + + // Append parameter types and names + append("("); + final List parameterTypes = method + .getParameterTypes(); + final List parameterNames = method + .getParameterNames(); + for (int i = 0; i < parameterTypes.size(); i++) { + final AnnotatedJavaType paramType = parameterTypes.get(i); + final JavaSymbolName paramName = parameterNames.get(i); + for (final AnnotationMetadata methodParameterAnnotation : paramType + .getAnnotations()) { + outputAnnotation(methodParameterAnnotation); + append(" "); + } + append(paramType.getJavaType().getNameIncludingTypeParameters( + false, resolver)); + append(" "); + append(paramName.getSymbolName()); + if (i < parameterTypes.size() - 1) { + append(", "); + } + } + + // Add exceptions to be thrown + final List throwsTypes = method.getThrowsTypes(); + if (throwsTypes.size() > 0) { + append(") throws "); + for (int i = 0; i < throwsTypes.size(); i++) { + append(throwsTypes.get(i).getNameIncludingTypeParameters( + false, resolver)); + if (throwsTypes.size() > i + 1) { + append(", "); + } + } + } + else { + append(")"); + } + + if (isInterfaceMethod) { + append(";"); + } + else { + append(" {"); + this.newLine(false); + + // Add body + indent(); + append(method.getBody()); + indentRemove(); + + appendFormalLine("}"); + } + this.newLine(); + } + } + + private void writeInnerTypeConstructors(final JavaType innerType, final List constructors, + final boolean defineTarget, final boolean isInterfaceMethod) { + for (final ConstructorMetadata constructor : constructors) { + Validate.isTrue( + constructor.getParameterTypes().size() == constructor + .getParameterNames().size(), + "One constructor has mismatched parameter names against parameter types"); + + // Append annotations + for (final AnnotationMetadata annotation : constructor.getAnnotations()) { + appendIndent(); + outputAnnotation(annotation); + this.newLine(false); + } + + // Append " " portion + appendIndent(); + if (constructor.getModifier() != 0) { + append(Modifier.toString(constructor.getModifier())); + append(" "); + } + + append(innerType.getSimpleTypeName()); + + // Append parameter types and names + append("("); + final List parameterTypes = constructor + .getParameterTypes(); + final List parameterNames = constructor + .getParameterNames(); + for (int i = 0; i < parameterTypes.size(); i++) { + final AnnotatedJavaType paramType = parameterTypes.get(i); + final JavaSymbolName paramName = parameterNames.get(i); + for (final AnnotationMetadata methodParameterAnnotation : paramType + .getAnnotations()) { + outputAnnotation(methodParameterAnnotation); + append(" "); + } + append(paramType.getJavaType().getNameIncludingTypeParameters( + false, resolver)); + append(" "); + append(paramName.getSymbolName()); + if (i < parameterTypes.size() - 1) { + append(", "); + } + } + + // Add exceptions to be thrown + final List throwsTypes = constructor.getThrowsTypes(); + if (throwsTypes.size() > 0) { + append(") throws "); + for (int i = 0; i < throwsTypes.size(); i++) { + append(throwsTypes.get(i).getNameIncludingTypeParameters( + false, resolver)); + if (throwsTypes.size() > i + 1) { + append(", "); + } + } + } + else { + append(")"); + } + + if (isInterfaceMethod) { + append(";"); + } + else { + append(" {"); + this.newLine(false); + + // Add body + indent(); + append(constructor.getBody()); + indentRemove(); + + appendFormalLine("}"); + } + this.newLine(); + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdTriggerBasedMetadataProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdTriggerBasedMetadataProvider.java new file mode 100644 index 000000000..a6325eced --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdTriggerBasedMetadataProvider.java @@ -0,0 +1,16 @@ +package org.springframework.roo.classpath.itd; + +import org.springframework.roo.classpath.TriggerBasedMetadataProvider; +import org.springframework.roo.model.JavaType; + +/** + * An {@link ItdMetadataProvider} that permits registration of different + * {@link JavaType}s as metadata trigger annotations. See + * {@link AbstractItdMetadataProvider} for more information about triggers. + * + * @author Ben Alex + * @since 1.1 + */ +public interface ItdTriggerBasedMetadataProvider extends ItdMetadataProvider, + TriggerBasedMetadataProvider { +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdTypeDetailsProvidingMetadataItem.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdTypeDetailsProvidingMetadataItem.java new file mode 100644 index 000000000..20dac5a47 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/ItdTypeDetailsProvidingMetadataItem.java @@ -0,0 +1,15 @@ +package org.springframework.roo.classpath.itd; + +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.metadata.MetadataItem; + +/** + * Indicates a {@link MetadataItem} implementation that can provide + * {@link ItdTypeDetails}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ItdTypeDetailsProvidingMetadataItem extends + MemberHoldingTypeDetailsMetadataItem { +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/itd/MemberHoldingTypeDetailsMetadataItem.java b/classpath/src/main/java/org/springframework/roo/classpath/itd/MemberHoldingTypeDetailsMetadataItem.java new file mode 100644 index 000000000..6878e4a08 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/itd/MemberHoldingTypeDetailsMetadataItem.java @@ -0,0 +1,34 @@ +package org.springframework.roo.classpath.itd; + +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataItem; + +/** + * Indicates a {@link MetadataItem} capable of returning + * {@link MemberHoldingTypeDetails}. + *

    + * Most Roo metadata builds types, and therefore can provide member information + * via the {@link MemberHoldingTypeDetails} interface. This interface offers a + * convenient way for discovering available member information for iteration + * etc. See for example {@link MemberDetailsScanner} for a common usage pattern. + * + * @author Ben Alex + * @since 1.1.1 + * @param the type of {@link MemberHoldingTypeDetails} that will be provided + */ +public interface MemberHoldingTypeDetailsMetadataItem + extends MetadataItem { + + /** + * Obtains the {@link MemberHoldingTypeDetails}, if available. + *

    + * An {@link MemberHoldingTypeDetails} should be returned even if no members + * should be introduced. Only return null if there was a failure during + * parsing or other unexpected condition. + * + * @return the details, or null if the details are unavailable or no member + * details are required + */ + T getMemberHoldingTypeDetails(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/CoreLayerProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/CoreLayerProvider.java new file mode 100644 index 000000000..eac429328 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/CoreLayerProvider.java @@ -0,0 +1,18 @@ +package org.springframework.roo.classpath.layers; + +/** + * A built-in {@link LayerAdapter}, in other words one that ships with Spring + * Roo. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public abstract class CoreLayerProvider extends LayerAdapter { + + /** + * This implementation returns {@link LayerProvider#CORE_LAYER_PRIORITY} + */ + public int getPriority() { + return CORE_LAYER_PRIORITY; + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerAdapter.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerAdapter.java new file mode 100644 index 000000000..bc51b52b8 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerAdapter.java @@ -0,0 +1,32 @@ +package org.springframework.roo.classpath.layers; + +/** + * Convenience class for addon developers wishing to implement their own + * {@link LayerProvider}. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public abstract class LayerAdapter implements LayerProvider { + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final LayerProvider other = (LayerProvider) obj; + return getLayerPosition() == other.getLayerPosition(); + } + + @Override + public int hashCode() { + return getLayerPosition(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerProvider.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerProvider.java new file mode 100644 index 000000000..5a806bd87 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerProvider.java @@ -0,0 +1,73 @@ +package org.springframework.roo.classpath.layers; + +import org.springframework.roo.model.JavaType; + +/** + * Provides persistence-related methods at a given layer of the application. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface LayerProvider { + + /** + * The priority of the core layers. + */ + int CORE_LAYER_PRIORITY = 0; + + /** + * Returns the position of this layer relative to others. + * + * @return a large number for higher-level layers, a smaller number for + * lower-level layers + */ + int getLayerPosition(); + + /** + * A layer provider should determine if it can provide + * {@link MemberTypeAdditions} for a given target entity and construct it + * accordingly. If it can not provide the requested functionality it should + * simply return null; + * + * @param callerMID the caller's metadata ID + * @param methodIdentifier specifies the method which is being requested + * @param targetEntity specifies the target entity + * @param idType specifies the ID type used by the target entity + * @param autowire specified where the addition should be autowired (if + * applicable) + * @param methodParameters parameters which are passed in to the method + * @return {@link MemberTypeAdditions} if a layer provider can offer this + * functionality, null otherwise + */ + MemberTypeAdditions getMemberTypeAdditions(String callerMID, + String methodIdentifier, JavaType targetEntity, JavaType idType, + boolean autowire, MethodParameter... methodParameters); + + /** + * A layer provider should determine if it can provide + * {@link MemberTypeAdditions} for a given target entity and construct it + * accordingly. If it can not provide the requested functionality it should + * simply return null; + * + * @param callerMID the caller's metadata ID + * @param methodIdentifier specifies the method which is being requested + * @param targetEntity specifies the target entity + * @param idType specifies the ID type used by the target entity + * @param methodParameters parameters which are passed in to the method + * @return {@link MemberTypeAdditions} if a layer provider can offer this + * functionality, null otherwise + */ + MemberTypeAdditions getMemberTypeAdditions(String callerMID, + String methodIdentifier, JavaType targetEntity, JavaType idType, + MethodParameter... methodParameters); + + /** + * Returns the priority of this layer relative to other implementations with + * the same position. + * + * @return a value greater than {@link #CORE_LAYER_PRIORITY} in order to + * take precedence over the core {@link LayerProvider}s + * @see #getLayerPosition() + */ + int getPriority(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerService.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerService.java new file mode 100644 index 000000000..eaa1824ed --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerService.java @@ -0,0 +1,102 @@ +package org.springframework.roo.classpath.layers; + +import java.util.Collection; + +import org.springframework.roo.model.JavaType; + +/** + * Provides upper-layer code (such as MVC, GWT, and tests) with the + * {@link MemberTypeAdditions} they need to make to their source code in order + * to invoke persistence-related operations such as persist and + * find. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public interface LayerService { + + /** + * Returns source code modifications for a requested operation offered by a + * layer provider + * + * @param metadataIdentificationString Id of calling metadata provider + * (required) + * @param methodIdentifier specifies the method which is being requested + * (required) + * @param targetEntity specifies the target entity (required) + * @param idType specifies the ID type used by the target entity (required) + * @param layerPosition the position of the layer invoking this method + * @param methodParameters parameters passed in to the method (types and + * names), if any + * @return {@link MemberTypeAdditions} if a layer provider can offer this + * functionality, null otherwise + */ + MemberTypeAdditions getMemberTypeAdditions( + String metadataIdentificationString, String methodIdentifier, + JavaType targetEntity, JavaType idType, int layerPosition, + Collection methodParameters); + + /** + * Returns source code modifications for a requested operation offered by a + * layer provider + * + * @param metadataIdentificationString Id of calling metadata provider + * (required) + * @param methodIdentifier specifies the method which is being requested + * (required) + * @param targetEntity specifies the target entity (required) + * @param idType specifies the ID type used by the target entity (required) + * @param layerPosition the position of the layer invoking this method + * @param methodParameters parameters passed in to the method (types and + * names), if any + * @return {@link MemberTypeAdditions} if a layer provider can offer this + * functionality, null otherwise + */ + MemberTypeAdditions getMemberTypeAdditions( + String metadataIdentificationString, String methodIdentifier, + JavaType targetEntity, JavaType idType, int layerPosition, + boolean autowire, + Collection methodParameters); + + /** + * Returns source code modifications for a requested operation offered by a + * layer provider + * + * @param metadataIdentificationString Id of calling metadata provider + * (required) + * @param methodIdentifier specifies the method which is being requested + * (required) + * @param targetEntity specifies the target entity (required) + * @param idType specifies the ID type used by the target entity (required) + * @param layerPosition the position of the layer invoking this method + * @param methodParameters parameters passed in to the method (types and + * names), if any + * @return {@link MemberTypeAdditions} if a layer provider can offer this + * functionality, null otherwise + */ + MemberTypeAdditions getMemberTypeAdditions( + String metadataIdentificationString, String methodIdentifier, + JavaType targetEntity, JavaType idType, int layerPosition, + MethodParameter... methodParameters); + + /** + * Returns source code modifications for a requested operation offered by a + * layer provider + * + * @param metadataIdentificationString Id of calling metadata provider + * (required) + * @param methodIdentifier specifies the method which is being requested + * (required) + * @param targetEntity specifies the target entity (required) + * @param idType specifies the ID type used by the target entity (required) + * @param layerPosition the position of the layer invoking this method + * @param methodParameters parameters passed in to the method (types and + * names), if any + * @return {@link MemberTypeAdditions} if a layer provider can offer this + * functionality, null otherwise + */ + MemberTypeAdditions getMemberTypeAdditions( + String metadataIdentificationString, String methodIdentifier, + JavaType targetEntity, JavaType idType, int layerPosition, + boolean autowire, MethodParameter... methodParameters); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerServiceImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerServiceImpl.java new file mode 100644 index 000000000..6ae985078 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerServiceImpl.java @@ -0,0 +1,136 @@ +package org.springframework.roo.classpath.layers; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; + +/** + * The {@link LayerService} implementation. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +@Reference(name = "layerProvider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = LayerProvider.class, cardinality = ReferenceCardinality.MANDATORY_MULTIPLE) +public class LayerServiceImpl implements LayerService { + + /** + * Sorts two {@link LayerProvider}s into descending order of position. + * + * @author Andrew Swan + * @author Stefan Schmidt + * @since 1.2.0 + */ + static class DescendingLayerComparator implements + Comparator, Serializable { + + private static final long serialVersionUID = 2840103254559366403L; + + public int compare(final LayerProvider provider1, + final LayerProvider provider2) { + if (provider1.equals(provider2)) { + return 0; + } + final int difference = provider2.getLayerPosition() + - provider1.getLayerPosition(); + Validate.validState(difference != 0, provider1.getClass() + .getSimpleName() + + " and " + + provider2.getClass().getSimpleName() + + " both have position " + provider1.getLayerPosition()); + return difference; + } + } + + // Mutex + private final Object mutex = new Object(); + + private final SortedSet providers = new TreeSet( + new DescendingLayerComparator()); + + protected void bindLayerProvider(final LayerProvider provider) { + synchronized (mutex) { + providers.add(provider); + } + } + + public MemberTypeAdditions getMemberTypeAdditions( + final String metadataIdentificationString, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final int layerPosition, + final Collection methodParameters) { + final MethodParameter[] methodParametersArray = methodParameters + .toArray(new MethodParameter[methodParameters.size()]); + return getMemberTypeAdditions(metadataIdentificationString, + methodIdentifier, targetEntity, idType, layerPosition, + methodParametersArray); + } + + public MemberTypeAdditions getMemberTypeAdditions( + final String metadataIdentificationString, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final int layerPosition, boolean autowire, + final Collection methodParameters) { + final MethodParameter[] methodParametersArray = methodParameters + .toArray(new MethodParameter[methodParameters.size()]); + return getMemberTypeAdditions(metadataIdentificationString, + methodIdentifier, targetEntity, idType, layerPosition, + autowire, methodParametersArray); + } + + public MemberTypeAdditions getMemberTypeAdditions( + final String metadataIdentificationString, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final int layerPosition, + final MethodParameter... methodParameters) { + return getMemberTypeAdditions(metadataIdentificationString, + methodIdentifier, targetEntity, idType, layerPosition, true, + methodParameters); + } + + public MemberTypeAdditions getMemberTypeAdditions( + final String metadataIdentificationString, + final String methodIdentifier, final JavaType targetEntity, + final JavaType idType, final int layerPosition, + final boolean autowire, final MethodParameter... methodParameters) { + Validate.notBlank(metadataIdentificationString, + "metadataIdentificationString is required"); + Validate.notBlank(methodIdentifier, "methodIdentifier is required"); + Validate.notNull(targetEntity, "targetEntity is required"); + for (final LayerProvider provider : new ArrayList( + providers)) { + if (provider.getLayerPosition() >= layerPosition) { + continue; + } + final MemberTypeAdditions additions = provider + .getMemberTypeAdditions(metadataIdentificationString, + methodIdentifier, targetEntity, idType, autowire, + methodParameters); + if (additions != null) { + return additions; + } + } + return null; + } + + protected void unbindLayerProvider(final LayerProvider provider) { + synchronized (mutex) { + if (providers.contains(provider)) { + providers.remove(provider); + } + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerType.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerType.java new file mode 100644 index 000000000..12340e4fe --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerType.java @@ -0,0 +1,62 @@ +package org.springframework.roo.classpath.layers; + +/** + * Typical layers within a user application. Roo is not limited to these layers + * alone; layer-providing addons can specify any desired integer position in + * order to appear in the correct part of the application architecture relative + * to the core position values shown below. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public enum LayerType { + + /** + * The pattern by which entities provide their own persistence methods. + */ + ACTIVE_RECORD(20), + + /** + * Infrastructure component that provides low-level persistence operations, + * e.g. to a single table of a relational database. Usually implemented via + * a specific persistence technology such as JPA or JDBC. + */ + DAO(40), + + /** + * The ultimate consumer of persistence-related operations, for example the + * application's web or integration test layer. + */ + HIGHEST(100), + + /** + * Domain type that provides collection-like access to instances of + * aggregate roots; implementations are usually persistence agnostic. + */ + REPOSITORY(60), + + /** + * Domain type that implements an application's use-cases. + */ + SERVICE(80); + + private final int position; + + /** + * Constructor + * + * @param position the position of this layer relative to other layers + */ + private LayerType(final int position) { + this.position = position; + } + + /** + * Returns the position of this layer relative to other layers + * + * @return any integer + */ + public int getPosition() { + return position; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerTypeMatcher.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerTypeMatcher.java new file mode 100644 index 000000000..5b19797ec --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/LayerTypeMatcher.java @@ -0,0 +1,76 @@ +package org.springframework.roo.classpath.layers; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.customdata.taggers.AnnotatedTypeMatcher; +import org.springframework.roo.classpath.customdata.taggers.Matcher; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; +import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * A {@link Matcher} used for layering support; identifies layer components + * (services, repositories, etc) by the presence of a given tag, and sets each + * such component's {@link CustomDataKeys#LAYER_TYPE} tag to a list of the + * domain types managed by that component (as a + * List<{@link JavaType}>). + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public class LayerTypeMatcher extends AnnotatedTypeMatcher { + + private final JavaSymbolName domainTypesAttribute; + private final JavaType layerAnnotationType; + + /** + * Constructor + * + * @param layerAnnotation the annotation type to match on and read + * attributes of (required) + * @param domainTypesAttribute the attribute of the above annotation that + * identifies the domain type(s) being managed (required) + */ + public LayerTypeMatcher(final JavaType layerAnnotation, + final JavaSymbolName domainTypesAttribute) { + super(CustomDataKeys.LAYER_TYPE, layerAnnotation); + Validate.notNull(layerAnnotation, "Layer annotation is required"); + Validate.notNull(domainTypesAttribute, + "Domain types attribute is required"); + this.domainTypesAttribute = domainTypesAttribute; + layerAnnotationType = layerAnnotation; + } + + @Override + public Object getTagValue(final MemberHoldingTypeDetails type) { + final AnnotationMetadata layerAnnotation = MemberFindingUtils + .getAnnotationOfType(type.getAnnotations(), layerAnnotationType); + if (layerAnnotation == null + || layerAnnotation.getAttribute(domainTypesAttribute) == null) { + return null; + } + final AnnotationAttributeValue value = layerAnnotation + .getAttribute(domainTypesAttribute); + final List domainTypes = new ArrayList(); + if (value instanceof ClassAttributeValue) { + domainTypes.add(((ClassAttributeValue) value).getValue()); + } + else if (value instanceof ArrayAttributeValue) { + final ArrayAttributeValue castValue = (ArrayAttributeValue) value; + for (final AnnotationAttributeValue val : castValue.getValue()) { + if (val instanceof ClassAttributeValue) { + domainTypes.add(((ClassAttributeValue) val).getValue()); + } + } + } + return domainTypes; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/MemberTypeAdditions.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/MemberTypeAdditions.java new file mode 100644 index 000000000..90c6069dd --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/MemberTypeAdditions.java @@ -0,0 +1,235 @@ +package org.springframework.roo.classpath.layers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.classpath.details.AbstractMemberHoldingTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * The required additions to a given type in order to invoke a given application + * layer method, e.g. findAll(). Instances are immutable. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public class MemberTypeAdditions { + + /** + * Builds the code snippet for a method call with the given properties + * + * @param targetName the name of the object or class on which the method is + * being invoked (if not blank, must be a valid Java name) + * @param methodName the name of the method being invoked (must be a valid + * Java name) + * @param parameterNames the names of any parameters passed to the method + * @return a non-blank Java snippet + */ + static String buildMethodCall(final String targetName, + final String methodName, + final Collection parameters) { + JavaSymbolName.assertJavaNameLegal(methodName); + final StringBuilder methodCall = new StringBuilder(); + if (StringUtils.isNotBlank(targetName)) { + JavaSymbolName.assertJavaNameLegal(targetName); + methodCall.append(targetName); + methodCall.append("."); + } + methodCall.append(methodName); + methodCall.append("("); + for (final Iterator iter = parameters.iterator(); iter + .hasNext();) { + final MethodParameter parameter = iter.next(); + methodCall.append(parameter.getValue()); + if (iter.hasNext()) { + methodCall.append(", "); + } + } + methodCall.append(")"); + return methodCall.toString(); + } + + /** + * Factory method that builds the method call from the given target, method, + * and list of parameter names. + * + * @param builder stores any changes the caller should make in order to make + * the given method call, e.g. the field that is the method + * target (required) + * @param targetName the name of the object or class on which the method is + * being invoked (if not blank, must be a valid Java name) + * @param methodName the name of the method being invoked (must be a valid + * Java name) + * @param isStatic whether the invoked method is static + * @param parameterNames the names of any parameters passed to the method + * (required) + */ + public static MemberTypeAdditions getInstance( + final ClassOrInterfaceTypeDetailsBuilder builder, + final String targetName, final String methodName, + final boolean isStatic, final List parameters) { + return new MemberTypeAdditions(builder, methodName, buildMethodCall( + targetName, methodName, parameters), isStatic, parameters); + } + + /** + * Factory method that builds the method call from the given target, method, + * and array of parameter names. + * + * @param builder stores any changes the caller should make in order to make + * the given method call, e.g. the field that is the method + * target (required) + * @param targetName the name of the object or class on which the method is + * being invoked (if not blank, must be a valid Java name) + * @param methodName the name of the method being invoked (must be a valid + * Java name) + * @param isStatic whether the invoked method is static + * @param parameterNames the names of any parameters passed to the method + * (required) + */ + public static MemberTypeAdditions getInstance( + final ClassOrInterfaceTypeDetailsBuilder builder, + final String targetName, final String methodName, + final boolean isStatic, final MethodParameter... parameters) { + return getInstance(builder, targetName, methodName, isStatic, + Arrays.asList(parameters)); + } + + private final ClassOrInterfaceTypeDetailsBuilder classOrInterfaceDetailsBuilder; + private final boolean isStatic; + + private final String methodCall; + + private final String methodName; + + private final List methodParameters; + + /** + * Constructor that takes a pre-built method call. + * + * @param builder stores any changes the caller should make in order to make + * the given method call, e.g. the field that is the method + * target; can be null if the caller requires no + * changes other than the given method call + * @param methodName the bare name of the method being invoked (required) + * @param methodCall a valid Java snippet that calls the method, including + * any required target and parameters, for example "foo.bar(baz)" + * (required) + * @param isStatic whether the invoked method is static + * @param methodParameters the parameters taken by the invoked method (can + * be null) + */ + public MemberTypeAdditions( + final ClassOrInterfaceTypeDetailsBuilder builder, + final String methodName, final String methodCall, + final boolean isStatic, final List methodParameters) { + Validate.notBlank(methodName, "Invalid method name '%s'", methodName); + Validate.notBlank(methodCall, "Invalid method signature '%s'", + methodCall); + classOrInterfaceDetailsBuilder = builder; + this.methodCall = methodCall; + this.methodName = methodName; + this.methodParameters = new ArrayList(); + CollectionUtils.populate(this.methodParameters, methodParameters); + this.isStatic = isStatic; + } + + /** + * Copies this instance's additions (if any) into the given builder + * + * @param targetBuilder the ITD builder to receive the additions (required) + * @param governorClassOrInterfaceTypeDetails the + * {@link ClassOrInterfaceTypeDetails} of the governor (required) + */ + public void copyAdditionsTo( + final AbstractMemberHoldingTypeDetailsBuilder targetBuilder, + final ClassOrInterfaceTypeDetails governorClassOrInterfaceTypeDetails) { + if (classOrInterfaceDetailsBuilder != null) { + classOrInterfaceDetailsBuilder.copyTo(targetBuilder, + governorClassOrInterfaceTypeDetails); + } + } + + /** + * Returns the field on which this method is invoked + * + * @return null if it's a static method call + * @throws IllegalStateException if there's more than one field in the + * builder + */ + public FieldMetadata getInvokedField() { + if (classOrInterfaceDetailsBuilder == null) { + return null; + } + final List declaredFields = classOrInterfaceDetailsBuilder + .getDeclaredFields(); + switch (declaredFields.size()) { + case 0: + return null; + case 1: + return declaredFields.get(0).build(); + default: + throw new IllegalStateException("Multiple fields introduced for " + + this); + } + } + + /** + * Returns the snippet of Java code that calls the method in question, for + * example "personService.findAll()". + * + * @return a non-blank String + */ + public String getMethodCall() { + return methodCall; + } + + /** + * Returns the bare name of the invoked method + * + * @return a non-blank name + */ + public String getMethodName() { + return methodName; + } + + /** + * Returns the parameters taken by the invoked method + * + * @return a non-null copy of this list + */ + public List getMethodParameters() { + return new ArrayList(methodParameters); + } + + /** + * Indicates whether this is a static method call + * + * @return see above + */ + public boolean isStatic() { + return isStatic; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("classOrInterfaceDetailsBuilder", + classOrInterfaceDetailsBuilder); + builder.append("methodName", methodName); + builder.append("methodCall", methodCall); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/MethodParameter.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/MethodParameter.java new file mode 100644 index 000000000..c80707fc4 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/MethodParameter.java @@ -0,0 +1,98 @@ +package org.springframework.roo.classpath.layers; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * A parameter being passed to a layer method. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MethodParameter extends MutablePair { + + private static final long serialVersionUID = -3652851128787182692L; + + /** + * Converts the given list of pairs to a list of {@link MethodParameter}s + * + * @param parameters the pairs to convert (can be null) + * @return + */ + public static List asList( + final List> parameters) { + final List list = new ArrayList(); + if (parameters != null) { + for (final MutablePair parameter : parameters) { + list.add(new MethodParameter(parameter.getKey(), parameter + .getValue())); + } + } + return list; + } + + private final JavaType type; + + private final JavaSymbolName name; + + /** + * Constructor. + * + * @param type the parameter's type (required) + * @param name the parameter's name (required) + */ + public MethodParameter(final JavaType type, final JavaSymbolName name) { + Validate.notNull(type, "Parameter type is required"); + Validate.notNull(name, "Parameter name is required"); + this.type = type; + this.name = name; + } + + /** + * Constructor + * + * @param type the parameter's type (required) + * @param name the parameter's name (required) + */ + public MethodParameter(final JavaType type, final String name) { + this(type, new JavaSymbolName(name)); + } + + @Override + public JavaType getLeft() { + return type; + } + + @Override + public JavaSymbolName getRight() { + return name; + } + + @Override + public void setLeft(final JavaType left) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRight(final JavaSymbolName right) { + throw new UnsupportedOperationException(); + } + + @Override + public JavaSymbolName setValue(final JavaSymbolName value) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("key: ").append(type.getFullyQualifiedTypeName()); + builder.append(", value: ").append(name.getSymbolName()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/layers/Priority.java b/classpath/src/main/java/org/springframework/roo/classpath/layers/Priority.java new file mode 100644 index 000000000..6e2882707 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/layers/Priority.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath.layers; + +/** + * Priority enum. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +public enum Priority { + HIGH(100), LOW(0), MEDIUM(50); + + private int priority; + + private Priority(final int priority) { + this.priority = priority; + } + + public int getNumericValue() { + return priority; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/AbstractOperations.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/AbstractOperations.java new file mode 100644 index 000000000..93e84d522 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/AbstractOperations.java @@ -0,0 +1,109 @@ +package org.springframework.roo.classpath.operations; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; + +import org.osgi.framework.BundleContext; + +/** + * Abstract base class for operations classes. Contains common methods. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Component(componentAbstract = true) +public abstract class AbstractOperations { + + protected static Logger LOGGER = HandlerUtils + .getLogger(AbstractOperations.class); + + @Reference protected FileManager fileManager; + + protected BundleContext context; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + /** + * This method will copy the contents of a directory to another if the + * resource does not already exist in the target directory + * + * @param sourceAntPath the source path + * @param targetDirectory the target directory + */ + public void copyDirectoryContents(final String sourceAntPath, + String targetDirectory, final boolean replace) { + Validate.notBlank(sourceAntPath, "Source path required"); + Validate.notBlank(targetDirectory, "Target directory required"); + + if (!targetDirectory.endsWith("/")) { + targetDirectory += "/"; + } + + if (!fileManager.exists(targetDirectory)) { + fileManager.createDirectory(targetDirectory); + } + + final String path = FileUtils.getPath(getClass(), sourceAntPath); + final Iterable urls = OSGiUtils.findEntriesByPattern( + context, path); + Validate.notNull(urls, + "Could not search bundles for resources for Ant Path '%s'", + path); + for (final URL url : urls) { + final String fileName = url.getPath().substring( + url.getPath().lastIndexOf("/") + 1); + if (replace) { + try { + String contents = IOUtils.toString(url); + fileManager.createOrUpdateTextFileIfRequired( + targetDirectory + fileName, contents, false); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + else { + if (!fileManager.exists(targetDirectory + fileName)) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = url.openStream(); + outputStream = fileManager.createFile( + targetDirectory + fileName).getOutputStream(); + IOUtils.copy(inputStream, outputStream); + } + catch (final Exception e) { + throw new IllegalStateException( + "Encountered an error during copying of resources for the add-on.", + e); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + } + } + + public Document getDocumentTemplate(final String templateName) { + return XmlUtils.readXml(FileUtils.getInputStream(getClass(), + templateName)); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/Cardinality.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/Cardinality.java new file mode 100644 index 000000000..48e39910c --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/Cardinality.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath.operations; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides cardinality options for "set" relationships. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +public enum Cardinality { + MANY_TO_MANY, MANY_TO_ONE, ONE_TO_MANY, ONE_TO_ONE; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathCommands.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathCommands.java new file mode 100644 index 000000000..e855ec140 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathCommands.java @@ -0,0 +1,93 @@ +package org.springframework.roo.classpath.operations; + +import static org.springframework.roo.shell.OptionContexts.INTERFACE; +import static org.springframework.roo.shell.OptionContexts.SUPERCLASS; +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Shell commands for creating classes, interfaces, and enums. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class ClasspathCommands implements CommandMarker { + + @Reference private ClasspathOperations classpathOperations; + + @CliCommand(value = "class", help = "Creates a new Java class source file in any project path") + public void createClass( + @CliOption(key = "class", optionContext = UPDATE_PROJECT, mandatory = true, help = "The name of the class to create") final JavaType name, + @CliOption(key = "rooAnnotations", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether the generated class should have common Roo annotations") final boolean rooAnnotations, + @CliOption(key = "path", mandatory = false, unspecifiedDefaultValue = "FOCUSED|SRC_MAIN_JAVA", specifiedDefaultValue = "FOCUSED|SRC_MAIN_JAVA", help = "Source directory to create the class in") final LogicalPath path, + @CliOption(key = "extends", mandatory = false, unspecifiedDefaultValue = "java.lang.Object", optionContext = SUPERCLASS, help = "The superclass (defaults to java.lang.Object)") final JavaType superclass, + @CliOption(key = "implements", mandatory = false, optionContext = INTERFACE, help = "The interface to implement") final JavaType implementsType, + @CliOption(key = "abstract", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether the generated class should be marked as abstract") final boolean createAbstract, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + classpathOperations.createClass(name, rooAnnotations, path, superclass, + implementsType, createAbstract, permitReservedWords); + } + + @CliCommand(value = "constructor", help = "Creates a class constructor") + public void createConstructor( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this constructor") final JavaType name, + @CliOption(key = "fields", mandatory = false, specifiedDefaultValue = "", optionContext = "constructor-fields", help = "The fields to include in the constructor. Multiple field names must be a double-quoted list separated by spaces") final Set fields) { + + classpathOperations.createConstructor(name, fields); + } + + @CliCommand(value = "enum type", help = "Creates a new Java enum source file in any project path") + public void createEnum( + @CliOption(key = "class", optionContext = UPDATE_PROJECT, mandatory = true, help = "The name of the enum to create") final JavaType name, + @CliOption(key = "path", mandatory = false, unspecifiedDefaultValue = "FOCUSED|SRC_MAIN_JAVA", specifiedDefaultValue = "FOCUSED|SRC_MAIN_JAVA", help = "Source directory to create the enum in") final LogicalPath path, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + classpathOperations.createEnum(name, path, permitReservedWords); + } + + @CliCommand(value = "interface", help = "Creates a new Java interface source file in any project path") + public void createInterface( + @CliOption(key = "class", optionContext = UPDATE_PROJECT, mandatory = true, help = "The name of the interface to create") final JavaType name, + @CliOption(key = "path", mandatory = false, unspecifiedDefaultValue = "FOCUSED|SRC_MAIN_JAVA", specifiedDefaultValue = "FOCUSED|SRC_MAIN_JAVA", help = "Source directory to create the interface in") final LogicalPath path, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + classpathOperations.createInterface(name, path, permitReservedWords); + } + + @CliCommand(value = "enum constant", help = "Inserts a new enum constant into an enum") + public void enumConstant( + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the enum class to receive this field") final JavaType name, + @CliOption(key = "name", mandatory = true, help = "The name of the constant") final JavaSymbolName fieldName, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + classpathOperations.enumConstant(name, fieldName, permitReservedWords); + } + + @CliCommand(value = "focus", help = "Changes focus to a different type") + public void focus( + @CliOption(key = "class", mandatory = true, optionContext = UPDATE_PROJECT, help = "The type to focus on") final JavaType type) { + classpathOperations.focus(type); + } + + @CliAvailabilityIndicator({ "class", "constructor", "interface", + "enum type", "enum constant" }) + public boolean isProjectAvailable() { + return classpathOperations.isProjectAvailable(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathOperations.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathOperations.java new file mode 100644 index 000000000..60c4f6f9a --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathOperations.java @@ -0,0 +1,86 @@ +package org.springframework.roo.classpath.operations; + +import java.util.Set; + +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; + +/** + * Classpath-related operations + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface ClasspathOperations { + + /** + * Creates a new Java class source file in any project path. + * + * @param name the name of the class to create + * @param rooAnnotations whether the generated class should have common Roo + * annotations + * @param path the source directory in which to create the class + * @param superclass the superclass (defaults to {@link Object}) + * @param implementsType the interface to implement + * @param createAbstract whether the generated class should be marked as + * abstract + * @param permitReservedWords whether reserved words are ignored by Roo + */ + void createClass(final JavaType name, final boolean rooAnnotations, + final LogicalPath path, final JavaType superclass, + final JavaType implementsType, final boolean createAbstract, + final boolean permitReservedWords); + + /** + * Creates a new constructor in the specified class with the fields + * provided. + *

    + * If the set of fields is null, a public no-arg constructor will be created + * if not already present. If fields is not null but empty or if all of the + * supplied fields do not exist in the class, the method returns silently. + * + * @param name the name of the class (required). + * @param fields the fields to include in the constructor. + */ + void createConstructor(final JavaType name, final Set fields); + + /** + * Creates a new Java enum source file in any project path. + * + * @param name the name of the enum to create + * @param path the source directory in which to create the enum + * @param permitReservedWords whether reserved words are ignored by Roo + */ + void createEnum(final JavaType name, final LogicalPath path, + final boolean permitReservedWords); + + /** + * Creates a new Java interface source file in any project path. + * + * @param name the name of the interface to create + * @param path the source directory in which to create the interface + * @param permitReservedWords whether reserved words are ignored by Roo + */ + void createInterface(final JavaType name, final LogicalPath path, + final boolean permitReservedWords); + + /** + * Inserts a new enum constant into an enum. + * + * @param name the enum class to receive this field + * @param fieldName the name of the constant + * @param permitReservedWords whether reserved words are ignored by Roo + */ + void enumConstant(final JavaType name, final JavaSymbolName fieldName, + final boolean permitReservedWords); + + /** + * Changes the focus to the given type. + * + * @param type the type to focus on (required) + */ + void focus(final JavaType type); + + boolean isProjectAvailable(); +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathOperationsImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathOperationsImpl.java new file mode 100644 index 000000000..e182df121 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/ClasspathOperationsImpl.java @@ -0,0 +1,261 @@ +package org.springframework.roo.classpath.operations; + +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.RooJavaType.ROO_EQUALS; +import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN; +import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE; +import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.model.ReservedWords; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * OSGi implementation of {@link ClasspathOperations}. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class ClasspathOperationsImpl implements ClasspathOperations { + + @Reference MetadataService metadataService; + @Reference PathResolver pathResolver; + @Reference ProjectOperations projectOperations; + @Reference StaticFieldConverter staticFieldConverter; + @Reference TypeLocationService typeLocationService; + @Reference TypeManagementService typeManagementService; + + @Override + public void createClass(final JavaType name, final boolean rooAnnotations, + final LogicalPath path, final JavaType superclass, + final JavaType implementsType, final boolean createAbstract, + final boolean permitReservedWords) { + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(name); + } + + Validate.isTrue( + !JdkJavaType.isPartOfJavaLang(name.getSimpleTypeName()), + "Class name '%s' is part of java.lang", + name.getSimpleTypeName()); + + int modifier = Modifier.PUBLIC; + if (createAbstract) { + modifier |= Modifier.ABSTRACT; + } + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, path); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, modifier, name, + PhysicalTypeCategory.CLASS); + + if (!superclass.equals(OBJECT)) { + final ClassOrInterfaceTypeDetails superclassClassOrInterfaceTypeDetails = typeLocationService + .getTypeDetails(superclass); + if (superclassClassOrInterfaceTypeDetails != null) { + cidBuilder + .setSuperclass(new ClassOrInterfaceTypeDetailsBuilder( + superclassClassOrInterfaceTypeDetails)); + } + } + + final List extendsTypes = new ArrayList(); + extendsTypes.add(superclass); + cidBuilder.setExtendsTypes(extendsTypes); + + if (implementsType != null) { + final Set implementsTypes = new LinkedHashSet(); + final ClassOrInterfaceTypeDetails typeDetails = typeLocationService + .getTypeDetails(declaredByMetadataId); + if (typeDetails != null) { + implementsTypes.addAll(typeDetails.getImplementsTypes()); + } + implementsTypes.add(implementsType); + cidBuilder.setImplementsTypes(implementsTypes); + } + + if (rooAnnotations) { + final List annotations = new ArrayList(); + annotations.add(new AnnotationMetadataBuilder(ROO_JAVA_BEAN)); + annotations.add(new AnnotationMetadataBuilder(ROO_TO_STRING)); + annotations.add(new AnnotationMetadataBuilder(ROO_EQUALS)); + annotations.add(new AnnotationMetadataBuilder(ROO_SERIALIZABLE)); + cidBuilder.setAnnotations(annotations); + } + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + @Override + public void createConstructor(final JavaType name, final Set fields) { + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(name); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", + name.getFullyQualifiedTypeName()); + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + final List constructorFields = new ArrayList(); + final List declaredFields = javaTypeDetails + .getDeclaredFields(); + if (fields != null) { + for (final String field : fields) { + declared: for (final FieldMetadata declaredField : declaredFields) { + if (field.equals(declaredField.getFieldName() + .getSymbolName())) { + constructorFields.add(declaredField); + break declared; + } + } + } + if (constructorFields.isEmpty()) { + // User supplied a set of fields that do not exist in the + // class, so return without creating any constructor + return; + } + } + + // Search for an existing constructor + final List parameterTypes = new ArrayList(); + for (final FieldMetadata fieldMetadata : constructorFields) { + parameterTypes.add(fieldMetadata.getFieldType()); + } + + final ConstructorMetadata result = javaTypeDetails + .getDeclaredConstructor(parameterTypes); + if (result != null) { + // Found an existing constructor on this class + return; + } + + final List parameterNames = new ArrayList(); + + final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); + bodyBuilder.appendFormalLine("super();"); + for (final FieldMetadata field : constructorFields) { + final String fieldName = field.getFieldName().getSymbolName(); + bodyBuilder.appendFormalLine("this." + fieldName + " = " + + fieldName + ";"); + parameterNames.add(field.getFieldName()); + } + + // Create the constructor + final ConstructorMetadataBuilder constructorBuilder = new ConstructorMetadataBuilder( + declaredByMetadataId); + constructorBuilder.setModifier(Modifier.PUBLIC); + constructorBuilder.setParameterTypes(AnnotatedJavaType + .convertFromJavaTypes(parameterTypes)); + constructorBuilder.setParameterNames(parameterNames); + constructorBuilder.setBodyBuilder(bodyBuilder); + + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + javaTypeDetails); + cidBuilder.addConstructor(constructorBuilder); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + @Override + public void createEnum(final JavaType name, final LogicalPath path, + final boolean permitReservedWords) { + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(name); + } + final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier( + name, path); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + physicalTypeId, Modifier.PUBLIC, name, + PhysicalTypeCategory.ENUMERATION); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + @Override + public void createInterface(final JavaType name, final LogicalPath path, + final boolean permitReservedWords) { + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(name); + } + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, path); + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + declaredByMetadataId, Modifier.PUBLIC, name, + PhysicalTypeCategory.INTERFACE); + typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); + } + + @Override + public void enumConstant(final JavaType name, + final JavaSymbolName fieldName, final boolean permitReservedWords) { + if (!permitReservedWords) { + // No need to check the "name" as if the class exists it is assumed + // it is a legal name + ReservedWords.verifyReservedWordsNotPresent(fieldName); + } + + final String declaredByMetadataId = PhysicalTypeIdentifier + .createIdentifier(name, + pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); + typeManagementService.addEnumConstant(declaredByMetadataId, fieldName); + } + + @Override + public void focus(final JavaType type) { + Validate.notNull(type, "Specify the type to focus on"); + final String physicalTypeIdentifier = typeLocationService + .getPhysicalTypeIdentifier(type); + Validate.notNull(physicalTypeIdentifier, "Cannot locate the type %s", + type.getFullyQualifiedTypeName()); + final PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService + .get(physicalTypeIdentifier); + Validate.notNull(ptm, "Class %s does not exist", + PhysicalTypeIdentifier.getFriendlyName(physicalTypeIdentifier)); + } + + @Override + public boolean isProjectAvailable() { + return projectOperations.isFocusedProjectAvailable(); + } + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(InheritanceType.class); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(InheritanceType.class); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/DateTime.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/DateTime.java new file mode 100644 index 000000000..1f217e2f2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/DateTime.java @@ -0,0 +1,78 @@ +package org.springframework.roo.classpath.operations; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides date format options for {@link Date} and {@link Calendar} types. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public enum DateTime { + MEDIUM('M'), NONE('-'), SHORT('S'); + // Disabled due to incompatibility between Dojo and JDK dateformat handling + // LONG('L'), FULL('F'); + + /** + * This method will return the DateTime style for the character of the style + * argument. If no style is recognized it will return DateFormat.SHORT. + * + * @param style the date or time style, ie 'S' + * @return the DateTime style. + */ + public static int parseDateFormat(final char style) { + switch (style) { + case 'M': + return DateFormat.MEDIUM; + case 'L': + return DateFormat.LONG; + case 'F': + return DateFormat.FULL; + default: + return DateFormat.SHORT; + } + } + + /** + * This method will return the DateTime style for the character of the style + * argument. For example style of '-' will return DateTime.NULL. + * + * @param style the date or time style, ie 'S' + * @return the DateTime style for the provided style argument + */ + public static DateTime parseDateTimeFormat(final char style) { + switch (style) { + case 'S': + return DateTime.SHORT; + case 'M': + return DateTime.MEDIUM; + // Disabled due to incompatibility between Dojo and JDK dateformat + // handling + // case 'L' : return DateTime.LONG; + // case 'F' : return DateTime.FULL; + } + return DateTime.NONE; + } + + private char shortKey; + + private DateTime(final char shortKey) { + this.shortKey = shortKey; + } + + public char getShortKey() { + return shortKey; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + builder.append("shortKey", shortKey); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/EnumType.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/EnumType.java new file mode 100644 index 000000000..b4c59f655 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/EnumType.java @@ -0,0 +1,20 @@ +package org.springframework.roo.classpath.operations; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides enum types for JPA use. + * + * @author Ben Alex + * @since 1.0 + */ +public enum EnumType { + ORDINAL, STRING; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/Fetch.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/Fetch.java new file mode 100644 index 000000000..c9a47359c --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/Fetch.java @@ -0,0 +1,20 @@ +package org.springframework.roo.classpath.operations; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides fetch type options for "set" relationships. + * + * @author Ben Alex + * @since 1.0 + */ +public enum Fetch { + EAGER, LAZY; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/FieldCommands.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/FieldCommands.java new file mode 100644 index 000000000..61e31cc39 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/FieldCommands.java @@ -0,0 +1,782 @@ +package org.springframework.roo.classpath.operations; + +import static org.springframework.roo.model.JdkJavaType.LIST; +import static org.springframework.roo.model.JdkJavaType.SET; +import static org.springframework.roo.model.JpaJavaType.EMBEDDABLE; +import static org.springframework.roo.model.JpaJavaType.ENTITY; +import static org.springframework.roo.model.SpringJavaType.PERSISTENT; +import static org.springframework.roo.shell.OptionContexts.PROJECT; +import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.classpath.PhysicalTypeDetails; +import org.springframework.roo.classpath.PhysicalTypeMetadata; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.TypeManagementService; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.comments.CommentFormatter; +import org.springframework.roo.classpath.operations.jsr303.BooleanField; +import org.springframework.roo.classpath.operations.jsr303.CollectionField; +import org.springframework.roo.classpath.operations.jsr303.DateField; +import org.springframework.roo.classpath.operations.jsr303.DateFieldPersistenceType; +import org.springframework.roo.classpath.operations.jsr303.EmbeddedField; +import org.springframework.roo.classpath.operations.jsr303.EnumField; +import org.springframework.roo.classpath.operations.jsr303.FieldDetails; +import org.springframework.roo.classpath.operations.jsr303.ListField; +import org.springframework.roo.classpath.operations.jsr303.NumericField; +import org.springframework.roo.classpath.operations.jsr303.ReferenceField; +import org.springframework.roo.classpath.operations.jsr303.SetField; +import org.springframework.roo.classpath.operations.jsr303.StringField; +import org.springframework.roo.classpath.operations.jsr303.UploadedFileContentType; +import org.springframework.roo.classpath.operations.jsr303.UploadedFileField; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.model.ReservedWords; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; + +/** + * Additional shell commands for the purpose of creating fields. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class FieldCommands implements CommandMarker { + + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private MetadataService metadataService; + @Reference private ProjectOperations projectOperations; + @Reference private StaticFieldConverter staticFieldConverter; + @Reference private TypeLocationService typeLocationService; + @Reference private TypeManagementService typeManagementService; + + private final Set legalNumericPrimitives = new HashSet(); + + protected void activate(final ComponentContext context) { + legalNumericPrimitives.add(Short.class.getName()); + legalNumericPrimitives.add(Byte.class.getName()); + legalNumericPrimitives.add(Integer.class.getName()); + legalNumericPrimitives.add(Long.class.getName()); + legalNumericPrimitives.add(Float.class.getName()); + legalNumericPrimitives.add(Double.class.getName()); + staticFieldConverter.add(Cardinality.class); + staticFieldConverter.add(Fetch.class); + staticFieldConverter.add(EnumType.class); + staticFieldConverter.add(DateTime.class); + } + + @CliCommand(value = "field boolean", help = "Adds a private boolean field to an existing Java source file") + public void addFieldBoolean( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "assertFalse", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must assert false") final boolean assertFalse, + @CliOption(key = "assertTrue", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must assert true") final boolean assertTrue, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "value", mandatory = false, help = "Inserts an optional Spring @Value annotation with the given content") final String value, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "primitive", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to use a primitive type") final boolean primitive, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + final BooleanField fieldDetails = new BooleanField( + physicalTypeIdentifier, primitive ? JavaType.BOOLEAN_PRIMITIVE + : JavaType.BOOLEAN_OBJECT, fieldName); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + fieldDetails.setAssertFalse(assertFalse); + fieldDetails.setAssertTrue(assertTrue); + if (column != null) { + fieldDetails.setColumn(column); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + if (value != null) { + fieldDetails.setValue(value); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field date", help = "Adds a private date field to an existing Java source file") + public void addFieldDateJpa( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, optionContext = "java-date", help = "The Java type of the entity") final JavaType fieldType, + @CliOption(key = "persistenceType", mandatory = false, help = "The type of persistent storage to be used") final DateFieldPersistenceType persistenceType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "future", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be in the future") final boolean future, + @CliOption(key = "past", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be in the past") final boolean past, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "value", mandatory = false, help = "Inserts an optional Spring @Value annotation with the given content") final String value, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords, + @CliOption(key = "dateFormat", mandatory = false, unspecifiedDefaultValue = "MEDIUM", specifiedDefaultValue = "MEDIUM", help = "Indicates the style of the date format (ignored if dateTimeFormatPattern is specified)") final DateTime dateFormat, + @CliOption(key = "timeFormat", mandatory = false, unspecifiedDefaultValue = "NONE", specifiedDefaultValue = "NONE", help = "Indicates the style of the time format (ignored if dateTimeFormatPattern is specified)") final DateTime timeFormat, + @CliOption(key = "dateTimeFormatPattern", mandatory = false, help = "Indicates a DateTime format pattern such as yyyy-MM-dd hh:mm:ss a") final String pattern) { + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + final DateField fieldDetails = new DateField(physicalTypeIdentifier, + fieldType, fieldName); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + fieldDetails.setFuture(future); + fieldDetails.setPast(past); + if (JdkJavaType.isDateField(fieldType)) { + fieldDetails + .setPersistenceType(persistenceType != null ? persistenceType + : DateFieldPersistenceType.JPA_TIMESTAMP); + } + if (column != null) { + fieldDetails.setColumn(column); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + if (dateFormat != null) { + fieldDetails.setDateFormat(dateFormat); + } + if (timeFormat != null) { + fieldDetails.setTimeFormat(timeFormat); + } + if (pattern != null) { + fieldDetails.setPattern(pattern); + } + if (value != null) { + fieldDetails.setValue(value); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field embedded", help = "Adds a private @Embedded field to an existing Java source file ") + public void addFieldEmbeddedJpa( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, optionContext = PROJECT, help = "The Java type of the @Embeddable class") final JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the @Entity class to receive this field") final JavaType typeName, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + // Check if the field type is a JPA @Embeddable class + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(fieldType); + Validate.notNull( + cid, + "The specified target '--type' does not exist or can not be found. Please create this type first."); + Validate.notNull(cid.getAnnotation(EMBEDDABLE), + "The field embedded command is only applicable to JPA @Embeddable field types."); + + // Check if the requested entity is a JPA @Entity + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + final PhysicalTypeMetadata targetTypeMetadata = (PhysicalTypeMetadata) metadataService + .get(physicalTypeIdentifier); + Validate.notNull( + targetTypeMetadata, + "The specified target '--class' does not exist or can not be found. Please create this type first."); + final PhysicalTypeDetails targetPtd = targetTypeMetadata + .getMemberHoldingTypeDetails(); + Validate.isInstanceOf(MemberHoldingTypeDetails.class, targetPtd); + + final ClassOrInterfaceTypeDetails targetTypeCid = (ClassOrInterfaceTypeDetails) targetPtd; + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(this.getClass().getName(), targetTypeCid); + Validate.isTrue( + memberDetails.getAnnotation(ENTITY) != null + || memberDetails.getAnnotation(PERSISTENT) != null, + "The field embedded command is only applicable to JPA @Entity or Spring Data @Persistent target types."); + + final EmbeddedField fieldDetails = new EmbeddedField( + physicalTypeIdentifier, fieldType, fieldName); + + insertField(fieldDetails, permitReservedWords, false); + } + + @CliCommand(value = "field enum", help = "Adds a private enum field to an existing Java source file") + public void addFieldEnum( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, help = "The enum type of this field") final JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "enumType", mandatory = false, help = "The fetch semantics at a JPA level") final EnumType enumType, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(cid, "The type specified, '%s', doesn't exist", + typeName); + + final String physicalTypeIdentifier = cid.getDeclaredByMetadataId(); + final EnumField fieldDetails = new EnumField(physicalTypeIdentifier, + fieldType, fieldName); + if (column != null) { + fieldDetails.setColumn(column); + } + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (enumType != null) { + fieldDetails.setEnumType(enumType); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field number", help = "Adds a private numeric field to an existing Java source file") + public void addFieldNumber( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, optionContext = "java-number", help = "The Java type of the entity") JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "decimalMin", mandatory = false, help = "The BigDecimal string-based representation of the minimum value") final String decimalMin, + @CliOption(key = "decimalMax", mandatory = false, help = "The BigDecimal string based representation of the maximum value") final String decimalMax, + @CliOption(key = "digitsInteger", mandatory = false, help = "Maximum number of integral digits accepted for this number") final Integer digitsInteger, + @CliOption(key = "digitsFraction", mandatory = false, help = "Maximum number of fractional digits accepted for this number") final Integer digitsFraction, + @CliOption(key = "min", mandatory = false, help = "The minimum value") final Long min, + @CliOption(key = "max", mandatory = false, help = "The maximum value") final Long max, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "value", mandatory = false, help = "Inserts an optional Spring @Value annotation with the given content") final String value, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "primitive", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to use a primitive type if possible") final boolean primitive, + @CliOption(key = "unique", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether to mark the field with a unique constraint") final boolean unique, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + if (primitive + && legalNumericPrimitives.contains(fieldType + .getFullyQualifiedTypeName())) { + fieldType = new JavaType(fieldType.getFullyQualifiedTypeName(), 0, + DataType.PRIMITIVE, null, null); + } + final NumericField fieldDetails = new NumericField( + physicalTypeIdentifier, fieldType, fieldName); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (decimalMin != null) { + fieldDetails.setDecimalMin(decimalMin); + } + if (decimalMax != null) { + fieldDetails.setDecimalMax(decimalMax); + } + if (digitsInteger != null) { + fieldDetails.setDigitsInteger(digitsInteger); + } + if (digitsFraction != null) { + fieldDetails.setDigitsFraction(digitsFraction); + } + if (min != null) { + fieldDetails.setMin(min); + } + if (max != null) { + fieldDetails.setMax(max); + } + if (column != null) { + fieldDetails.setColumn(column); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + if (unique) { + fieldDetails.setUnique(true); + } + if (value != null) { + fieldDetails.setValue(value); + } + + Validate.isTrue( + fieldDetails.isDigitsSetCorrectly(), + "Must specify both --digitsInteger and --digitsFractional for @Digits to be added"); + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field reference", help = "Adds a private reference field to an existing Java source file (eg the 'many' side of a many-to-one)") + public void addFieldReferenceJpa( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, optionContext = PROJECT, help = "The Java type of the entity to reference") final JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "joinColumnName", mandatory = false, help = "The JPA @JoinColumn name") final String joinColumnName, + @CliOption(key = "referencedColumnName", mandatory = false, help = "The JPA @JoinColumn referencedColumnName") final String referencedColumnName, + @CliOption(key = "cardinality", mandatory = false, unspecifiedDefaultValue = "MANY_TO_ONE", specifiedDefaultValue = "MANY_TO_ONE", help = "The relationship cardinality at a JPA level") final Cardinality cardinality, + @CliOption(key = "fetch", mandatory = false, help = "The fetch semantics at a JPA level") final Fetch fetch, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(fieldType); + Validate.notNull( + cid, + "The specified target '--type' does not exist or can not be found. Please create this type first."); + + // Check if the requested entity is a JPA @Entity + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(this.getClass().getName(), cid); + final AnnotationMetadata entityAnnotation = memberDetails + .getAnnotation(ENTITY); + final AnnotationMetadata persistentAnnotation = memberDetails + .getAnnotation(PERSISTENT); + Validate.isTrue( + entityAnnotation != null || persistentAnnotation != null, + "The field reference command is only applicable to JPA @Entity or Spring Data @Persistent target types."); + + Validate.isTrue(cardinality == Cardinality.MANY_TO_ONE + || cardinality == Cardinality.ONE_TO_ONE, + "Cardinality must be MANY_TO_ONE or ONE_TO_ONE for the field reference command"); + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + final ReferenceField fieldDetails = new ReferenceField( + physicalTypeIdentifier, fieldType, fieldName, cardinality); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (joinColumnName != null) { + fieldDetails.setJoinColumnName(joinColumnName); + } + if (referencedColumnName != null) { + Validate.notNull(joinColumnName, + "@JoinColumn name is required if specifying a referencedColumnName"); + fieldDetails.setReferencedColumnName(referencedColumnName); + } + if (fetch != null) { + fieldDetails.setFetch(fetch); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field set", help = "Adds a private Set field to an existing Java source file (eg the 'one' side of a many-to-one)") + public void addFieldSetJpa( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, help = "The entity which will be contained within the Set") final JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "mappedBy", mandatory = false, help = "The field name on the referenced type which owns the relationship") final JavaSymbolName mappedBy, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "sizeMin", mandatory = false, help = "The minimum number of elements in the collection") final Integer sizeMin, + @CliOption(key = "sizeMax", mandatory = false, help = "The maximum number of elements in the collection") final Integer sizeMax, + @CliOption(key = "cardinality", mandatory = false, unspecifiedDefaultValue = "MANY_TO_MANY", specifiedDefaultValue = "MANY_TO_MANY", help = "The relationship cardinality at a JPA level") Cardinality cardinality, + @CliOption(key = "fetch", mandatory = false, help = "The fetch semantics at a JPA level") final Fetch fetch, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(fieldType); + Validate.notNull( + cid, + "The specified target '--type' does not exist or can not be found. Please create this type first."); + + // Check if the requested entity is a JPA @Entity + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(this.getClass().getName(), cid); + final AnnotationMetadata entityAnnotation = memberDetails + .getAnnotation(ENTITY); + final AnnotationMetadata persistentAnnotation = memberDetails + .getAnnotation(PERSISTENT); + + if (entityAnnotation != null) { + Validate.isTrue(cardinality == Cardinality.ONE_TO_MANY + || cardinality == Cardinality.MANY_TO_MANY, + "Cardinality must be ONE_TO_MANY or MANY_TO_MANY for the field set command"); + } + else if (cid.getPhysicalTypeCategory() == PhysicalTypeCategory.ENUMERATION) { + cardinality = null; + } + else if (persistentAnnotation != null) { + // Yes, we can deal with that + } + else { + throw new IllegalStateException( + "The field set command is only applicable to enum, JPA @Entity or Spring Data @Persistence elements"); + } + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s', doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + final SetField fieldDetails = new SetField(physicalTypeIdentifier, + new JavaType(SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, + null, Arrays.asList(fieldType)), fieldName, fieldType, + cardinality); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (sizeMin != null) { + fieldDetails.setSizeMin(sizeMin); + } + if (sizeMax != null) { + fieldDetails.setSizeMax(sizeMax); + } + if (mappedBy != null) { + fieldDetails.setMappedBy(mappedBy); + } + if (fetch != null) { + fieldDetails.setFetch(fetch); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field list", help = "Adds a private List field to an existing Java source file (eg the 'one' side of a many-to-one)") + public void addFieldListJpa( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, help = "The entity which will be contained within the Set") final JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = "update,project", help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "mappedBy", mandatory = false, help = "The field name on the referenced type which owns the relationship") final JavaSymbolName mappedBy, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "sizeMin", mandatory = false, help = "The minimum number of elements in the collection") final Integer sizeMin, + @CliOption(key = "sizeMax", mandatory = false, help = "The maximum number of elements in the collection") final Integer sizeMax, + @CliOption(key = "cardinality", mandatory = false, unspecifiedDefaultValue = "MANY_TO_MANY", specifiedDefaultValue = "MANY_TO_MANY", help = "The relationship cardinality at a JPA level") Cardinality cardinality, + @CliOption(key = "fetch", mandatory = false, help = "The fetch semantics at a JPA level") final Fetch fetch, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(fieldType); + Validate.notNull( + cid, + "The specified target '--type' does not exist or can not be found. Please create this type first."); + + // Check if the requested entity is a JPA @Entity + final MemberDetails memberDetails = memberDetailsScanner + .getMemberDetails(this.getClass().getName(), cid); + final AnnotationMetadata entityAnnotation = memberDetails + .getAnnotation(ENTITY); + final AnnotationMetadata persistentAnnotation = memberDetails + .getAnnotation(PERSISTENT); + + if (entityAnnotation != null) { + Validate.isTrue(cardinality == Cardinality.ONE_TO_MANY + || cardinality == Cardinality.MANY_TO_MANY, + "Cardinality must be ONE_TO_MANY or MANY_TO_MANY for the field list command"); + } + else if (cid.getPhysicalTypeCategory() == PhysicalTypeCategory.ENUMERATION) { + cardinality = null; + } + else if (persistentAnnotation != null) { + // Yes, we can deal with that + } + else { + throw new IllegalStateException( + "The field list command is only applicable to enum, JPA @Entity or Spring Data @Persistence elements"); + } + + final ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(javaTypeDetails, + "The type specified, '%s' doesn't exist", typeName); + + final String physicalTypeIdentifier = javaTypeDetails + .getDeclaredByMetadataId(); + final ListField fieldDetails = new ListField(physicalTypeIdentifier, + new JavaType(LIST.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, Arrays.asList(fieldType)), + fieldName, fieldType, cardinality); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (sizeMin != null) { + fieldDetails.setSizeMin(sizeMin); + } + if (sizeMax != null) { + fieldDetails.setSizeMax(sizeMax); + } + if (mappedBy != null) { + fieldDetails.setMappedBy(mappedBy); + } + if (fetch != null) { + fieldDetails.setFetch(fetch); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field string", help = "Adds a private string field to an existing Java source file") + public void addFieldString( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the field to add") final JavaSymbolName fieldName, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "decimalMin", mandatory = false, help = "The BigDecimal string-based representation of the minimum value") final String decimalMin, + @CliOption(key = "decimalMax", mandatory = false, help = "The BigDecimal string based representation of the maximum value") final String decimalMax, + @CliOption(key = "sizeMin", mandatory = false, help = "The minimum string length") final Integer sizeMin, + @CliOption(key = "sizeMax", mandatory = false, help = "The maximum string length") final Integer sizeMax, + @CliOption(key = "regexp", mandatory = false, help = "The required regular expression pattern") final String regexp, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "value", mandatory = false, help = "Inserts an optional Spring @Value annotation with the given content") final String value, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "unique", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether to mark the field with a unique constraint") final boolean unique, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords, + @CliOption(key = "lob", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates that this field is a Large Object") final boolean lob) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(cid, "The type specified, '%s', doesn't exist", + typeName); + + final String physicalTypeIdentifier = cid.getDeclaredByMetadataId(); + final StringField fieldDetails = new StringField( + physicalTypeIdentifier, fieldName); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (decimalMin != null) { + fieldDetails.setDecimalMin(decimalMin); + } + if (decimalMax != null) { + fieldDetails.setDecimalMax(decimalMax); + } + if (sizeMin != null) { + fieldDetails.setSizeMin(sizeMin); + } + if (sizeMax != null) { + fieldDetails.setSizeMax(sizeMax); + } + if (regexp != null) { + fieldDetails.setRegexp(regexp.replace("\\", "\\\\")); + } + if (column != null) { + fieldDetails.setColumn(column); + } + if (comment != null) { + fieldDetails.setComment(comment); + } + if (unique) { + fieldDetails.setUnique(true); + } + if (value != null) { + fieldDetails.setValue(value); + } + + if (lob) { + fieldDetails.getInitedAnnotations().add( + new AnnotationMetadataBuilder("javax.persistence.Lob")); + } + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliCommand(value = "field file", help = "Adds a byte array field for storing uploaded file contents (JSF-scaffolded UIs only)") + public void addFileUploadField( + @CliOption(key = { "", "fieldName" }, mandatory = true, help = "The name of the file upload field to add") final JavaSymbolName fieldName, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "contentType", mandatory = true, help = "The content type of the file") final UploadedFileContentType contentType, + @CliOption(key = "autoUpload", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Whether the file is uploaded automatically when selected") final boolean autoUpload, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(cid, "The type specified, '%s', doesn't exist", + typeName); + + final String physicalTypeIdentifier = cid.getDeclaredByMetadataId(); + final UploadedFileField fieldDetails = new UploadedFileField( + physicalTypeIdentifier, fieldName, contentType); + fieldDetails.setAutoUpload(autoUpload); + fieldDetails.setNotNull(notNull); + if (column != null) { + fieldDetails.setColumn(column); + } + + insertField(fieldDetails, permitReservedWords, false); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(Cardinality.class); + staticFieldConverter.remove(Fetch.class); + staticFieldConverter.remove(EnumType.class); + staticFieldConverter.remove(DateTime.class); + } + + private void insertField(final FieldDetails fieldDetails, + final boolean permitReservedWords, final boolean transientModifier) { + if (!permitReservedWords) { + ReservedWords.verifyReservedWordsNotPresent(fieldDetails + .getFieldName()); + if (fieldDetails.getColumn() != null) { + ReservedWords.verifyReservedWordsNotPresent(fieldDetails + .getColumn()); + } + } + + final List annotations = fieldDetails + .getInitedAnnotations(); + fieldDetails.decorateAnnotationsList(annotations); + fieldDetails.setAnnotations(annotations); + + String initializer = null; + if (fieldDetails instanceof CollectionField) { + final CollectionField collectionField = (CollectionField) fieldDetails; + initializer = "new " + collectionField.getInitializer() + "()"; + } + else if (fieldDetails instanceof DateField + && fieldDetails.getFieldName().getSymbolName() + .equals("created")) { + initializer = "new Date()"; + } + int modifier = Modifier.PRIVATE; + if (transientModifier) { + modifier += Modifier.TRANSIENT; + } + fieldDetails.setModifiers(modifier); + + // Format the passed-in comment (if given) + formatFieldComment(fieldDetails); + + final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( + fieldDetails); + fieldBuilder.setFieldInitializer(initializer); + typeManagementService.addField(fieldBuilder.build()); + } + + private void formatFieldComment(FieldDetails fieldDetails) { + // If a comment was defined, we need to format it + if (fieldDetails.getComment() != null) { + + // First replace all "" with the proper escape sequence \" + String unescapedMultiLineComment = fieldDetails.getComment() + .replaceAll("\"\"", "\\\\\""); + + // Then unescape all characters + unescapedMultiLineComment = StringEscapeUtils + .unescapeJava(unescapedMultiLineComment); + + CommentFormatter commentFormatter = new CommentFormatter(); + String javadocComment = commentFormatter + .formatStringAsJavadoc(unescapedMultiLineComment); + + fieldDetails.setComment(commentFormatter.format(javadocComment, 1)); + } + } + + @CliCommand(value = "field other", help = "Inserts a private field into the specified file") + public void insertField( + @CliOption(key = "fieldName", mandatory = true, help = "The name of the field") final JavaSymbolName fieldName, + @CliOption(key = "type", mandatory = true, help = "The Java type of this field") final JavaType fieldType, + @CliOption(key = "class", mandatory = false, unspecifiedDefaultValue = "*", optionContext = UPDATE_PROJECT, help = "The name of the class to receive this field") final JavaType typeName, + @CliOption(key = "notNull", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value cannot be null") final boolean notNull, + @CliOption(key = "nullRequired", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Whether this value must be null") final boolean nullRequired, + @CliOption(key = "comment", mandatory = false, help = "An optional comment for JavaDocs") final String comment, + @CliOption(key = "column", mandatory = false, help = "The JPA @Column name") final String column, + @CliOption(key = "value", mandatory = false, help = "Inserts an optional Spring @Value annotation with the given content") final String value, + @CliOption(key = "transient", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates to mark the field as transient") final boolean transientModifier, + @CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true", help = "Indicates whether reserved words are ignored by Roo") final boolean permitReservedWords) { + + final ClassOrInterfaceTypeDetails cid = typeLocationService + .getTypeDetails(typeName); + Validate.notNull(cid, "The type specified, '%s', doesn't exist", + typeName); + + final String physicalTypeIdentifier = cid.getDeclaredByMetadataId(); + final FieldDetails fieldDetails = new FieldDetails( + physicalTypeIdentifier, fieldType, fieldName); + fieldDetails.setNotNull(notNull); + fieldDetails.setNullRequired(nullRequired); + if (comment != null) { + fieldDetails.setComment(comment); + } + if (column != null) { + fieldDetails.setColumn(column); + } + + insertField(fieldDetails, permitReservedWords, transientModifier); + } + + @CliAvailabilityIndicator({ "field other", "field number", "field string", + "field date", "field boolean", "field enum", "field embedded", + "field file" }) + public boolean isJdkFieldManagementAvailable() { + return projectOperations.isFocusedProjectAvailable(); + } + + @CliAvailabilityIndicator({ "field reference", "field set", "field list" }) + public boolean isJpaFieldManagementAvailable() { + // In a separate method in case we decide to check for JPA registration + // in the future + return projectOperations.isFocusedProjectAvailable(); + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/HintCommands.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintCommands.java new file mode 100644 index 000000000..ce9248633 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintCommands.java @@ -0,0 +1,28 @@ +package org.springframework.roo.classpath.operations; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Shell commands for hinting services. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class HintCommands implements CommandMarker { + + @Reference private HintOperations hintOperations; + + @CliCommand(value = "hint", help = "Provides step-by-step hints and context-sensitive guidance") + public String hint( + @CliOption(key = { "topic", "" }, mandatory = false, unspecifiedDefaultValue = "", optionContext = "disable-string-converter,topics", help = "The topic for which advice should be provided") final String topic) { + + return hintOperations.hint(topic); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/HintConverter.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintConverter.java new file mode 100644 index 000000000..8f61cea78 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintConverter.java @@ -0,0 +1,44 @@ +package org.springframework.roo.classpath.operations; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link String} that understands the "topics" option + * context. + * + * @author Ben Alex + * @since 1.1 + */ +@Service +@Component +public class HintConverter implements Converter { + + @Reference private HintOperations hintOperations; + + public String convertFromText(final String value, + final Class requiredType, final String optionContext) { + return value; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + for (final String currentTopic : hintOperations.getCurrentTopics()) { + completions.add(new Completion(currentTopic)); + } + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return String.class.isAssignableFrom(requiredType) + && optionContext.contains("topics"); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/HintOperations.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintOperations.java new file mode 100644 index 000000000..ff3e291b2 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintOperations.java @@ -0,0 +1,10 @@ +package org.springframework.roo.classpath.operations; + +import java.util.SortedSet; + +public interface HintOperations { + + SortedSet getCurrentTopics(); + + String hint(String topic); +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/HintOperationsImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintOperationsImpl.java new file mode 100644 index 000000000..b1d73efba --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/HintOperationsImpl.java @@ -0,0 +1,112 @@ +package org.springframework.roo.classpath.operations; + +import java.io.File; +import java.math.BigDecimal; +import java.util.Enumeration; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.shell.AbstractShell; + +/** + * Base implementation of {@link HintOperations}. + *

    + * This implementation relies on a predefined resource bundle containing all of + * this hints. This is likely to be replaced in the future with a more + * extensible implementation so third-party add-ons can provide their own hints + * (see ROO-610 for details). + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.1 + */ +@Service +@Component +public class HintOperationsImpl implements HintOperations { + + private static final String ANT_MATCH_DIRECTORY_PATTERN = File.separator + + "**" + File.separator; + private static ResourceBundle bundle = ResourceBundle + .getBundle(HintCommands.class.getName()); + + @Reference private FileManager fileManager; + @Reference private PathResolver pathResolver; + @Reference private ProjectOperations projectOperations; + + private String determineTopic() { + if (!projectOperations.isFocusedProjectAvailable()) { + return "start"; + } + + if (!(fileManager.exists(pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, "META-INF/persistence.xml")) || fileManager + .exists(pathResolver.getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, + "META-INF/spring/applicationContext-mongo.xml")))) { + return "persistence"; + } + + if (new BigDecimal(NumberUtils.max(getItdCount("Jpa_ActiveRecord"), + getItdCount("Jpa_Entity"), getItdCount("Mongo_Entity"))) + .compareTo(BigDecimal.ZERO) == 0) { + return "entities"; + } + + final int javaBeanCount = getItdCount("JavaBean"); + if (javaBeanCount == 0) { + return "fields"; + } + + return "general"; + } + + public SortedSet getCurrentTopics() { + final SortedSet result = new TreeSet(); + final String topic = determineTopic(); + if ("general".equals(topic)) { + for (final Enumeration keys = bundle.getKeys(); keys + .hasMoreElements();) { + result.add(keys.nextElement()); + } + // result.addAll(bundle.keySet()); ResourceBundle.keySet() method in + // JDK 6+ + } + else { + result.add(topic); + } + return result; + } + + private int getItdCount(final String itdUniquenessFilenameSuffix) { + return fileManager.findMatchingAntPath( + pathResolver.getFocusedRoot(Path.SRC_MAIN_JAVA) + + ANT_MATCH_DIRECTORY_PATTERN + "*_Roo_" + + itdUniquenessFilenameSuffix + ".aj").size(); + } + + public String hint(String topic) { + if (StringUtils.isBlank(topic)) { + topic = determineTopic(); + } + try { + final String message = bundle.getString(topic); + return message.replace("\r", IOUtils.LINE_SEPARATOR).replace( + "${completion_key}", AbstractShell.completionKeys); + } + catch (final MissingResourceException exception) { + return "Cannot find topic '" + topic + "'"; + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/InheritanceType.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/InheritanceType.java new file mode 100644 index 000000000..325f3f85f --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/InheritanceType.java @@ -0,0 +1,20 @@ +package org.springframework.roo.classpath.operations; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides inheritance type for JPA entities. + * + * @author Ben Alex + * @since 1.0 + */ +public enum InheritanceType { + JOINED, SINGLE_TABLE, TABLE_PER_CLASS; + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name()); + return builder.toString(); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/BooleanField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/BooleanField.java new file mode 100644 index 000000000..c9da21be7 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/BooleanField.java @@ -0,0 +1,52 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.Jsr303JavaType.ASSERT_FALSE; +import static org.springframework.roo.model.Jsr303JavaType.ASSERT_TRUE; + +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public class BooleanField extends FieldDetails { + + /** Whether the JSR 303 @AssertFalse annotation will be added */ + private boolean assertFalse; + + /** Whether the JSR 303 @AssertTrue annotation will be added */ + private boolean assertTrue; + + public BooleanField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + if (assertTrue) { + annotations.add(new AnnotationMetadataBuilder(ASSERT_TRUE)); + } + if (assertFalse) { + annotations.add(new AnnotationMetadataBuilder(ASSERT_FALSE)); + } + } + + public boolean isAssertFalse() { + return assertFalse; + } + + public boolean isAssertTrue() { + return assertTrue; + } + + public void setAssertFalse(final boolean assertFalse) { + this.assertFalse = assertFalse; + } + + public void setAssertTrue(final boolean assertTrue) { + this.assertTrue = assertTrue; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/CollectionField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/CollectionField.java new file mode 100644 index 000000000..27eadd97e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/CollectionField.java @@ -0,0 +1,85 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.Jsr303JavaType.SIZE; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public abstract class CollectionField extends FieldDetails { + + /** The generic type that will be used within the collection */ + private JavaType genericParameterTypeName; + + /** + * Whether the JSR 303 @Size annotation will be added; provides the "max" + * attribute (defaults to {@link Integer#MAX_VALUE}) + */ + private Integer sizeMax; + + /** + * Whether the JSR 303 @Size annotation will be added; provides the "min" + * attribute (defaults to 0) + */ + private Integer sizeMin; + + public CollectionField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName, + final JavaType genericParameterTypeName) { + super(physicalTypeIdentifier, fieldType, fieldName); + Validate.notNull(genericParameterTypeName, + "Generic parameter type name is required"); + this.genericParameterTypeName = genericParameterTypeName; + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + if (sizeMin != null || sizeMax != null) { + final List> attrs = new ArrayList>(); + if (sizeMin != null) { + attrs.add(new IntegerAttributeValue(new JavaSymbolName("min"), + sizeMin)); + } + if (sizeMax != null) { + attrs.add(new IntegerAttributeValue(new JavaSymbolName("max"), + sizeMax)); + } + annotations.add(new AnnotationMetadataBuilder(SIZE, attrs)); + } + } + + public JavaType getGenericParameterTypeName() { + return genericParameterTypeName; + } + + public abstract JavaType getInitializer(); + + public Integer getSizeMax() { + return sizeMax; + } + + public Integer getSizeMin() { + return sizeMin; + } + + public void setGenericParameterTypeName( + final JavaType genericParameterTypeName) { + this.genericParameterTypeName = genericParameterTypeName; + } + + public void setSizeMax(final Integer sizeMax) { + this.sizeMax = sizeMax; + } + + public void setSizeMin(final Integer sizeMin) { + this.sizeMin = sizeMin; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/DateField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/DateField.java new file mode 100644 index 000000000..552c492e6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/DateField.java @@ -0,0 +1,146 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JpaJavaType.TEMPORAL; +import static org.springframework.roo.model.JpaJavaType.TEMPORAL_TYPE; +import static org.springframework.roo.model.Jsr303JavaType.FUTURE; +import static org.springframework.roo.model.Jsr303JavaType.PAST; +import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.operations.DateTime; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * This field can optionally provide the mandatory JSR 220 temporal annotation. + * + * @author Ben Alex + * @since 1.0 + */ +public class DateField extends FieldDetails { + + private DateTime dateFormat; + + /** Whether the JSR 303 @Future annotation will be added */ + private boolean future; + + /** Whether the JSR 303 @Past annotation will be added */ + private boolean past; + + /** + * Custom date formatting through a DateTime pattern such as yyyy/mm/dd + * h:mm:ss a. + */ + private String pattern; + + /** Whether the JSR 220 @Temporal annotation will be added */ + private DateFieldPersistenceType persistenceType; + + private DateTime timeFormat; + + public DateField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + if (past) { + annotations.add(new AnnotationMetadataBuilder(PAST)); + } + if (future) { + annotations.add(new AnnotationMetadataBuilder(FUTURE)); + } + if (persistenceType != null) { + // Add JSR 220 @Temporal annotation + String value = null; + if (persistenceType == DateFieldPersistenceType.JPA_DATE) { + value = "DATE"; + } + else if (persistenceType == DateFieldPersistenceType.JPA_TIME) { + value = "TIME"; + } + else if (persistenceType == DateFieldPersistenceType.JPA_TIMESTAMP) { + value = "TIMESTAMP"; + } + final List> attrs = new ArrayList>(); + attrs.add(new EnumAttributeValue(new JavaSymbolName("value"), + new EnumDetails(TEMPORAL_TYPE, new JavaSymbolName(value)))); + annotations.add(new AnnotationMetadataBuilder(TEMPORAL, attrs)); + } + // Always add a DateTimeFormat annotation + final List> attributes = new ArrayList>(); + if (pattern != null) { + attributes.add(new StringAttributeValue(new JavaSymbolName( + "pattern"), pattern)); + } + else { + final String dateStyle = null != dateFormat ? String + .valueOf(dateFormat.getShortKey()) : "M"; + final String timeStyle = null != timeFormat ? String + .valueOf(timeFormat.getShortKey()) : "-"; + attributes.add(new StringAttributeValue( + new JavaSymbolName("style"), dateStyle + timeStyle)); + } + annotations.add(new AnnotationMetadataBuilder(DATE_TIME_FORMAT, + attributes)); + } + + public DateTime getDateFormat() { + return dateFormat; + } + + public String getPattern() { + return pattern; + } + + public DateFieldPersistenceType getPersistenceType() { + return persistenceType; + } + + public DateTime getTimeFormat() { + return timeFormat; + } + + public boolean isFuture() { + return future; + } + + public boolean isPast() { + return past; + } + + public void setDateFormat(final DateTime dateFormat) { + this.dateFormat = dateFormat; + } + + public void setFuture(final boolean future) { + this.future = future; + } + + public void setPast(final boolean past) { + this.past = past; + } + + public void setPattern(final String pattern) { + this.pattern = pattern; + } + + public void setPersistenceType( + final DateFieldPersistenceType persistenceType) { + this.persistenceType = persistenceType; + } + + public void setTimeFormat(final DateTime timeFormat) { + this.timeFormat = timeFormat; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/DateFieldPersistenceType.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/DateFieldPersistenceType.java new file mode 100644 index 000000000..08dd140f3 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/DateFieldPersistenceType.java @@ -0,0 +1,5 @@ +package org.springframework.roo.classpath.operations.jsr303; + +public enum DateFieldPersistenceType { + JPA_DATE, JPA_TIME, JPA_TIMESTAMP +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/EmbeddedField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/EmbeddedField.java new file mode 100644 index 000000000..4f364e24e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/EmbeddedField.java @@ -0,0 +1,31 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JpaJavaType.EMBEDDED; + +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * This field is intended for use with JSR 220 and will create a @Embedded + * annotation. + * + * @author Alan Stewart + * @since 1.1 + */ +public class EmbeddedField extends FieldDetails { + + public EmbeddedField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + annotations.add(new AnnotationMetadataBuilder(EMBEDDED)); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/EnumField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/EnumField.java new file mode 100644 index 000000000..abd39a68d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/EnumField.java @@ -0,0 +1,58 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JpaJavaType.ENUMERATED; +import static org.springframework.roo.model.JpaJavaType.ENUM_TYPE; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.operations.EnumType; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * This field is intended for use with JSR 220 and will create a @Enumerated + * annotation. + * + * @author Ben Alex + * @since 1.0 + */ +public class EnumField extends FieldDetails { + + private EnumType enumType; + + public EnumField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + final List> attributes = new ArrayList>(); + + if (enumType != null) { + JavaSymbolName value = new JavaSymbolName("ORDINAL"); + if (enumType == EnumType.STRING) { + value = new JavaSymbolName("STRING"); + } + attributes.add(new EnumAttributeValue(new JavaSymbolName("value"), + new EnumDetails(ENUM_TYPE, value))); + } + + annotations.add(new AnnotationMetadataBuilder(ENUMERATED, attributes)); + } + + public EnumType getEnumType() { + return enumType; + } + + public void setEnumType(final EnumType enumType) { + this.enumType = enumType; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/FieldDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/FieldDetails.java new file mode 100644 index 000000000..29a130b03 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/FieldDetails.java @@ -0,0 +1,213 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JpaJavaType.COLUMN; +import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; +import static org.springframework.roo.model.Jsr303JavaType.NULL; +import static org.springframework.roo.model.SpringJavaType.VALUE; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Base class containing common JSR 303 and JSR 220 properties that can be + * auto-generated. + * + * @author Ben Alex + * @since 1.0 + */ +public class FieldDetails { + + /** The JPA @Column value */ + private String column; + + /** Any JavaDoc comments (reserved for future expansion) */ + protected String comment = ""; + + /** The name of the field to be added */ + private final JavaSymbolName fieldName; + + /** The type of field to be added */ + private final JavaType fieldType; + + /** Whether the JSR 303 @NotNull annotation will be added */ + private boolean notNull; + + /** Whether the JSR 303 @Null annotation will be added */ + private boolean nullRequired; + + /** The type that will receive the field */ + private final String physicalTypeIdentifier; + + /** Whether unique = true is added to the @Column annotation */ + private boolean unique; + + /** The Spring @Value value **/ + private String value; + + /** Field Modifiers (e.g. private, transient) */ + private int modifiers; + + /** Contains field annotations */ + private List annotations; + + /** + * Constructor + * + * @param physicalTypeIdentifier + * @param fieldType + * @param fieldName + */ + public FieldDetails(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + Validate.isTrue(PhysicalTypeIdentifier.isValid(physicalTypeIdentifier), + "Destination physical type identifier is invalid"); + Validate.notNull(fieldType, "Field type required"); + Validate.notNull(fieldName, "Field name required"); + this.physicalTypeIdentifier = physicalTypeIdentifier; + this.fieldType = fieldType; + this.fieldName = fieldName; + } + + public void decorateAnnotationsList( + final List annotations) { + Validate.notNull(annotations); + + if (notNull) { + annotations.add(new AnnotationMetadataBuilder(NOT_NULL)); + } + + if (nullRequired) { + annotations.add(new AnnotationMetadataBuilder(NULL)); + } + + AnnotationMetadataBuilder columnBuilder = null; + if (column != null) { + final List> attrs = new ArrayList>(); + attrs.add(new StringAttributeValue(new JavaSymbolName("name"), + column)); + columnBuilder = new AnnotationMetadataBuilder(COLUMN, attrs); + } + if (unique) { + if (columnBuilder != null) { + columnBuilder.addBooleanAttribute("unique", true); + } + else { + final List> attrs = new ArrayList>(); + attrs.add(new BooleanAttributeValue( + new JavaSymbolName("unique"), true)); + columnBuilder = new AnnotationMetadataBuilder(COLUMN, attrs); + } + } + if (value != null) { + final List> attrs = new ArrayList>(); + attrs.add(new StringAttributeValue(new JavaSymbolName("value"), + value)); + annotations.add(new AnnotationMetadataBuilder(VALUE, attrs)); + } + if (fieldName.getSymbolName().equals("created")) { + if (columnBuilder == null) { + columnBuilder = new AnnotationMetadataBuilder(COLUMN); + } + columnBuilder.addBooleanAttribute("updatable", false); + } + + if (columnBuilder != null) { + annotations.add(columnBuilder); + } + } + + public String getColumn() { + return column; + } + + public String getComment() { + return comment; + } + + public JavaSymbolName getFieldName() { + return fieldName; + } + + public JavaType getFieldType() { + return fieldType; + } + + public String getPhysicalTypeIdentifier() { + return physicalTypeIdentifier; + } + + public String getValue() { + return value; + } + + public boolean isNotNull() { + return notNull; + } + + public boolean isNullRequired() { + return nullRequired; + } + + public boolean isUnique() { + return unique; + } + + public int getModifiers() { + return modifiers; + } + + public List getAnnotations() { + return annotations; + } + + public List getInitedAnnotations() { + if (annotations == null) { + annotations = new ArrayList(); + } + return annotations; + } + + public void setColumn(final String column) { + this.column = column; + } + + public void setComment(final String comment) { + if (StringUtils.isNotBlank(comment)) { + this.comment = comment; + } + } + + public void setNotNull(final boolean notNull) { + this.notNull = notNull; + } + + public void setNullRequired(final boolean nullRequired) { + this.nullRequired = nullRequired; + } + + public void setUnique(final boolean unique) { + this.unique = unique; + } + + public void setValue(final String value) { + this.value = value; + } + + public void setModifiers(int modifiers) { + this.modifiers = modifiers; + } + + public void setAnnotations(List annotations) { + this.annotations = annotations; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/ListField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/ListField.java new file mode 100644 index 000000000..d8e36c317 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/ListField.java @@ -0,0 +1,31 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JdkJavaType.ARRAY_LIST; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.operations.Cardinality; +import org.springframework.roo.classpath.operations.jsr303.SetField; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public class ListField extends SetField { + + public ListField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName, + final JavaType genericParameterTypeName, + final Cardinality cardinality) { + super(physicalTypeIdentifier, fieldType, fieldName, + genericParameterTypeName, cardinality); + } + + @Override + public JavaType getInitializer() { + final List params = new ArrayList(); + params.add(getGenericParameterTypeName()); + return new JavaType(ARRAY_LIST.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, params); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/NumericField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/NumericField.java new file mode 100644 index 000000000..ba28b10b1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/NumericField.java @@ -0,0 +1,112 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.Jsr303JavaType.DIGITS; +import static org.springframework.roo.model.Jsr303JavaType.MAX; +import static org.springframework.roo.model.Jsr303JavaType.MIN; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.classpath.details.annotations.LongAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; + +public class NumericField extends StringOrNumericField { + + /** + * Whether the JSR 303 @Digits annotation will be added (you must also set + * digitsInteger) + */ + private Integer digitsFraction; + + /** + * Whether the JSR 303 @Digits annotation will be added (you must also set + * digitsFractional) + */ + private Integer digitsInteger; + + /** Whether the JSR 303 @Max annotation will be added */ + private Long max; + + /** Whether the JSR 303 @Min annotation will be added */ + private Long min; + + public NumericField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + if (min != null) { + final List> attrs = new ArrayList>(); + attrs.add(new LongAttributeValue(new JavaSymbolName("value"), min)); + annotations.add(new AnnotationMetadataBuilder(MIN, attrs)); + } + if (max != null) { + final List> attrs = new ArrayList>(); + attrs.add(new LongAttributeValue(new JavaSymbolName("value"), max)); + annotations.add(new AnnotationMetadataBuilder(MAX, attrs)); + } + Validate.isTrue(isDigitsSetCorrectly(), + "Validation constraints for @Digit are not correctly set"); + if (digitsInteger != null) { + final List> attrs = new ArrayList>(); + attrs.add(new IntegerAttributeValue(new JavaSymbolName("integer"), + digitsInteger)); + attrs.add(new IntegerAttributeValue(new JavaSymbolName("fraction"), + digitsFraction)); + annotations.add(new AnnotationMetadataBuilder(DIGITS, attrs)); + } + } + + public Integer getDigitsFraction() { + return digitsFraction; + } + + public Integer getDigitsInteger() { + return digitsInteger; + } + + public Long getMax() { + return max; + } + + public Long getMin() { + return min; + } + + public boolean isDigitsSetCorrectly() { + return digitsInteger == null && digitsFraction == null + || digitsInteger != null && digitsFraction != null; + } + + public void setDigitsFraction(final Integer digitsFractional) { + digitsFraction = digitsFractional; + } + + public void setDigitsInteger(final Integer digitsInteger) { + this.digitsInteger = digitsInteger; + } + + public void setMax(final Long max) { + if (JdkJavaType.isDoubleOrFloat(getFieldType())) { + LOGGER.warning("@Max constraint is not supported for double or float fields"); + } + this.max = max; + } + + public void setMin(final Long min) { + if (JdkJavaType.isDoubleOrFloat(getFieldType())) { + LOGGER.warning("@Min constraint is not supported for double or float fields"); + } + this.min = min; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/ReferenceField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/ReferenceField.java new file mode 100644 index 000000000..d0e2ad29b --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/ReferenceField.java @@ -0,0 +1,126 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JpaJavaType.FETCH_TYPE; +import static org.springframework.roo.model.JpaJavaType.JOIN_COLUMN; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.operations.Cardinality; +import org.springframework.roo.classpath.operations.Fetch; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Properties used by the many-to-one side of a relationship (called a + * "reference"). + *

    + * For example, an Order-LineItem link would have the LineItem contain a + * "reference" back to Order. + *

    + * Limited support for collection mapping is provided. This reflects the + * pragmatic goals of ROO and the fact a user can edit the generated files by + * hand anyway. + *

    + * This field is intended for use with JSR 220 and will create a @ManyToOne and @JoinColumn + * annotation. + * + * @author Ben Alex + * @since 1.0 + */ +public class ReferenceField extends FieldDetails { + + private final Cardinality cardinality; + private Fetch fetch; + private String joinColumnName; + private String referencedColumnName; + + public ReferenceField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName, + final Cardinality cardinality) { + super(physicalTypeIdentifier, fieldType, fieldName); + this.cardinality = cardinality; + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + final List> attributes = new ArrayList>(); + + if (fetch != null) { + JavaSymbolName value = new JavaSymbolName("EAGER"); + if (fetch == Fetch.LAZY) { + value = new JavaSymbolName("LAZY"); + } + attributes.add(new EnumAttributeValue(new JavaSymbolName("fetch"), + new EnumDetails(FETCH_TYPE, value))); + } + + switch (cardinality) { + case ONE_TO_MANY: + annotations.add(new AnnotationMetadataBuilder(ONE_TO_MANY, + attributes)); + break; + case MANY_TO_MANY: + annotations.add(new AnnotationMetadataBuilder(MANY_TO_MANY, + attributes)); + break; + case ONE_TO_ONE: + annotations.add(new AnnotationMetadataBuilder(ONE_TO_ONE, + attributes)); + break; + default: + annotations.add(new AnnotationMetadataBuilder(MANY_TO_ONE, + attributes)); + break; + } + + if (joinColumnName != null) { + final List> joinColumnAttrs = new ArrayList>(); + joinColumnAttrs.add(new StringAttributeValue(new JavaSymbolName( + "name"), joinColumnName)); + + if (referencedColumnName != null) { + joinColumnAttrs.add(new StringAttributeValue( + new JavaSymbolName("referencedColumnName"), + referencedColumnName)); + } + annotations.add(new AnnotationMetadataBuilder(JOIN_COLUMN, + joinColumnAttrs)); + } + } + + public Fetch getFetch() { + return fetch; + } + + public String getJoinColumnName() { + return joinColumnName; + } + + public String getReferencedColumnName() { + return referencedColumnName; + } + + public void setFetch(final Fetch fetch) { + this.fetch = fetch; + } + + public void setJoinColumnName(final String joinColumnName) { + this.joinColumnName = joinColumnName; + } + + public void setReferencedColumnName(final String referencedColumnName) { + this.referencedColumnName = referencedColumnName; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/RooUploadedFile.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/RooUploadedFile.java new file mode 100644 index 000000000..786263680 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/RooUploadedFile.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a field is used for storing uploaded file contents. + * + * @author Alan Stewart + * @since 1.2.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.SOURCE) +public @interface RooUploadedFile { + + boolean autoUpload() default false; + + String contentType(); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/SetField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/SetField.java new file mode 100644 index 000000000..b462633c8 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/SetField.java @@ -0,0 +1,135 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JdkJavaType.HASH_SET; +import static org.springframework.roo.model.JpaJavaType.CASCADE_TYPE; +import static org.springframework.roo.model.JpaJavaType.ELEMENT_COLLECTION; +import static org.springframework.roo.model.JpaJavaType.FETCH_TYPE; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; +import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.classpath.operations.Cardinality; +import org.springframework.roo.classpath.operations.Fetch; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.EnumDetails; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Properties used by the one side of a many-to-one relationship or an @ElementCollection + * of enums (called a "set"). + *

    + * For example, an Order-LineItem link would have the Order contain a "set" of + * Orders. + *

    + * Limited support for collection mapping is provided. This reflects the + * pragmatic goals of the tool and the fact a user can edit the generated files + * by hand anyway. + *

    + * This field is intended for use with JSR 220 and will create a @OneToMany + * annotation or in the case of enums, an @ElementCollection annotation will be + * created. + * + * @author Ben Alex + * @since 1.0 + */ +public class SetField extends CollectionField { + + private final Cardinality cardinality; + + private Fetch fetch; + /** + * Whether the JSR 220 @OneToMany.mappedBy annotation attribute will be + * added + */ + private JavaSymbolName mappedBy; + + public SetField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName, + final JavaType genericParameterTypeName, + final Cardinality cardinality) { + super(physicalTypeIdentifier, fieldType, fieldName, + genericParameterTypeName); + this.cardinality = cardinality; + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + final List> attributes = new ArrayList>(); + + if (cardinality == null) { + // Assume set field is an enum + annotations.add(new AnnotationMetadataBuilder(ELEMENT_COLLECTION)); + } + else { + attributes.add(new EnumAttributeValue( + new JavaSymbolName("cascade"), new EnumDetails( + CASCADE_TYPE, new JavaSymbolName("ALL")))); + if (fetch != null) { + JavaSymbolName value = new JavaSymbolName("EAGER"); + if (fetch == Fetch.LAZY) { + value = new JavaSymbolName("LAZY"); + } + attributes.add(new EnumAttributeValue(new JavaSymbolName( + "fetch"), new EnumDetails(FETCH_TYPE, value))); + } + if (mappedBy != null) { + attributes.add(new StringAttributeValue(new JavaSymbolName( + "mappedBy"), mappedBy.getSymbolName())); + } + + switch (cardinality) { + case ONE_TO_MANY: + annotations.add(new AnnotationMetadataBuilder(ONE_TO_MANY, + attributes)); + break; + case MANY_TO_MANY: + annotations.add(new AnnotationMetadataBuilder(MANY_TO_MANY, + attributes)); + break; + case ONE_TO_ONE: + annotations.add(new AnnotationMetadataBuilder(ONE_TO_ONE, + attributes)); + break; + default: + annotations.add(new AnnotationMetadataBuilder(MANY_TO_ONE, + attributes)); + break; + } + } + } + + public Fetch getFetch() { + return fetch; + } + + @Override + public JavaType getInitializer() { + final List params = new ArrayList(); + params.add(getGenericParameterTypeName()); + return new JavaType(HASH_SET.getFullyQualifiedTypeName(), 0, + DataType.TYPE, null, params); + } + + public JavaSymbolName getMappedBy() { + return mappedBy; + } + + public void setFetch(final Fetch fetch) { + this.fetch = fetch; + } + + public void setMappedBy(final JavaSymbolName mappedBy) { + this.mappedBy = mappedBy; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/StringField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/StringField.java new file mode 100644 index 000000000..186e3458d --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/StringField.java @@ -0,0 +1,97 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.Jsr303JavaType.PATTERN; +import static org.springframework.roo.model.Jsr303JavaType.SIZE; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.IntegerAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Extra validation properties specified to String properties. + * + * @author Ben Alex + * @since 1.0 + */ +public class StringField extends StringOrNumericField { + + /** Whether the JSR 3030 @Pattern annotation will be added */ + private String regexp; + + /** + * Whether the JSR 303 @Size annotation will be added; provides the "max" + * attribute (defaults to {@link Integer#MAX_VALUE}) + */ + private Integer sizeMax; + + /** + * Whether the JSR 303 @Size annotation will be added; provides the "min" + * attribute (defaults to 0) + */ + private Integer sizeMin; + + public StringField(final String physicalTypeIdentifier, + final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, JavaType.STRING, fieldName); + } + + @Deprecated + public StringField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + if (sizeMin != null || sizeMax != null) { + final List> attrs = new ArrayList>(); + if (sizeMin != null) { + attrs.add(new IntegerAttributeValue(new JavaSymbolName("min"), + sizeMin)); + } + if (sizeMax != null) { + attrs.add(new IntegerAttributeValue(new JavaSymbolName("max"), + sizeMax)); + } + annotations.add(new AnnotationMetadataBuilder(SIZE, attrs)); + } + if (regexp != null) { + final List> attrs = new ArrayList>(); + attrs.add(new StringAttributeValue(new JavaSymbolName("regexp"), + regexp)); + annotations.add(new AnnotationMetadataBuilder(PATTERN, attrs)); + } + } + + public String getRegexp() { + return regexp; + } + + public Integer getSizeMax() { + return sizeMax; + } + + public Integer getSizeMin() { + return sizeMin; + } + + public void setRegexp(final String regexp) { + this.regexp = regexp; + } + + public void setSizeMax(final Integer sizeMax) { + this.sizeMax = sizeMax; + } + + public void setSizeMin(final Integer sizeMin) { + this.sizeMin = sizeMin; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/StringOrNumericField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/StringOrNumericField.java new file mode 100644 index 000000000..623a934fc --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/StringOrNumericField.java @@ -0,0 +1,73 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MAX; +import static org.springframework.roo.model.Jsr303JavaType.DECIMAL_MIN; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.model.JdkJavaType; +import org.springframework.roo.support.logging.HandlerUtils; + +public class StringOrNumericField extends FieldDetails { + + protected static final Logger LOGGER = HandlerUtils + .getLogger(StringOrNumericField.class); + + /** Whether the JSR 303 @DecimalMax annotation will be added */ + private String decimalMax; + + /** Whether the JSR 303 @DecimalMin annotation will be added */ + private String decimalMin; + + public StringOrNumericField(final String physicalTypeIdentifier, + final JavaType fieldType, final JavaSymbolName fieldName) { + super(physicalTypeIdentifier, fieldType, fieldName); + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + if (decimalMin != null) { + final List> attrs = new ArrayList>(); + attrs.add(new StringAttributeValue(new JavaSymbolName("value"), + decimalMin)); + annotations.add(new AnnotationMetadataBuilder(DECIMAL_MIN, attrs)); + } + if (decimalMax != null) { + final List> attrs = new ArrayList>(); + attrs.add(new StringAttributeValue(new JavaSymbolName("value"), + decimalMax)); + annotations.add(new AnnotationMetadataBuilder(DECIMAL_MAX, attrs)); + } + } + + public String getDecimalMax() { + return decimalMax; + } + + public String getDecimalMin() { + return decimalMin; + } + + public void setDecimalMax(final String decimalMax) { + if (JdkJavaType.isDoubleOrFloat(getFieldType())) { + LOGGER.warning("@DecimalMax constraint is not supported for double or float fields"); + } + this.decimalMax = decimalMax; + } + + public void setDecimalMin(final String decimalMin) { + if (JdkJavaType.isDoubleOrFloat(getFieldType())) { + LOGGER.warning("@DecimalMin constraint is not supported for double or float fields"); + } + this.decimalMin = decimalMin; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/UploadedFileContentType.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/UploadedFileContentType.java new file mode 100644 index 000000000..61f93304e --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/UploadedFileContentType.java @@ -0,0 +1,40 @@ +package org.springframework.roo.classpath.operations.jsr303; + +/** + * The Internet media type or content-type of an uploaded file. + *

    + * Only common content types are included. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public enum UploadedFileContentType { + CSS("text/css"), CSV("text/csv"), DOC("application/msword"), GIF( + "image/gif"), HTML("text/html"), JAVASCRIPT("text/javascript"), JPG( + "image/jpeg"), JSON("application/json"), MP3("audio/mpeg"), MP4( + "audio/mp4"), MPEG("video/mpeg"), PDF("application/pdf"), PNG( + "image/png"), TXT("text/plain"), XLS("application/vnd.ms-excel"), XML( + "text/xml"), ZIP("application/zip"); + + public static UploadedFileContentType getFileExtension( + final String contentType) { + for (final UploadedFileContentType uploadedFileContentType : UploadedFileContentType + .values()) { + if (uploadedFileContentType.getContentType().equals(contentType)) { + return uploadedFileContentType; + } + } + throw new IllegalStateException("Unknown content type '" + contentType + + "'"); + } + + private String contentType; + + private UploadedFileContentType(final String contentType) { + this.contentType = contentType; + } + + public String getContentType() { + return contentType; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/UploadedFileField.java b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/UploadedFileField.java new file mode 100644 index 000000000..67a729ea1 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/operations/jsr303/UploadedFileField.java @@ -0,0 +1,54 @@ +package org.springframework.roo.classpath.operations.jsr303; + +import static org.springframework.roo.model.JpaJavaType.LOB; +import static org.springframework.roo.model.RooJavaType.ROO_UPLOADED_FILE; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.BooleanAttributeValue; +import org.springframework.roo.classpath.details.annotations.StringAttributeValue; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +public class UploadedFileField extends FieldDetails { + + private boolean autoUpload; + private final UploadedFileContentType contentType; + + public UploadedFileField(final String physicalTypeIdentifier, + final JavaSymbolName fieldName, + final UploadedFileContentType contentType) { + super(physicalTypeIdentifier, JavaType.BYTE_ARRAY_PRIMITIVE, fieldName); + this.contentType = contentType; + } + + @Override + public void decorateAnnotationsList( + final List annotations) { + super.decorateAnnotationsList(annotations); + + final List> attrs = new ArrayList>(); + attrs.add(new StringAttributeValue(new JavaSymbolName("contentType"), + contentType.getContentType())); + + if (autoUpload) { + attrs.add(new BooleanAttributeValue( + new JavaSymbolName("autoUpload"), autoUpload)); + } + + annotations + .add(new AnnotationMetadataBuilder(ROO_UPLOADED_FILE, attrs)); + annotations.add(new AnnotationMetadataBuilder(LOB)); + } + + public UploadedFileContentType getContentType() { + return contentType; + } + + public void setAutoUpload(final boolean autoUpload) { + this.autoUpload = autoUpload; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/persistence/PersistenceMemberLocator.java b/classpath/src/main/java/org/springframework/roo/classpath/persistence/PersistenceMemberLocator.java new file mode 100644 index 000000000..5a126f3bb --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/persistence/PersistenceMemberLocator.java @@ -0,0 +1,67 @@ +package org.springframework.roo.classpath.persistence; + +import java.util.List; + +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.model.JavaType; + +/** + * Provides metadata about persistence-related members of domain types. + * + * @author Stefan Schmidt + * @author Andrew Swan + * @since 1.2.0 + */ +public interface PersistenceMemberLocator { + + /** + * Locates embedded identifier types for a given domain type. + * + * @param domainType The domain type (needs to be part of the project) + * @return a list of identifier fields (not null, may be empty) + */ + List getEmbeddedIdentifierFields(JavaType domainType); + + /** + * Returns the ID accessor for the given domain type + * + * @param domainType the domain type (can be null) + * @return null if the given type is null or does + * not have an ID accessor + */ + MethodMetadata getIdentifierAccessor(JavaType domainType); + + /** + * Returns the identifier fields of the given domain type. + * + * @param domainType The domain type (can be null) + * @return a list of identifier fields (not null, may be empty) + */ + List getIdentifierFields(JavaType domainType); + + /** + * Returns the identifier type of the given domain type. + * + * @param domainType The domain type (can be null) + * @return the identifier type (may be null) + */ + JavaType getIdentifierType(JavaType domainType); + + /** + * Returns the version accessor for the given domain type. + * + * @param domainType the domain type (can be null) + * @return null if the given type is null or does + * not have a version accessor + */ + MethodMetadata getVersionAccessor(JavaType domainType); + + /** + * Locates the version field for a given domain type. + * + * @param domainType The domain type (needs to be part of the project) + * @return a version field (may be null) + */ + FieldMetadata getVersionField(JavaType domainType); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/persistence/PersistenceMemberLocatorImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/persistence/PersistenceMemberLocatorImpl.java new file mode 100644 index 000000000..852f2b2aa --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/persistence/PersistenceMemberLocatorImpl.java @@ -0,0 +1,244 @@ +package org.springframework.roo.classpath.persistence; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.classpath.ItdDiscoveryService; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.scanner.MemberDetails; +import org.springframework.roo.classpath.scanner.MemberDetailsScanner; +import org.springframework.roo.model.JavaType; + +/** + * This implementation of {@link PersistenceMemberLocator} scans for the + * presence of persistence ID tags for {@link MemberDetails} for a given domain + * type. + * + * @author Stefan Schmidt + * @since 1.2.0 + */ +@Component +@Service +public class PersistenceMemberLocatorImpl implements PersistenceMemberLocator { + + @Reference private ItdDiscoveryService itdDiscoveryService; + @Reference private MemberDetailsScanner memberDetailsScanner; + @Reference private TypeLocationService typeLocationService; + + private final Map> domainTypeEmbeddedIdFieldsCache = new HashMap>(); + private final Map domainTypeIdAccessorCache = new HashMap(); + private final Map domainTypeIdCache = new HashMap(); + private final Map> domainTypeIdFieldsCache = new HashMap>(); + private final Map domainTypeVersionAccessorCache = new HashMap(); + private final Map domainTypeVersionFieldCache = new HashMap(); + + public List getEmbeddedIdentifierFields( + final JavaType domainType) { + updateCache(domainType); + if (domainTypeEmbeddedIdFieldsCache.containsKey(domainType)) { + return new ArrayList( + domainTypeEmbeddedIdFieldsCache.get(domainType)); + } + return new ArrayList(); + } + + public MethodMetadata getIdentifierAccessor(final JavaType domainType) { + updateCache(domainType); + return domainTypeIdAccessorCache.get(domainType); + } + + public List getIdentifierFields(final JavaType domainType) { + updateCache(domainType); + if (domainTypeIdFieldsCache.containsKey(domainType)) { + return new ArrayList( + domainTypeIdFieldsCache.get(domainType)); + } + else if (domainTypeEmbeddedIdFieldsCache.containsKey(domainType)) { + return new ArrayList( + domainTypeEmbeddedIdFieldsCache.get(domainType)); + } + + return new ArrayList(); + } + + public JavaType getIdentifierType(final JavaType domainType) { + updateCache(domainType); + if (domainTypeIdCache.containsKey(domainType)) { + return domainTypeIdCache.get(domainType); + } + return null; + } + + private MemberDetails getMemberDetails( + final ClassOrInterfaceTypeDetails typeDetails) { + return memberDetailsScanner.getMemberDetails(getClass().getName(), + typeDetails); + } + + private MemberDetails getMemberDetails(final JavaType type) { + final ClassOrInterfaceTypeDetails typeDetails = typeLocationService + .getTypeDetails(type); + if (typeDetails == null) { + return null; + } + return memberDetailsScanner.getMemberDetails(getClass().getName(), + typeDetails); + } + + public MethodMetadata getVersionAccessor(final JavaType domainType) { + updateCache(domainType); + return domainTypeVersionAccessorCache.get(domainType); + } + + public FieldMetadata getVersionField(final JavaType domainType) { + updateCache(domainType); + return domainTypeVersionFieldCache.get(domainType); + } + + private boolean haveAssociatedTypesChanged(final JavaType javaType) { + return typeLocationService.hasTypeChanged(getClass().getName(), + javaType) + || itdDiscoveryService.haveItdsChanged(getClass().getName(), + javaType); + } + + private void populateEmbeddedIdFields(final MemberDetails details, + final JavaType type) { + final List embeddedIdFields = MemberFindingUtils + .getFieldsWithTag(details, CustomDataKeys.EMBEDDED_ID_FIELD); + if (!embeddedIdFields.isEmpty()) { + domainTypeEmbeddedIdFieldsCache.remove(type); + domainTypeEmbeddedIdFieldsCache.put(type, + new ArrayList()); + final MemberDetails memberDetails = getMemberDetails(embeddedIdFields + .get(0).getFieldType()); + if (memberDetails != null) { + for (final FieldMetadata field : memberDetails.getFields()) { + if (!field.getCustomData().keySet() + .contains(CustomDataKeys.SERIAL_VERSION_UUID_FIELD)) { + domainTypeEmbeddedIdFieldsCache.get(type).add(field); + } + } + } + } + else if (domainTypeEmbeddedIdFieldsCache.containsKey(type)) { + domainTypeEmbeddedIdFieldsCache.remove(type); + } + } + + private void populateIdAccessors(final MemberDetails details, + final JavaType type) { + final MethodMetadata idAccessor = MemberFindingUtils + .getMostConcreteMethodWithTag(details, + CustomDataKeys.IDENTIFIER_ACCESSOR_METHOD); + if (idAccessor != null) { + domainTypeIdAccessorCache.put(type, idAccessor); + } + else if (domainTypeIdAccessorCache.containsKey(type)) { + domainTypeIdAccessorCache.remove(type); + } + } + + private void populateIdFields(final MemberDetails details, + final JavaType type) { + final List idFields = MemberFindingUtils + .getFieldsWithTag(details, CustomDataKeys.IDENTIFIER_FIELD); + final List embeddedIdFields = MemberFindingUtils + .getFieldsWithTag(details, CustomDataKeys.EMBEDDED_ID_FIELD); + if (!idFields.isEmpty()) { + domainTypeIdFieldsCache.put(type, idFields); + } + else if (!embeddedIdFields.isEmpty()) { + domainTypeIdFieldsCache.put(type, embeddedIdFields); + } + else if (domainTypeIdFieldsCache.containsKey(type)) { + domainTypeIdFieldsCache.remove(type); + } + } + + private void populateIdTypes(final MemberDetails details, + final JavaType type) { + final List idFields = MemberFindingUtils + .getFieldsWithTag(details, CustomDataKeys.IDENTIFIER_FIELD); + final List embeddedIdFields = MemberFindingUtils + .getFieldsWithTag(details, CustomDataKeys.EMBEDDED_ID_FIELD); + if (!idFields.isEmpty()) { + domainTypeIdCache.put(type, idFields.get(0).getFieldType()); + } + else if (!embeddedIdFields.isEmpty()) { + domainTypeIdCache.put(type, embeddedIdFields.get(0).getFieldType()); + } + else { + domainTypeIdCache.remove(type); + } + } + + private void populateVersionAccessor(final MemberDetails details, + final JavaType type) { + final MethodMetadata versionAccessor = MemberFindingUtils + .getMostConcreteMethodWithTag(details, + CustomDataKeys.VERSION_ACCESSOR_METHOD); + if (versionAccessor != null) { + domainTypeVersionAccessorCache.put(type, versionAccessor); + } + else if (domainTypeVersionAccessorCache.containsKey(type)) { + domainTypeVersionAccessorCache.remove(type); + } + } + + private void populateVersionField(final MemberDetails details, + final JavaType type) { + final List versionFields = MemberFindingUtils + .getFieldsWithTag(details, CustomDataKeys.VERSION_FIELD); + if (!versionFields.isEmpty()) { + domainTypeVersionFieldCache.put(type, versionFields.get(0)); + } + else if (domainTypeVersionFieldCache.containsKey(type)) { + domainTypeVersionFieldCache.remove(type); + } + } + + private void updateCache(final JavaType domainType) { + if (!haveAssociatedTypesChanged(domainType)) { + return; + } + + final ClassOrInterfaceTypeDetails domainTypeDetails = typeLocationService + .getTypeDetails(domainType); + if (domainTypeDetails == null + || !domainTypeDetails.getCustomData().keySet() + .contains(CustomDataKeys.PERSISTENT_TYPE)) { + return; + } + + final MemberDetails memberDetails = getMemberDetails(domainTypeDetails); + + // Update normal persistence ID fields cache + populateIdTypes(memberDetails, domainType); + + // Update normal persistence ID cache + populateIdFields(memberDetails, domainType); + + // Update embedded ID fields cache + populateEmbeddedIdFields(memberDetails, domainType); + + // Update ID accessor cache + populateIdAccessors(memberDetails, domainType); + + // Update version field cache + populateVersionField(memberDetails, domainType); + + // Update version accessor cache + populateVersionAccessor(memberDetails, domainType); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/preferences/Preferences.java b/classpath/src/main/java/org/springframework/roo/classpath/preferences/Preferences.java new file mode 100644 index 000000000..116dc9885 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/preferences/Preferences.java @@ -0,0 +1,108 @@ +package org.springframework.roo.classpath.preferences; + +import java.io.UnsupportedEncodingException; +import java.util.prefs.BackingStoreException; + +import org.apache.commons.lang3.Validate; + +/** + * A node in the user's tree of preferences. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class Preferences { + + private final java.util.prefs.Preferences preferences; // "Delegate" pattern + + /** + * Constructor for delegating to a Java Preferences instance. + * + * @param preferences the preferences to read and write (required) + */ + public Preferences(final java.util.prefs.Preferences preferences) { + Validate.notNull(preferences, "Delegate preferences are required"); + this.preferences = preferences; + } + + /** + * Flushes any changes in these preferences to the persistent storage. + * + * @throws IllegalStateException if there was a problem + */ + public void flush() { + try { + preferences.flush(); + } + catch (final BackingStoreException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns the byte array with the given key, or if none, an empty array. + * + * @param key the key whose value to retrieve (can be null) + * @return null iff a null key is given + */ + public byte[] getByteArray(final String key) { + return getByteArray(key, null, "ignored"); + } + + /** + * Returns the byte array with the given key, or if none, the given default + * value. + * + * @param key the key whose value to retrieve (can be null) + * @param defaultValue can be null to default to an empty array + * @param characterSetName the name of the character set into which to + * encode the given default value + * @return null iff a null key is given + * @throws UnsupportedOperationException if the given character set is not + * supported + */ + public byte[] getByteArray(final String key, final String defaultValue, + final String characterSetName) { + if (key == null) { + return null; + } + try { + final byte[] defaultBytes = defaultValue == null ? new byte[0] + : defaultValue.getBytes(characterSetName); + return preferences.getByteArray(key, defaultBytes); + } + catch (final UnsupportedEncodingException e) { + throw new UnsupportedOperationException(e); + } + } + + /** + * Returns the int with the given key, or if none, the given default value. + * + * @param key the key whose value to retrieve (can be null) + * @param defaultValue see above + */ + public int getInt(final String key, final int defaultValue) { + return preferences.getInt(key, defaultValue); + } + + /** + * Adds the given byte array under the given key. + * + * @param key + * @param value + */ + public void putByteArray(final String key, final byte[] value) { + preferences.putByteArray(key, value); + } + + /** + * Adds the given int value under the given key. + * + * @param key + * @param value + */ + public void putInt(final String key, final int value) { + preferences.putInt(key, value); + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/preferences/PreferencesService.java b/classpath/src/main/java/org/springframework/roo/classpath/preferences/PreferencesService.java new file mode 100644 index 000000000..c36503756 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/preferences/PreferencesService.java @@ -0,0 +1,23 @@ +package org.springframework.roo.classpath.preferences; + +/** + * Manages user preferences. Use this interface instead of the Java + * {@link java.util.prefs.Preferences} API in order to minimise coupling, both + * for increased testability and to allow for alternative implementations. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface PreferencesService { + + /** + * Returns the user's preferences for the package in which the given class + * resides. + * + * @param owningClass the class for whose package to retrieve the user's + * preferences (required) + * @return a non-null instance + * @see Preferences#userNodeForPackage(Class) + */ + Preferences getPreferencesFor(Class owningClass); +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/preferences/PreferencesServiceImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/preferences/PreferencesServiceImpl.java new file mode 100644 index 000000000..15ae09a19 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/preferences/PreferencesServiceImpl.java @@ -0,0 +1,36 @@ +package org.springframework.roo.classpath.preferences; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; + +/** + * The {@link PreferencesService} implementation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class PreferencesServiceImpl implements PreferencesService { + + public Preferences getPreferencesFor(final Class owningClass) { + // Create the Preferences object, suppressing + // "Created user preferences directory" messages if there is no Java + // preferences directory + // TODO Switch to UAA's PreferencesUtils (but must wait for UAA 1.0.3 + // due to bug in UAA 1.0.2 and earlier) + final Logger l = Logger.getLogger("java.util.prefs"); + final Level original = l.getLevel(); + try { + l.setLevel(Level.WARNING); + return new Preferences( + java.util.prefs.Preferences.userNodeForPackage(owningClass)); + } + finally { + l.setLevel(original); + } + } +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetails.java b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetails.java new file mode 100644 index 000000000..a211848e6 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetails.java @@ -0,0 +1,209 @@ +package org.springframework.roo.classpath.scanner; + +import java.util.List; +import java.util.Set; + +import org.springframework.roo.classpath.PhysicalTypeDetails; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.IdentifiableJavaStructure; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; + +/** + * Immutable representation of member details scanned at a particular point in + * time. + *

    + * Please note that {@link MemberDetails} as well as all types it eventually + * refers to are all immutable. + *

    + * {@link MemberDetails} represents a convenient, customizable aggregation of + * member information at a particular point in time and for a particular + * {@link MetadataProvider}. It is distinct from other key types as follows: + *

      + *
    • {@link MetadataItem}s are specific to a given add-on. They also + * frequently reflect a single AspectJ ITD.
    • + *
    • {@link PhysicalTypeDetails} instances only contain details of a parsed + * .java source file.
    • + *
    • {@link ItdTypeDetails} instances only represent details of a created .aj + * ITD source file.
    • + *
    • {@link MemberHoldingTypeDetails}s represent a single compilation unit.
    • + *
    • {@link IdentifiableJavaStructure}s represent a single member within a + * {@link MemberHoldingTypeDetails} instance.
    • + *
    • {@link MemberDetails}represent all available + * {@link MemberHoldingTypeDetails} for a given type at this time and for this + * metadata provider.
    • + *
    + * + * @author Ben Alex + * @since 1.1 + */ +public interface MemberDetails { + + /** + * Locates the specified type-level annotation on any of the + * {@link MemberHoldingTypeDetails} in this {@link MemberDetails}. + * + * @param type the type of annotation to locate (required) + * @return the annotation, or null if not found + * @since 1.2.0 + */ + AnnotationMetadata getAnnotation(JavaType type); + + /** + * Searches all {@link MemberHoldingTypeDetails} and returns all + * constructors. + * + * @return zero or more constructors (never null) + * @since 1.2.0 + */ + List getConstructors(); + + /** + * Returns an immutable representation of the member holders. + * + * @return a List of immutable member holders (never null or empty) + */ + List getDetails(); + + /** + * Returns the names of this type's dynamic finders + * + * @return a non-null list + * @since 1.2.0 + */ + List getDynamicFinderNames(); + + /** + * Searches all {@link MemberHoldingTypeDetails} and returns all fields. + * + * @return zero or more fields (never null) + */ + List getFields(); + + /** + * Locates a method with the name presented. Searches all + * {@link MemberDetails} until the first such method is located or none can + * be found. + * + * @param methodName the method name to locate (can be null) + * @return the first located method, or null if the method name + * is null or such a method cannot be found + * @since 1.2.0 + */ + MethodMetadata getMethod(JavaSymbolName methodName); + + /** + * Locates a method with the name and parameter signature presented. + * Searches all {@link MemberDetails} until the first such method is located + * or none can be found. + * + * @param methodName the method name to locate (can be null) + * @param parameters the method parameter signature to locate (can be null + * if no parameters are required) + * @return the first located method, or null if the method name + * is null or such a method cannot be found + * @since 1.2.0 + */ + MethodMetadata getMethod(JavaSymbolName methodName, + List parameters); + + /** + * Locates a method with the name and parameter signature presented that is + * not declared by the presented MID. + * + * @param methodName the method name to locate (can be null) + * @param parameters the method parameter signature to locate (can be null + * if no parameters are required) + * @param excludingMid the MID that a found method cannot be declared by + * @return the first located method, or null if the method name + * is null or such a method cannot be found + * @since 1.2.0 + */ + MethodMetadata getMethod(JavaSymbolName methodName, + List parameters, String excludingMid); + + /** + * Searches all {@link MemberHoldingTypeDetails} and returns all methods. + * + * @return zero or more methods (never null) + * @since 1.2.0 + */ + List getMethods(); + + /** + * Searches all {@link MemberDetails} and returns all methods which contain + * a given {@link CustomData} tag. + * + * @param memberDetails the {@link MemberDetails} to search (required) + * @param tagKey the {@link CustomData} key to search for + * @return zero or more methods (never null) + * @since 1.2.0 + */ + List getMethodsWithTag(Object tagKey); + + /** + * Determines the most concrete {@link MemberHoldingTypeDetails} in cases + * where multiple matches are found for a given tag. + * + * @param tagKey the {@link CustomData} key to search for (required) + * @return the most concrete tagged method or null if not found + * @since 1.2.0 + */ + MethodMetadata getMostConcreteMethodWithTag(Object tagKey); + + /** + * Returns the type of this class' persistent fields, including those in + * collections, but excluding: + *
      + *
    • the ID field
    • + *
    • the version field
    • + *
    • JPA-transient fields
    • + *
    • immutable fields (i.e. that don't have both a getter and a setter)
    • + *
    • embedded ID fields
    • + *
    • the collection types themselves
    • + *
    + * + * @param thisType the owning Java type (required) + * @param persistenceMemberLocator for finding the ID and version fields + * (required) + * @return a non-null set with stable iteration order + * @since 1.2.0 + */ + Set getPersistentFieldTypes(JavaType thisType, + PersistenceMemberLocator persistenceMemberLocator); + + /** + * Indicates whether a method specified by the method attributes is present + * and isn't declared by the passed in MID. + * + * @param methodName the name of the method being searched for + * @param parameterTypes the parameters of the method being searched for + * @param declaredByMetadataId the MID to be used to see if a found method + * is declared by the MID + * @return see above + * @since 1.2.0 + */ + boolean isMethodDeclaredByAnother(JavaSymbolName methodName, + List parameterTypes, String declaredByMetadataId); + + /** + * Indicates whether the requesting MID is annotated with the specified + * annotation. + * + * @param annotationMetadata the annotation to look for + * @param requestingMid the MID interested in + * @return see above + * @since 1.2.0 + */ + boolean isRequestingAnnotatedWith(AnnotationMetadata annotationMetadata, + String requestingMid); +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsBuilder.java b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsBuilder.java new file mode 100644 index 000000000..eb7411acf --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsBuilder.java @@ -0,0 +1,310 @@ +package org.springframework.roo.classpath.scanner; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.roo.classpath.details.AbstractMemberHoldingTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.ConstructorMetadataBuilder; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.FieldMetadataBuilder; +import org.springframework.roo.classpath.details.ImportMetadata; +import org.springframework.roo.classpath.details.ItdTypeDetails; +import org.springframework.roo.classpath.details.ItdTypeDetailsBuilder; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.MethodMetadataBuilder; +import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.CustomDataBuilder; +import org.springframework.roo.model.CustomDataKey; + +/** + * Builder for {@link MemberDetails}. + * + * @author Alan Stewart + * @author Ben Alex + * @author James Tyrrell + * @since 1.1.3 + */ +public class MemberDetailsBuilder { + + static class TypeDetailsBuilder extends + AbstractMemberHoldingTypeDetailsBuilder { + + private final MemberHoldingTypeDetails existing; + + protected TypeDetailsBuilder(final MemberHoldingTypeDetails existing) { + super(existing); + this.existing = existing; + } + + public void addDataToConstructor(final ConstructorMetadata replacement, + final CustomData customData) { + // If the MIDs don't match then the proposed can't be a replacement + if (!replacement.getDeclaredByMetadataId().equals( + getDeclaredByMetadataId())) { + return; + } + for (final ConstructorMetadataBuilder existingConstructor : getDeclaredConstructors()) { + if (AnnotatedJavaType.convertFromAnnotatedJavaTypes( + existingConstructor.getParameterTypes()).equals( + AnnotatedJavaType + .convertFromAnnotatedJavaTypes(replacement + .getParameterTypes()))) { + for (final Object key : customData.keySet()) { + existingConstructor.putCustomData(key, + customData.get(key)); + } + break; + } + } + } + + public void addDataToField(final FieldMetadata replacement, + final CustomData customData) { + // If the MIDs don't match then the proposed can't be a replacement + if (!replacement.getDeclaredByMetadataId().equals( + getDeclaredByMetadataId())) { + return; + } + for (final FieldMetadataBuilder existingField : getDeclaredFields()) { + if (existingField.getFieldName().equals( + replacement.getFieldName())) { + for (final Object key : customData.keySet()) { + existingField.putCustomData(key, customData.get(key)); + } + break; + } + } + } + + public void addDataToMethod(final MethodMetadata replacement, + final CustomData customData) { + // If the MIDs don't match then the proposed can't be a replacement + if (!replacement.getDeclaredByMetadataId().equals( + getDeclaredByMetadataId())) { + return; + } + for (final MethodMetadataBuilder existingMethod : getDeclaredMethods()) { + if (existingMethod.getMethodName().equals( + replacement.getMethodName())) { + if (AnnotatedJavaType.convertFromAnnotatedJavaTypes( + existingMethod.getParameterTypes()).equals( + AnnotatedJavaType + .convertFromAnnotatedJavaTypes(replacement + .getParameterTypes()))) { + for (final Object key : customData.keySet()) { + existingMethod.putCustomData(key, + customData.get(key)); + } + break; + } + } + } + } + + @Override + public void addImports(final Collection imports) { + throw new UnsupportedOperationException(); // No known use case + } + + public MemberHoldingTypeDetails build() { + if (existing instanceof ItdTypeDetails) { + final ItdTypeDetailsBuilder itdBuilder = new ItdTypeDetailsBuilder( + (ItdTypeDetails) existing); + // Push in all members that may have been modified + itdBuilder.setDeclaredFields(getDeclaredFields()); + itdBuilder.setDeclaredMethods(getDeclaredMethods()); + itdBuilder.setAnnotations(getAnnotations()); + itdBuilder.setCustomData(getCustomData()); + itdBuilder.setDeclaredConstructors(getDeclaredConstructors()); + itdBuilder.setDeclaredInitializers(getDeclaredInitializers()); + itdBuilder.setDeclaredInnerTypes(getDeclaredInnerTypes()); + itdBuilder.setExtendsTypes(getExtendsTypes()); + itdBuilder.setImplementsTypes(getImplementsTypes()); + itdBuilder.setModifier(getModifier()); + return itdBuilder.build(); + } + else if (existing instanceof ClassOrInterfaceTypeDetails) { + final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( + (ClassOrInterfaceTypeDetails) existing); + // Push in all members that may + cidBuilder.setDeclaredFields(getDeclaredFields()); + cidBuilder.setDeclaredMethods(getDeclaredMethods()); + cidBuilder.setAnnotations(getAnnotations()); + cidBuilder.setCustomData(getCustomData()); + cidBuilder.setDeclaredConstructors(getDeclaredConstructors()); + cidBuilder.setDeclaredInitializers(getDeclaredInitializers()); + cidBuilder.setDeclaredInnerTypes(getDeclaredInnerTypes()); + cidBuilder.setExtendsTypes(getExtendsTypes()); + cidBuilder.setImplementsTypes(getImplementsTypes()); + cidBuilder.setModifier(getModifier()); + return cidBuilder.build(); + } + else { + throw new IllegalStateException( + "Unknown instance of MemberHoldingTypeDetails"); + } + } + } + + private boolean changed = false; + private final Map memberHoldingTypeDetailsMap = new LinkedHashMap(); + private final MemberDetails originalMemberDetails; + + private final Map typeDetailsBuilderMap = new LinkedHashMap(); + + public MemberDetailsBuilder( + final Collection memberHoldingTypeDetailsList) { + originalMemberDetails = new MemberDetailsImpl( + memberHoldingTypeDetailsList); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : originalMemberDetails + .getDetails()) { + memberHoldingTypeDetailsMap.put( + memberHoldingTypeDetails.getDeclaredByMetadataId(), + memberHoldingTypeDetails); + } + } + + public MemberDetailsBuilder(final MemberDetails memberDetails) { + originalMemberDetails = memberDetails; + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails + .getDetails()) { + memberHoldingTypeDetailsMap.put( + memberHoldingTypeDetails.getDeclaredByMetadataId(), + memberHoldingTypeDetails); + } + } + + public MemberDetails build() { + if (changed) { + for (final TypeDetailsBuilder typeDetailsBuilder : typeDetailsBuilderMap + .values()) { + memberHoldingTypeDetailsMap.put( + typeDetailsBuilder.getDeclaredByMetadataId(), + typeDetailsBuilder.build()); + } + return new MemberDetailsImpl( + new ArrayList( + memberHoldingTypeDetailsMap.values())); + } + return originalMemberDetails; + } + + private void doModification(final ConstructorMetadata constructor, + final CustomData customData) { + final MemberHoldingTypeDetails memberHoldingTypeDetails = memberHoldingTypeDetailsMap + .get(constructor.getDeclaredByMetadataId()); + if (memberHoldingTypeDetails != null) { + final ConstructorMetadata matchedConstructor = memberHoldingTypeDetails + .getDeclaredConstructor(AnnotatedJavaType + .convertFromAnnotatedJavaTypes(constructor + .getParameterTypes())); + if (matchedConstructor != null + && !matchedConstructor.getCustomData().keySet() + .containsAll(customData.keySet())) { + final TypeDetailsBuilder typeDetailsBuilder = getTypeDetailsBuilder(memberHoldingTypeDetails); + typeDetailsBuilder + .addDataToConstructor(constructor, customData); + changed = true; + } + } + } + + private void doModification(final FieldMetadata field, + final CustomData customData) { + final MemberHoldingTypeDetails memberHoldingTypeDetails = memberHoldingTypeDetailsMap + .get(field.getDeclaredByMetadataId()); + if (memberHoldingTypeDetails != null) { + final FieldMetadata matchedField = memberHoldingTypeDetails + .getField(field.getFieldName()); + if (matchedField != null + && !matchedField.getCustomData().keySet() + .containsAll(customData.keySet())) { + final TypeDetailsBuilder typeDetailsBuilder = getTypeDetailsBuilder(memberHoldingTypeDetails); + typeDetailsBuilder.addDataToField(field, customData); + changed = true; + } + } + } + + private void doModification(final MemberHoldingTypeDetails type, + final CustomData customData) { + final MemberHoldingTypeDetails memberHoldingTypeDetails = memberHoldingTypeDetailsMap + .get(type.getDeclaredByMetadataId()); + if (memberHoldingTypeDetails != null) { + if (memberHoldingTypeDetails.getName().equals(type.getName()) + && !memberHoldingTypeDetails.getCustomData().keySet() + .containsAll(customData.keySet())) { + final TypeDetailsBuilder typeDetailsBuilder = getTypeDetailsBuilder(memberHoldingTypeDetails); + typeDetailsBuilder.getCustomData().append(customData); + changed = true; + } + } + } + + private void doModification(final MethodMetadata method, + final CustomData customData) { + final MemberHoldingTypeDetails memberHoldingTypeDetails = memberHoldingTypeDetailsMap + .get(method.getDeclaredByMetadataId()); + if (memberHoldingTypeDetails != null) { + final MethodMetadata matchedMethod = memberHoldingTypeDetails + .getMethod(method.getMethodName(), AnnotatedJavaType + .convertFromAnnotatedJavaTypes(method + .getParameterTypes())); + if (matchedMethod != null + && !matchedMethod.getCustomData().keySet() + .containsAll(customData.keySet())) { + final TypeDetailsBuilder typeDetailsBuilder = getTypeDetailsBuilder(memberHoldingTypeDetails); + typeDetailsBuilder.addDataToMethod(method, customData); + changed = true; + } + } + } + + private TypeDetailsBuilder getTypeDetailsBuilder( + final MemberHoldingTypeDetails memberHoldingTypeDetails) { + if (typeDetailsBuilderMap.containsKey(memberHoldingTypeDetails + .getDeclaredByMetadataId())) { + return typeDetailsBuilderMap.get(memberHoldingTypeDetails + .getDeclaredByMetadataId()); + } + final TypeDetailsBuilder typeDetailsBuilder = new TypeDetailsBuilder( + memberHoldingTypeDetails); + typeDetailsBuilderMap.put( + memberHoldingTypeDetails.getDeclaredByMetadataId(), + typeDetailsBuilder); + return typeDetailsBuilder; + } + + public void tag(final T toModify, final CustomDataKey key, + final Object value) { + if (toModify instanceof FieldMetadata) { + final CustomDataBuilder customDataBuilder = new CustomDataBuilder(); + customDataBuilder.put(key, value); + doModification((FieldMetadata) toModify, customDataBuilder.build()); + } + else if (toModify instanceof MethodMetadata) { + final CustomDataBuilder customDataBuilder = new CustomDataBuilder(); + customDataBuilder.put(key, value); + doModification((MethodMetadata) toModify, customDataBuilder.build()); + } + else if (toModify instanceof ConstructorMetadata) { + final CustomDataBuilder customDataBuilder = new CustomDataBuilder(); + customDataBuilder.put(key, value); + doModification((ConstructorMetadata) toModify, + customDataBuilder.build()); + } + else if (toModify instanceof MemberHoldingTypeDetails) { + final CustomDataBuilder customDataBuilder = new CustomDataBuilder(); + customDataBuilder.put(key, value); + doModification((MemberHoldingTypeDetails) toModify, + customDataBuilder.build()); + } + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsDecorator.java b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsDecorator.java new file mode 100644 index 000000000..992084310 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsDecorator.java @@ -0,0 +1,55 @@ +package org.springframework.roo.classpath.scanner; + +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.model.CustomDataAccessor; + +/** + * Provides the ability to modify or log the result of a + * {@link MemberDetailsScanner} operation before the method returns. This is + * useful to customize the results for a particular requesting class. + * + * @author Ben Alex + * @since 1.1 + */ +public interface MemberDetailsDecorator { + + /** + * Evaluates the incoming {@link MemberDetails} and either (a) returns the + * same instance if no changes are necessary or (b) returns a new instance + * of {@link MemberDetails} if changes are needed. + *

    + * This method will be called repeatedly until such time as every + * {@link MemberDetailsDecorator} returns the same {@link MemberDetails} + * instance as was passed. It is therefore essential that a + * new instance is only returned if an actual change to the result is required. + * An implementation can safely return the same {@link MemberDetails} as it + * was passed if in doubt, as it will always be re-invoked later on if + * another decorator changes the {@link MemberDetails} instance. Thus even + * decorators that depend on other decorators populating the + * {@link MemberDetails} (eg with new {@link CustomDataAccessor} + * information) can be executed in any order whatsoever, as they need only + * look for the expected data and return the same {@link MemberDetails} if + * it is not found. + * + * @param requestingClass the fully-qualified class name requesting the + * member details (required) + * @param memberDetails the current member holders (required) + * @return the originally-passed details (where possible) or a replacement + * details (never returns null) + */ + MemberDetails decorate(String requestingClass, MemberDetails memberDetails); + + /** + * Performs essentially the same function as decorate but only decorates + * {@link MemberHoldingTypeDetails} instances and ignores the type's + * members. + * + * @param requestingClass the fully-qualified class name requesting the + * member details (required) + * @param memberDetails the current member holders (required) + * @return the originally-passed details (where possible) or a replacement + * details (never returns null) + */ + MemberDetails decorateTypes(String requestingClass, + MemberDetails memberDetails); +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsImpl.java new file mode 100644 index 000000000..712c36519 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsImpl.java @@ -0,0 +1,212 @@ +package org.springframework.roo.classpath.scanner; + +import static org.springframework.roo.classpath.customdata.CustomDataKeys.EMBEDDED_FIELD; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.classpath.customdata.CustomDataKeys; +import org.springframework.roo.classpath.details.BeanInfoUtils; +import org.springframework.roo.classpath.details.ConstructorMetadata; +import org.springframework.roo.classpath.details.FieldMetadata; +import org.springframework.roo.classpath.details.MemberFindingUtils; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.details.MethodMetadata; +import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; +import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; +import org.springframework.roo.model.JavaSymbolName; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Default implementation of {@link MemberDetails}. + * + * @author Ben Alex + * @since 1.1 + */ +public class MemberDetailsImpl implements MemberDetails { + + private final List details = new ArrayList(); + + /** + * Constructs a new instance. + * + * @param details the member holders that should be stored in this instance + * (can be null) + */ + MemberDetailsImpl( + final Collection details) { + Validate.notEmpty(details, "Member holding details required"); + CollectionUtils.populate(this.details, details); + } + + public AnnotationMetadata getAnnotation(final JavaType type) { + Validate.notNull(type, "Annotation type to locate required"); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + final AnnotationMetadata md = memberHoldingTypeDetails + .getAnnotation(type); + if (md != null) { + return md; + } + } + return null; + } + + public List getConstructors() { + final List result = new ArrayList(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + result.addAll(memberHoldingTypeDetails.getDeclaredConstructors()); + } + return result; + } + + public List getDetails() { + return Collections.unmodifiableList(details); + } + + public List getDynamicFinderNames() { + final List dynamicFinderNames = new ArrayList(); + for (final MemberHoldingTypeDetails mhtd : details) { + dynamicFinderNames.addAll(mhtd.getDynamicFinderNames()); + } + return dynamicFinderNames; + } + + public List getFields() { + final List result = new ArrayList(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + result.addAll(memberHoldingTypeDetails.getDeclaredFields()); + } + return result; + } + + public MethodMetadata getMethod(final JavaSymbolName methodName) { + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + final MethodMetadata md = MemberFindingUtils.getDeclaredMethod( + memberHoldingTypeDetails, methodName); + if (md != null) { + return md; + } + } + return null; + } + + public MethodMetadata getMethod(final JavaSymbolName methodName, + final List parameters) { + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + final MethodMetadata md = MemberFindingUtils.getDeclaredMethod( + memberHoldingTypeDetails, methodName, parameters); + if (md != null) { + return md; + } + } + return null; + } + + public MethodMetadata getMethod(final JavaSymbolName methodName, + final List parameters, final String excludingMid) { + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + final MethodMetadata method = MemberFindingUtils.getDeclaredMethod( + memberHoldingTypeDetails, methodName, parameters); + if (method != null + && !method.getDeclaredByMetadataId().equals(excludingMid)) { + return method; + } + } + return null; + } + + public List getMethods() { + final List result = new ArrayList(); + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + result.addAll(memberHoldingTypeDetails.getDeclaredMethods()); + } + return result; + } + + public List getMethodsWithTag(final Object tagKey) { + Validate.notNull(tagKey, "Custom data key required"); + final List result = new ArrayList(); + for (final MethodMetadata method : getMethods()) { + if (method.getCustomData().keySet().contains(tagKey)) { + result.add(method); + } + } + return result; + } + + public MethodMetadata getMostConcreteMethodWithTag(final Object tagKey) { + return CollectionUtils.firstElementOf(getMethodsWithTag(tagKey)); + } + + public Set getPersistentFieldTypes(final JavaType thisType, + final PersistenceMemberLocator persistenceMemberLocator) { + final MethodMetadata identifierAccessor = persistenceMemberLocator + .getIdentifierAccessor(thisType); + final MethodMetadata versionAccessor = persistenceMemberLocator + .getVersionAccessor(thisType); + + final Set fieldTypes = new LinkedHashSet(); + for (final MethodMetadata method : getMethods()) { + // Not interested in non-accessor methods or persistence identifiers + // and version fields + if (!BeanInfoUtils.isAccessorMethod(method) + || method.hasSameName(identifierAccessor, versionAccessor)) { + continue; + } + + // Not interested in fields that are JPA transient fields or + // immutable fields + final FieldMetadata field = BeanInfoUtils + .getFieldForJavaBeanMethod(this, method); + if (field == null + || field.getCustomData().keySet() + .contains(CustomDataKeys.TRANSIENT_FIELD) + || !BeanInfoUtils.hasAccessorAndMutator(field, this)) { + continue; + } + final JavaType returnType = method.getReturnType(); + if (returnType.isCommonCollectionType()) { + for (final JavaType genericType : returnType.getParameters()) { + fieldTypes.add(genericType); + } + } + else { + if (!field.getCustomData().keySet().contains(EMBEDDED_FIELD)) { + fieldTypes.add(returnType); + } + } + } + return fieldTypes; + } + + public boolean isMethodDeclaredByAnother(final JavaSymbolName methodName, + final List parameterTypes, + final String declaredByMetadataId) { + final MethodMetadata method = getMethod(methodName, parameterTypes); + return method != null + && !method.getDeclaredByMetadataId().equals( + declaredByMetadataId); + } + + public boolean isRequestingAnnotatedWith( + final AnnotationMetadata annotationMetadata, + final String requestingMid) { + for (final MemberHoldingTypeDetails memberHoldingTypeDetails : details) { + if (MemberFindingUtils.getAnnotationOfType( + memberHoldingTypeDetails.getAnnotations(), + annotationMetadata.getAnnotationType()) != null) { + if (memberHoldingTypeDetails.getDeclaredByMetadataId().equals( + requestingMid)) { + return true; + } + } + } + return false; + } +} diff --git a/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsScanner.java b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsScanner.java new file mode 100644 index 000000000..1707521a7 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsScanner.java @@ -0,0 +1,77 @@ +package org.springframework.roo.classpath.scanner; + +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.model.CustomDataAccessor; +import org.springframework.roo.model.JavaType; + +/** + * Service that automatically builds a {@link MemberDetails} instance for a + * given class or interface type. + *

    + * Because Spring Roo encourages the use of multiple compilation units (ie + * AspectJ ITDs) in the creation of a single type, it is relatively complex to + * build a complete representation of the type. This is especially complex if a + * representation is desired in the middle of creating an + * {@link ItdTypeDetailsProvidingMetadataItem} for that same type (as the "rest" + * of the type information is often needed to produce the new ITD-based + * metadata, plus there are frequently infinite loops to avoid). + *

    + * A {@link MemberDetailsScanner} is therefore the recommended way for add-ons + * to build representations of Java types. There are several reasons: + *

      + *
    • The discovery and collation of multiple compilation units is delegated to + * a specialized, well-defined service ({@link MemberDetailsScanner})
    • + *
    • A {@link MemberDetailsDecorator} facility permits customization of the + * visible member information on a per-caller basis, thus offering a way to + * customize the information seen by a given caller
    • + *
    • {@link MemberDetailsDecorator}s can use the {@link CustomDataAccessor} + * facility to associate custom information with members and compilation units + * in an easy manne.
    • + *
    + * + * @author Ben Alex + * @since 1.1 + */ +public interface MemberDetailsScanner { + + /** + * Builds {@link MemberDetails} instance for the given + * {@link ClassOrInterfaceTypeDetails}. In particular, this includes all ITD + * members that can be acquired at this time. It also includes all members + * in the standard Java source class hierarchy (as per + * {@link ClassOrInterfaceTypeDetails#getSuperclass()}). + *

    + * No attempt is made at visibility resolution due to inheritance or aspect + * overriding, although we may add this in the future (it is difficult to + * add definitively given the detected members may be incomplete due to + * infinite loop avoidance). + *

    + * An implementation will pass the resulting {@link MemberDetails} through + * any detected {@link MemberDetailsDecorator} instances so they can + * potentially replace the result with a new instance that is ultimately + * returned. It is required that implementations invoke decorators in the + * alphabetic order of each decorator's fully qualified class name. Only + * when every {@link MemberDetailsDecorator} returns the same + * {@link MemberDetails} as it was passed will processing be regarded as + * complete. This manages any issues with respect to the order in which + * {@link MemberDetailsDecorator}s are invoked. + *

    + * The requesting class is presented as a String as per normal OSGi + * conventions. This avoids issues that may occur if a {@link Class} was + * used and the bundle owning that class was being deactivated. A + * {@link JavaType} was not used because of its comparatively heavier weight + * and is primarily used to represent user project-specific types (not + * internal types). + * + * @param requestingClass the fully-qualified class name requesting the + * member details (required; may be used for result + * customization) + * @param cid the class or interface for which to build member information + * (can be null) + * @return the discovered member details, or null if + * null {@link ClassOrInterfaceTypeDetails} were given + */ + MemberDetails getMemberDetails(String requestingClass, + ClassOrInterfaceTypeDetails cid); +} \ No newline at end of file diff --git a/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsScannerImpl.java b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsScannerImpl.java new file mode 100644 index 000000000..8ea3c29f3 --- /dev/null +++ b/classpath/src/main/java/org/springframework/roo/classpath/scanner/MemberDetailsScannerImpl.java @@ -0,0 +1,247 @@ +package org.springframework.roo.classpath.scanner; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.References; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; +import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; +import org.springframework.roo.classpath.itd.ItdMetadataProvider; +import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.metadata.MetadataService; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Default implementation of {@link MemberDetailsScanner}. + *

    + * Automatically detects all {@link MemberDetailsDecorator} instances in the + * OSGi container and will delegate to them during execution of the + * {@link #getMemberDetails(String, ClassOrInterfaceTypeDetails)} method. + *

    + * While internally this implementation will visit {@link MetadataProvider}s and + * {@link MemberDetailsDecorator}s in the order of their type name, it is + * essential an add-on developer does not rely on this behaviour. Correct use of + * the metadata infrastructure does not require special type naming approaches + * to be employed. The ordering behaviour exists solely to simplify debugging + * for add-on developers and log comparison between invocations. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class MemberDetailsScannerImpl implements MemberDetailsScanner { + + protected final static Logger LOGGER = HandlerUtils.getLogger(MemberDetailsScannerImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected MetadataService metadataService; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + private final SortedSet decorators = new TreeSet( + new Comparator() { + public int compare(final MemberDetailsDecorator o1, + final MemberDetailsDecorator o2) { + return o1.getClass().getName() + .compareTo(o2.getClass().getName()); + } + }); + + // Mutex + private final Object lock = new Object(); + + private final SortedSet providers = new TreeSet( + new Comparator() { + public int compare(final MetadataProvider o1, + final MetadataProvider o2) { + return o1.getClass().getName() + .compareTo(o2.getClass().getName()); + } + }); + + protected void bindDecorators() { + synchronized (lock) { + // Get all Services implement MemberDetailsDecorator interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MemberDetailsDecorator.class.getName(), null); + for(ServiceReference ref : references){ + MemberDetailsDecorator decorator = (MemberDetailsDecorator) this.context.getService(ref); + decorators.add(decorator); + } + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MemberDetailsDecorator on MemberDetailsScannerImpl."); + } + } + } + + protected void bindProviders() { + synchronized (lock) { + // Get all Services implement MetadataProvider interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataProvider.class.getName(), null); + for(ServiceReference ref : references){ + MetadataProvider provider = (MetadataProvider) this.context.getService(ref); + Validate.notNull(provider, "Metadata provider required"); + final String mid = provider.getProvidesType(); + Validate.isTrue( + MetadataIdentificationUtils.isIdentifyingClass(mid), + "Metadata provider '%s' violated interface contract by returning '%s'", + provider, mid); + providers.add(provider); + } + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataProvider on MemberDetailsScannerImpl."); + } + } + } + + protected void deactivate(final ComponentContext componentContext) { + // Empty + } + + public final MemberDetails getMemberDetails(final String requestingClass, + ClassOrInterfaceTypeDetails cid) { + + if(metadataService == null){ + metadataService = getMetadataService(); + } + + if(providers.isEmpty()){ + bindProviders(); + } + + if(decorators.isEmpty()){ + bindDecorators(); + } + + if (cid == null) { + return null; + } + synchronized (lock) { + // Create a list of discovered members + final List memberHoldingTypeDetails = new ArrayList(); + + // Build a List representing the class hierarchy, where the first + // element is the absolute superclass + final List cidHierarchy = new ArrayList(); + while (cid != null) { + cidHierarchy.add(0, cid); // Note to the top of the list + cid = cid.getSuperclass(); + } + + // Now we add this governor, plus all of its superclasses + for (final ClassOrInterfaceTypeDetails currentClass : cidHierarchy) { + memberHoldingTypeDetails.add(currentClass); + + // Locate all MetadataProvider instances that provide ITDs and + // thus MemberHoldingTypeDetails information + for (final MetadataProvider mp : providers) { + // Skip non-ITD providers + if (!(mp instanceof ItdMetadataProvider)) { + continue; + } + + // Skip myself + if (mp.getClass().getName().equals(requestingClass)) { + continue; + } + + // Determine the key the ITD provider uses for this + // particular type + final String key = ((ItdMetadataProvider) mp) + .getIdForPhysicalJavaType(currentClass + .getDeclaredByMetadataId()); + Validate.isTrue( + MetadataIdentificationUtils + .isIdentifyingInstance(key), + "ITD metadata provider '%s' returned an illegal key ('%s')", + mp, key); + + // Get the metadata and ensure we have ITD type details + // available + final MetadataItem metadataItem = metadataService.get(key); + if (metadataItem == null || !metadataItem.isValid()) { + continue; + } + Validate.isInstanceOf( + ItdTypeDetailsProvidingMetadataItem.class, + metadataItem, + "ITD metadata provider '%s' failed to return the correct metadata type", + mp); + final ItdTypeDetailsProvidingMetadataItem itdTypeDetailsMd = (ItdTypeDetailsProvidingMetadataItem) metadataItem; + if (itdTypeDetailsMd.getMemberHoldingTypeDetails() == null) { + continue; + } + + // Capture the member details + memberHoldingTypeDetails.add(itdTypeDetailsMd + .getMemberHoldingTypeDetails()); + } + } + + // Turn out list of discovered members into a result + MemberDetails result = new MemberDetailsImpl( + memberHoldingTypeDetails); + + // Loop until such time as we complete a full loop where no changes + // are made to the result + boolean additionalLoopRequired = true; + while (additionalLoopRequired) { + additionalLoopRequired = false; + for (final MemberDetailsDecorator decorator : decorators) { + final MemberDetails newResult = decorator.decorate( + requestingClass, result); + Validate.isTrue(newResult != null, + "Decorator '%s' returned an illegal result", + decorator.getClass().getName()); + if (newResult != null && !newResult.equals(result)) { + additionalLoopRequired = true; + } + result = newResult; + } + } + + return result; + } + } + + public MetadataService getMetadataService(){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on MemberDetailsScannerImpl."); + return null; + } + } +} \ No newline at end of file diff --git a/classpath/src/main/resources/bikeshop.roo b/classpath/src/main/resources/bikeshop.roo new file mode 100644 index 000000000..363caddbe --- /dev/null +++ b/classpath/src/main/resources/bikeshop.roo @@ -0,0 +1,44 @@ +project --topLevelPackage com.springsource.bikeshop +jpa setup --provider ECLIPSELINK --database H2_IN_MEMORY + +enum type --class ~.reference.ProductType +enum constant --name Frame +enum constant --name Brakes +enum constant --name Crank +enum constant --name Wheel +enum constant --name Headset +enum constant --name Handlebar +enum constant --name Saddle +enum constant --name Pedal +enum constant --name Cassette +enum constant --name Tyre +enum constant --name Seatpost +enum constant --name Stem +enum constant --name Derailleur +enum constant --name Fork + +entity jpa --class ~.domain.Product --activeRecord false --equals --testAutomatically +field string --fieldName name --sizeMax 25 --notNull +field string --fieldName description --sizeMax 250 +field enum --fieldName productType --type ~.reference.ProductType --notNull +field date --fieldName releaseDate --type java.util.Date +field number --fieldName weight --type java.math.BigDecimal --decimalMin 0.0 --decimalMax 9.99 +field file --fieldName image --contentType JPG +repository jpa --interface ~.domain.ProductRepository + +entity jpa --class ~.domain.Supplier --activeRecord false --equals --testAutomatically +field string --fieldName name --sizeMax 25 --notNull +field string --fieldName address --sizeMax 100 --notNull +field string --fieldName description +field number --type int --fieldName supplierNumber --min 1 --max 99 +field date --fieldName inceptionDate --type java.util.Date --past +field string --fieldName email --regexp "[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+" +field set --fieldName products --type ~.domain.Product --mappedBy supplier --notNull false --cardinality ONE_TO_MANY --fetch EAGER +repository jpa --interface ~.domain.SupplierRepository + +field reference --fieldName supplier --class ~.domain.Product --type ~.domain.Supplier --notNull + +web jsf setup +web jsf all --package ~.web + +logging setup --level INFO \ No newline at end of file diff --git a/classpath/src/main/resources/clinic.roo b/classpath/src/main/resources/clinic.roo new file mode 100644 index 000000000..7f1261e2b --- /dev/null +++ b/classpath/src/main/resources/clinic.roo @@ -0,0 +1,72 @@ +project --topLevelPackage com.springsource.petclinic + +jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY + +enum type --class ~.reference.PetType +enum constant --name Dog +enum constant --name Cat +enum constant --name Bird + +enum type --class ~.reference.Specialty +enum constant --name Cardiology +enum constant --name Dentistry +enum constant --name Nutrition + +entity jpa --class ~.domain.Pet --sequenceName PET_SEQ +entity jpa --class ~.domain.Visit --sequenceName VISIT_SEQ +entity jpa --class ~.domain.AbstractPerson --abstract +entity jpa --class ~.domain.Vet --extends ~.domain.AbstractPerson +entity jpa --class ~.domain.Owner --extends ~.domain.AbstractPerson + +field string --fieldName firstName --sizeMin 3 --sizeMax 30 --class ~.domain.AbstractPerson +field string --fieldName lastName --notNull --sizeMin 3 --sizeMax 30 +field string --fieldName address --notNull --sizeMax 50 --sizeMin 1 +field string --fieldName city --notNull --sizeMax 30 +field string --fieldName telephone --notNull +field string --fieldName homePage --sizeMax 30 +field string --fieldName email --sizeMax 30 --sizeMin 6 +field date --fieldName birthDay --type java.util.Date --notNull + +field string --fieldName description --sizeMax 255 --class ~.domain.Visit +field date --fieldName visitDate --type java.util.Date --notNull --past +field reference --fieldName pet --type ~.domain.Pet --notNull +field reference --fieldName vet --type ~.domain.Vet + +field boolean --fieldName sendReminders --notNull --primitive --class ~.domain.Pet +field string --fieldName name --notNull --sizeMin 1 +field number --fieldName weight --type java.lang.Float --notNull --min 0 +field reference --fieldName owner --type ~.domain.Owner +field enum --fieldName type --type ~.reference.PetType --notNull + +field date --fieldName employedSince --type java.util.Calendar --notNull --past --class ~.domain.Vet +field enum --fieldName specialty --type ~.reference.Specialty --notNull false + +field set --class ~.domain.Owner --fieldName pets --type ~.domain.Pet --mappedBy owner --notNull false --cardinality ONE_TO_MANY + +finder add --finderName findPetsByNameAndWeight --class ~.domain.Pet +finder add --finderName findPetsByOwner +finder add --finderName findPetsBySendRemindersAndWeightLessThan +finder add --finderName findPetsByTypeAndNameLike + +finder add --finderName findVisitsByDescriptionAndVisitDate --class ~.domain.Visit +finder add --finderName findVisitsByVisitDateBetween +finder add --finderName findVisitsByDescriptionLike + +test integration --entity ~.domain.Vet +test integration --entity ~.domain.Owner +test integration --entity ~.domain.Pet +test integration --entity ~.domain.Visit + +web mvc setup +web mvc all --package ~.web +web mvc finder all + +web mvc language --code de +web mvc language --code es + +selenium test --controller ~.web.OwnerController +selenium test --controller ~.web.PetController +selenium test --controller ~.web.VetController +selenium test --controller ~.web.VisitController + +logging setup --level INFO \ No newline at end of file diff --git a/classpath/src/main/resources/embedding.roo b/classpath/src/main/resources/embedding.roo new file mode 100644 index 000000000..e9fdba5d0 --- /dev/null +++ b/classpath/src/main/resources/embedding.roo @@ -0,0 +1,20 @@ +project --topLevelPackage org.springsource.embedding +web mvc setup +// These commands are sorted by command name then by provider +web mvc embed document --provider GOOGLE_PRESENTATION --documentId 0AcgEqD3JotEpZGQ4cmY4dDlfMzFjOWYyZmNnZA --viewName Google_Presentation +web mvc embed document --provider SCRIBD --documentId 27766735 --viewName Scribd_Document +web mvc embed document --provider SLIDESHARE --documentId springone2gxslidesstefanschmidt-100120044009-phpapp01 --viewName Slideshare_Presentation +web mvc embed generic --url http://www.youtube.com/watch?v=Gb1Z0lfl52I --viewName YouTube_Generic +web mvc embed map --location "8-20 Napier St, North Sydney, Australia" --viewName Google_Maps +web mvc embed photos --provider FLIKR --userId 21936447@N04 --albumId siefken --viewName Flickr +web mvc embed photos --provider PICASA --userId stsmedia --albumId 5361850989779114785 --viewName Picasa +web mvc embed stream video --provider LIVESTREAM --streamId winradio101 --viewName Livestream +web mvc embed stream video --provider USTREAM --streamId 4424524 --viewName Ustream +web mvc embed twitter --searchTerm "Spring Roo" --viewName Twitter +web mvc embed video --provider GOOGLE_VIDEO --videoId 1753096859715615067 --viewName Google_Video +web mvc embed video --provider SCREENR --videoId DOOs --viewName Screenr +web mvc embed video --provider VIDDLER --videoId 34a0a068 --viewName Viddler +web mvc embed video --provider VIMEO --videoId 11890173 --viewName Vimeo +web mvc embed video --provider YOUTUBE --videoId Gb1Z0lfl52I --viewName YouTube_Specific +quit +// Now start this application using "mvn jetty:run" or "mvn tomcat:run" \ No newline at end of file diff --git a/classpath/src/main/resources/expenses.roo.deprecated b/classpath/src/main/resources/expenses.roo.deprecated new file mode 100644 index 000000000..d73e158fb --- /dev/null +++ b/classpath/src/main/resources/expenses.roo.deprecated @@ -0,0 +1,37 @@ +project --topLevelPackage org.springsource.roo.extrack + +jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY + +enum type --class ~.shared.domain.Gender +enum constant --name MALE +enum constant --name FEMALE + +entity jpa --class ~.server.domain.Employee --testAutomatically +field string --fieldName displayName --notNull +field string --fieldName userName --sizeMin 3 --sizeMax 30 --notNull +field string --fieldName department +field reference --type Employee supervisor +field enum --fieldName gender --type ~.shared.domain.Gender +field boolean --fieldName admin --notNull + +entity jpa --class ~.server.domain.Report --testAutomatically +field string --fieldName purpose +field string --fieldName notes +field date --fieldName created --type java.util.Date +field string --fieldName department +field reference --type Employee reporter +field reference --type Employee approvedSupervisor + +entity jpa --class ~.server.domain.Expense --testAutomatically +field number --type java.lang.Double amount +field string --fieldName description +field reference --type Report report +field string --fieldName approval +field string --fieldName category +field date --fieldName created --type java.util.Date +field string --fieldName reasonDenied + +web gwt setup +web gwt all --proxyPackage ~.client.proxy --requestPackage ~.client.request + +logging setup --level INFO diff --git a/classpath/src/main/resources/gae-expenses.roo.deprecated b/classpath/src/main/resources/gae-expenses.roo.deprecated new file mode 100644 index 000000000..f02922977 --- /dev/null +++ b/classpath/src/main/resources/gae-expenses.roo.deprecated @@ -0,0 +1,41 @@ +project --topLevelPackage roo.extrack + +jpa setup --database GOOGLE_APP_ENGINE --provider DATANUCLEUS + +enum type --class ~.shared.domain.Gender +enum constant --name MALE +enum constant --name FEMALE + +entity jpa --class ~.server.domain.Employee --identifierType java.lang.String --versionType java.lang.Long +field string --fieldName displayName --notNull +field string --fieldName userName --sizeMin 3 --sizeMax 30 --notNull +field string --fieldName department +field enum --fieldName gender --type ~.shared.domain.Gender --enumType STRING +field boolean --fieldName admin --notNull + +entity jpa --class ~.server.domain.Report --identifierType java.lang.String --versionType java.lang.Long +field string --fieldName purpose +field string --fieldName notes +field date --fieldName created --type java.util.Date +field string --fieldName department +field reference --type Employee reporter --fetch LAZY +field reference --type Employee approvedSupervisor + +entity jpa --class ~.server.domain.Expense --identifierType java.lang.String --versionType java.lang.Long +field number --type java.lang.Double amount +field string --fieldName description +field reference --type Report report --fetch LAZY +field string --fieldName approval +field string --fieldName category +field date --fieldName created --type java.util.Date +field string --fieldName reasonDenied + +// Owned many-to-one relationships defining GAE entity group hierarchy +field list --class ~.server.domain.Employee --fieldName reports --type ~.server.domain.Report --mappedBy reporter --cardinality ONE_TO_MANY +field list --class ~.server.domain.Report --fieldName expenses --type ~.server.domain.Expense --mappedBy report --cardinality ONE_TO_MANY + +web gwt setup +web gwt all --proxyPackage ~.client.proxy --requestPackage ~.client.request + +logging setup --level INFO + diff --git a/classpath/src/main/resources/multimodule.roo b/classpath/src/main/resources/multimodule.roo new file mode 100644 index 000000000..eef2d8038 --- /dev/null +++ b/classpath/src/main/resources/multimodule.roo @@ -0,0 +1,75 @@ +project --topLevelPackage com.springsource.petclinic --packaging POM + +module create --moduleName core --topLevelPackage com.example.petclinic + +jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY + +enum type --class ~.reference.PetType +enum constant --name Dog +enum constant --name Cat +enum constant --name Bird + +enum type --class ~.reference.Specialty +enum constant --name Cardiology +enum constant --name Dentistry +enum constant --name Nutrition + +entity jpa --class ~.domain.Pet --testAutomatically +entity jpa --class ~.domain.Visit --testAutomatically +entity jpa --class ~.domain.AbstractPerson --abstract +entity jpa --class ~.domain.Vet --extends ~.domain.AbstractPerson --testAutomatically +entity jpa --class ~.domain.Owner --extends ~.domain.AbstractPerson --testAutomatically + +field string --fieldName firstName --sizeMin 3 --sizeMax 30 --class ~.domain.AbstractPerson +field string --fieldName lastName --notNull --sizeMin 3 --sizeMax 30 +field string --fieldName address --notNull --sizeMax 50 --sizeMin 1 +field string --fieldName city --notNull --sizeMax 30 +field string --fieldName telephone --notNull +field string --fieldName homePage --sizeMax 30 +field string --fieldName email --sizeMax 30 --sizeMin 6 +field date --fieldName birthDay --type java.util.Date --notNull + +field string --fieldName description --sizeMax 255 --class ~.domain.Visit +field date --fieldName visitDate --type java.util.Date --notNull --past +field reference --fieldName pet --type ~.domain.Pet --notNull +field reference --fieldName vet --type ~.domain.Vet + +field boolean --fieldName sendReminders --notNull --primitive --class ~.domain.Pet +field string --fieldName name --notNull --sizeMin 1 +field number --fieldName weight --type java.lang.Float --notNull --min 0 +field reference --fieldName owner --type ~.domain.Owner +field enum --fieldName type --type ~.reference.PetType --notNull + +field date --fieldName employedSince --type java.util.Calendar --notNull --past --class ~.domain.Vet +field enum --fieldName specialty --type ~.reference.Specialty --notNull false + +field set --class ~.domain.Owner --fieldName pets --type ~.domain.Pet --mappedBy owner --notNull false --cardinality ONE_TO_MANY + +finder add --finderName findPetsByNameAndWeight --class ~.domain.Pet +finder add --finderName findPetsByOwner +finder add --finderName findPetsBySendRemindersAndWeightLessThan +finder add --finderName findPetsByTypeAndNameLike + +finder add --finderName findVisitsByDescriptionAndVisitDate --class ~.domain.Visit +finder add --finderName findVisitsByVisitDateBetween +finder add --finderName findVisitsByDescriptionLike + +module focus --moduleName ~ +module create --moduleName ui --topLevelPackage com.example.petclinic.ui --packaging POM +module create --moduleName mvc --topLevelPackage com.example.petclinic.ui.mvc + +web mvc setup +web mvc all --package ~ +web mvc finder all + +web mvc language --code de +web mvc language --code es + +selenium test --controller ~.OwnerController +selenium test --controller ~.PetController +selenium test --controller ~.VetController +selenium test --controller ~.VisitController + +module focus --moduleName ui/mvc +logging setup --level INFO +dependency add --groupId com.example.petclinic --artifactId core --version 0.1.0.BUILD-SNAPSHOT diff --git a/classpath/src/main/resources/org/springframework/roo/classpath/operations/HintCommands.properties b/classpath/src/main/resources/org/springframework/roo/classpath/operations/HintCommands.properties new file mode 100644 index 000000000..c91120116 --- /dev/null +++ b/classpath/src/main/resources/org/springframework/roo/classpath/operations/HintCommands.properties @@ -0,0 +1,14 @@ +topics=The following hints are available to help you use Roo:\r\r general, start, persistence, entities, fields, relationships, \r web mvc, finders, eclipse, logging, gwt\r\rJust type 'hint topic_name' (without quotes) to view a specific hint. + +general=At this stage of the project, you have a few options:\r\r * List all hint topics via 'hint topics'\r * Create more fields with 'hint fields'\r * Create more entities with 'hint entities'\r * Create a web controller with 'hint web mvc'\r * Create dynamic finders with 'hint finders'\r * Setup your logging levels via 'hint logging'\r * Run tests via Maven (type 'perform tests')\r * Build a deployment artifact (type 'perform package')\r * Learn about Eclipse integration by typing 'hint eclipse'\r * Add support for Google Web Toolkit via 'hint gwt'\r * Discover all Roo commands by typing 'help' + +start=Welcome to Roo! We hope you enjoy your stay!\n\nBefore you can use many features of Roo, you need to start a new project.\r\rTo do this, type 'project' (without the quotes) and then hit ${completion_key}.\r\rEnter a --topLevelPackage like 'com.mycompany.projectname' (no quotes).\rWhen you've finished completing your --topLevelPackage, press ENTER.\rYour new project will then be created in the current working directory.\r\rNote that Roo frequently allows the use of ${completion_key}, so press ${completion_key} regularly.\rOnce your project is created, type 'hint' and ENTER for the next suggestion.\rYou're also welcome to visit http://forum.springframework.org for Roo help. +persistence=Roo requires the installation of a persistence configuration,\rfor example, JPA or MongoDB.\r\rFor JPA, type 'jpa setup' and then hit ${completion_key} three times.\rWe suggest you type 'H' then ${completion_key} to complete "HIBERNATE".\rAfter the --provider, press ${completion_key} twice for database choices.\rFor testing purposes, type (or ${completion_key}) HYPERSONIC_IN_MEMORY.\rIf you press ${completion_key} again, you'll see there are no more options.\rAs such, you're ready to press ENTER to execute the command.\r\rOnce JPA is installed, type 'hint' and ENTER for the next suggestion.\n\nSimilarly, for MongoDB persistence, type 'mongo setup' and ENTER. +entities=You can create entities either via Roo or your IDE.\nUsing the Roo shell is fast and easy, especially thanks to the ${completion_key} completion.\r\rStart by typing 'ent' and then hitting ${completion_key} twice.\rEnter the --class in the form '~.domain.MyEntityClassName'\rIn Roo, '~' means the --topLevelPackage you specified via 'create project'.\r\rAfter specify a --class argument, press SPACE then ${completion_key}. Note nothing appears.\rBecause nothing appears, it means you've entered all mandatory arguments.\rHowever, optional arguments do exist for this command (and most others in Roo).\rTo see the optional arguments, type '--' and then hit ${completion_key}. Mostly you won't\nneed any optional arguments, but let's select the --testAutomatically option\nand hit ENTER. You can always use this approach to view optional arguments.\r\rAfter creating an entity, use 'hint' for the next suggestion. +fields=You can add fields to your entities using either Roo or your IDE.\n\nTo add a new field, type 'field' and then hit ${completion_key}. Be sure to select\nyour entity and provide a legal Java field name. Use ${completion_key} to find an entity\nname, and '~' to refer to the top level package. Also remember to use ${completion_key}\nto access each mandatory argument for the command.\n\nAfter completing the mandatory arguments, press SPACE, type '--' and then ${completion_key}.\nThe optional arguments shown reflect official JSR 303 Validation constraints.\nFeel free to use an optional argument, or delete '--' and hit ENTER.\n\nIf creating multiple fields, use the UP arrow to access command history.\n\nAfter adding your fields, type 'hint' for the next suggestion.\nTo learn about setting up many-to-one fields, type 'hint relationships'. +relationships=You create persistent relationships via 'field set' and 'field reference'.\n\nFor example, consider the typical Order <-> LineItem case:\n\n ENTITY: Order ENTITY: LineItem\n Set items Order order\n\nTo setup this relationship in Roo, you would use:\n\nfield set --fieldName items --class Order --type LineItem --mappedBy order\nfield reference --fieldName order --class LineItem --type Order --notNull\n\nLearn about fields addition using 'hint fields'. +web\ mvc=Creating RESTful Spring MVC web controllers is quick and easy using Roo.\nControllers can be made that automatically expose an entity. Alternately, we\ncan create a stub, empty controller for you to finish off.\n\nFor the former, type 'web mvc setup' and hit ENTER followed by 'web mvc scaffold' and hit ${completion_key} three times.\nThe --class is the controller name; it need not reflect an entity name.\nWe suggest putting controllers under a '~.web' package to improve maintenance.\nYou can also specify the --backingType the controller should expose.\n\nAfter creating a controller, use 'hint' for further suggestions. +finders=Roo can automatically create complex, type-safe finder methods.\n\nBe sure to add fields to your entity before creating finders.\nLearn how to add fields by typing 'hint fields'.\n\nTo view available finders, type 'finder list' then ${completion_key}.\nNext select your entity class and hit ENTER. Names are then displayed.\nYou can see even more finder combinations by using --depth 2 or higher.\n\nTo add a finder, type 'finder add' and hit ${completion_key} twice.\nSpecify a --finderName from the earlier displayed output of 'list finders for'.\n\nFor more hints, type 'hint' and hit ENTER. +eclipse=It's easy to use your project in Eclipse or SpringSource Tool Suite (STS).\r\rTo set this up, you'll need to use the command prompt and then Eclipse itself:\r\r 1. Start by typing 'exit', to leave the Roo shell\r 2. Type 'mvn eclipse:clean eclipse:eclipse' to create Eclipse project files\r 3. Load Eclipse, then File > Import > Existing Projects Into Workspace\r 4. Ensure AJDT 1.6.5 or above is installed in Eclipse\r 5. Enable Window > Preferences > General > Workspace > Refresh Automatically\r 6. Finally, restart Roo (type 'roo' at the OS prompt) and enter 'hint'\r\rPlease note if you have the m2eclipse plugin installed, you need only select\rImport > Maven Projects from the Eclipse Import menu.\r\rFor the best Eclipse experience, we recommend SpringSource Tool Suite (STS):\r\r * Graphical editing of Roo commands\r * No need to use the Roo shell\r * Immediate importing of Roo projects\r * Full Spring projects integration\r * Many other productivity-increasing features\r\rDownload STS from http://springsource.com (it's free!).\r\rYou can also use 'perform eclipse' instead of leaving the Roo interface. +logging=You can easily configure logging levels in your project.\r\rRoo will update the log4j.properties file to control your logging.\r\rType 'logging setup' then hit ${completion_key} twice. We suggest 'DEBUG' level.\rYou may wish to specify the optional --package argument (defaults to 'ALL').\r\rRemember to type 'hint' for further hints and suggestions. +gwt=It's easy to create a GWT client in your project.\r\rJust type 'web gwt setup' and press ENTER.\r\rNote Roo's GWT support outputs GWT 2.2 applications. \ No newline at end of file diff --git a/classpath/src/main/resources/pizzashop.roo b/classpath/src/main/resources/pizzashop.roo new file mode 100644 index 000000000..4db54f6f2 --- /dev/null +++ b/classpath/src/main/resources/pizzashop.roo @@ -0,0 +1,60 @@ +// Create a new project +project --topLevelPackage com.springsource.pizzashop + +// Setup JPA persistence using EclipseLink and H2 +jpa setup --provider ECLIPSELINK --database H2_IN_MEMORY + +// Create domain entities +entity jpa --class ~.domain.Base --activeRecord false --testAutomatically +field string --fieldName name --sizeMin 2 --notNull + +entity jpa --class ~.domain.Topping --activeRecord false --testAutomatically +field string --fieldName name --sizeMin 2 --notNull + +entity jpa --class ~.domain.Pizza --activeRecord false --testAutomatically +field string --fieldName name --notNull --sizeMin 2 +field number --fieldName price --type java.math.BigDecimal +field set --fieldName toppings --type ~.domain.Topping +field reference --fieldName base --type ~.domain.Base + +entity jpa --class ~.domain.PizzaOrder --testAutomatically --activeRecord false --identifierType ~.domain.PizzaOrderPk +field string --fieldName name --notNull --sizeMin 2 +field string --fieldName address --sizeMax 30 +field number --fieldName total --type java.math.BigDecimal +field date --fieldName deliveryDate --type java.util.Date +field set --fieldName pizzas --type ~.domain.Pizza + +field string --fieldName shopCountry --class ~.domain.PizzaOrderPk +field string --fieldName shopCity +field string --fieldName shopName + +// Define a repository layer for persistence +repository jpa --interface ~.repository.ToppingRepository --entity ~.domain.Topping +repository jpa --interface ~.repository.BaseRepository --entity ~.domain.Base +repository jpa --interface ~.repository.PizzaRepository --entity ~.domain.Pizza +repository jpa --interface ~.repository.PizzaOrderRepository --entity ~.domain.PizzaOrder + +// Define a service/facade layer +service type --interface ~.service.ToppingService --entity ~.domain.Topping +service type --interface ~.service.BaseService --entity ~.domain.Base +service type --interface ~.service.PizzaService --entity ~.domain.Pizza +service type --interface ~.service.PizzaOrderService --entity ~.domain.PizzaOrder + +// Offer JSON remoting for all domain types through Spring MVC +json all --deepSerialize +web mvc json setup +web mvc json all --package ~.web + +web mvc setup +web mvc all --package ~.web + +// Example scripts for JSON remoting: +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{name: "Thin Crust"}' http://localhost:8080/pizzashop/bases +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '[{name: "Cheesy Crust"},{name: "Thick Crust"}]' http://localhost:8080/pizzashop/bases/jsonArray +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '[{name: "Fresh Tomato"},{name: "Prawns"},{name: "Mozarella"},{name: "Bogus"}]' http://localhost:8080/pizzashop/toppings/jsonArray +// curl -i -X DELETE -H "Accept: application/json" http://localhost:8080/pizzashop/toppings/7 +// curl -i -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -d '{id:6,name:"Mozzarella",version:1}' http://localhost:8080/pizzashop/toppings +// curl -i -H "Accept: application/json" http://localhost:8080/pizzashop/toppings +// curl -i -H "Accept: application/json" http://localhost:8080/pizzashop/toppings/6 +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{name:"Napolitana",price:7.5,base:{id:1},toppings:[{name: "Anchovy fillets"},{name: "Mozzarella"}]}' http://localhost:8080/pizzashop/pizzas +// curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{name:"Stefan",total:7.5,address:"Sydney, AU",deliveryDate:1314595427866,id:{shopCountry:"AU",shopCity:"Sydney",shopName:"Pizza Pan 1"},pizzas:[{id:8,version:1}]}' http://localhost:8080/pizzashop/pizzaorders diff --git a/classpath/src/main/resources/vote.roo b/classpath/src/main/resources/vote.roo new file mode 100644 index 000000000..dd880689f --- /dev/null +++ b/classpath/src/main/resources/vote.roo @@ -0,0 +1,28 @@ +project --topLevelPackage com.springsource.vote + +jpa setup --provider HIBERNATE --database HYPERSONIC_PERSISTENT + +entity jpa --class ~.domain.Choice --testAutomatically +field string namingChoice --notNull --sizeMin 1 --sizeMax 30 +field string description --sizeMax 80 +web mvc setup +web mvc scaffold ~.web.ChoiceController + +entity jpa --class Vote --testAutomatically +field reference choice --type Choice +field string ip --notNull --sizeMin 7 --sizeMax 15 +field date registered --type java.util.Date --notNull --past +web mvc scaffold ~.web.VoteController + +web mvc controller ~.web.PublicVoteController --preferredMapping /public + +web mvc language --code de +web mvc language --code es + +logging setup --level WARN --package WEB + +security setup + +finder list --class ~.domain.Vote --depth 2 --filter reg,betw,IpEq + +logging setup --level INFO \ No newline at end of file diff --git a/classpath/src/main/resources/wedding.roo b/classpath/src/main/resources/wedding.roo new file mode 100644 index 000000000..87876b289 --- /dev/null +++ b/classpath/src/main/resources/wedding.roo @@ -0,0 +1,31 @@ +project --topLevelPackage com.wedding +jpa setup --provider OPENJPA --database HYPERSONIC_PERSISTENT +database properties list +database properties set --key database.url --value jdbc:hsqldb:${user.home}/my-wedding +database properties list + +entity jpa --class ~.domain.Rsvp --testAutomatically +field string code --notNull --sizeMin 1 --sizeMax 30 +field string email --sizeMax 30 +field number attending --type java.lang.Integer +field string specialRequests --sizeMax 100 +field date confirmed --type java.util.Date + +web mvc setup +web mvc scaffold ~.web.RsvpController +selenium test --controller ~.web.RsvpController + +// (OPTION: quit, mvn test, mvn tomcat:run, localhost:8080/wedding, mvn selenium:selenese) + +logging setup --level ERROR --package WEB +security setup + +web mvc controller --class ~.web.PublicRsvpController +finder list --class ~.domain.Rsvp --filter code,equ +finder add --finderName findRsvpsByCodeEquals + +email sender setup --hostServer 127.0.0.1 +field email template --class ~.web.PublicRsvpController + +// Complete manual configuration as described at http://blog.springsource.com/roo-part-2/ +// Start from the "Final Steps" section, towards the bottom of the blog entry diff --git a/classpath/src/test/java/org/springframework/roo/classpath/PhysicalTypeIdentifierNamingUtilsTest.java b/classpath/src/test/java/org/springframework/roo/classpath/PhysicalTypeIdentifierNamingUtilsTest.java new file mode 100644 index 000000000..70a89b2ae --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/PhysicalTypeIdentifierNamingUtilsTest.java @@ -0,0 +1,48 @@ +package org.springframework.roo.classpath; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; + +/** + * Unit test of {@link PhysicalTypeIdentifierNamingUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PhysicalTypeIdentifierNamingUtilsTest { + + // instance ID + private static final String METADATA_CLASS_ID = "MID:org.springframework.roo.addon.plural.PluralMetadata"; + private static final String METADATA_INSTANCE_ID = "MID:org.springframework.roo.addon.plural.PluralMetadata#core|SRC_MAIN_JAVA?com.example.domain.Thing"; + private static final String MODULE = "core"; // Same as in the above + // instance ID + private static final Path PATH = Path.SRC_MAIN_JAVA; // Same as in the below + + @Test + public void testGetLogicalPathFromMetadataInstanceId() { + // Invoke + final LogicalPath logicalPath = PhysicalTypeIdentifierNamingUtils + .getPath(METADATA_INSTANCE_ID); + + // Check + assertEquals(LogicalPath.getInstance(PATH, MODULE), logicalPath); + } + + @Test + public void testGetModuleNameFromMetadataInstanceId() { + // Invoke + final String module = PhysicalTypeIdentifierNamingUtils + .getModule(METADATA_INSTANCE_ID); + + // Check + assertEquals(MODULE, module); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetPathFromMetadataClassId() { + PhysicalTypeIdentifierNamingUtils.getPath(METADATA_CLASS_ID); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/PhysicalTypeIdentifierTest.java b/classpath/src/test/java/org/springframework/roo/classpath/PhysicalTypeIdentifierTest.java new file mode 100644 index 000000000..4593dc1a2 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/PhysicalTypeIdentifierTest.java @@ -0,0 +1,33 @@ +package org.springframework.roo.classpath; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.Path; + +/** + * Unit test of {@link PhysicalTypeIdentifier} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PhysicalTypeIdentifierTest { + + private static final String USER_PROJECT_TYPE = "com.foo.Bar"; + + @Test + public void testGetJavaType() { + // Set up + final String metadataId = "MID:" + + PhysicalTypeIdentifier.class.getName() + "#" + + Path.SRC_MAIN_JAVA + "?" + USER_PROJECT_TYPE; + + // Invoke + final JavaType javaType = PhysicalTypeIdentifier + .getJavaType(metadataId); + + // Check + assertEquals(USER_PROJECT_TYPE, javaType.getFullyQualifiedTypeName()); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/TypeLocationServiceImplTest.java b/classpath/src/test/java/org/springframework/roo/classpath/TypeLocationServiceImplTest.java new file mode 100644 index 000000000..d737a48cf --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/TypeLocationServiceImplTest.java @@ -0,0 +1,62 @@ +package org.springframework.roo.classpath; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * Unit test of {@link TypeLocationServiceImpl}. + * + * @author Andrew Swan + * @since 1.3.0 + */ +public class TypeLocationServiceImplTest extends TestCase { + + public void testGetAllPackages() { + // Set up + final String leafPackage = "com.foo.bar"; + + // Invoke + final Set allPackages = TypeLocationServiceImpl + .getAllPackages(leafPackage); + + // Check + assertEquals(3, allPackages.size()); + assertTrue(allPackages.contains("com")); + assertTrue(allPackages.contains("com.foo")); + assertTrue(allPackages.contains("com.foo.bar")); + } + + public void testGetPackageFromType() { + // Set up + final String type = "com.foo.Bar"; + + // Invoke + final String pkg = TypeLocationServiceImpl.getPackageFromType(type); + + // Check + assertEquals("com.foo", pkg); + } + + public void testGetLowestCommonPackageWhenOneExists() { + // Set up + final String type1 = "com.foo.bar.A"; + final String type2 = "com.foo.baz.B"; + final Map> typesByPackage = new LinkedHashMap>(); + typesByPackage.put("com", Arrays.asList(type1, type2)); + typesByPackage.put("com.foo", Arrays.asList(type1, type2)); + typesByPackage.put("com.foo.bar", Arrays.asList(type1)); + typesByPackage.put("com.foo.baz", Arrays.asList(type2)); + + // Invoke + final String lowestCommonPackage = TypeLocationServiceImpl + .getLowestCommonPackage(2, typesByPackage); + + // Check + assertEquals("com.foo", lowestCommonPackage); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/converters/JavaPackageConverterTest.java b/classpath/src/test/java/org/springframework/roo/classpath/converters/JavaPackageConverterTest.java new file mode 100644 index 000000000..80da6cd40 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/converters/JavaPackageConverterTest.java @@ -0,0 +1,197 @@ +package org.springframework.roo.classpath.converters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.roo.classpath.converters.JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; + +/** + * Unit test of {@link JavaPackageConverter} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JavaPackageConverterTest { + + private static final String TOP_LEVEL_PACKAGE = "com.example"; + + // Fixture + private JavaPackageConverter converter; + private @Mock LastUsed mockLastUsed; + private @Mock ProjectOperations mockProjectOperations; + private @Mock TypeLocationService mockTypeLocationService; + + /** + * Asserts that converting the given text in the given option context + * results in the expected package name + * + * @param text + * @param optionContext + * @param expectedPackage + */ + private void assertConvertFromValidText(final String text, + final String optionContext, final String expectedPackage) { + // Set up + when(mockProjectOperations.isFocusedProjectAvailable()) + .thenReturn(true); + final Pom mockPom = mock(Pom.class); + when(mockProjectOperations.getFocusedModule()).thenReturn(mockPom); + when(mockTypeLocationService.getTopLevelPackageForModule(mockPom)) + .thenReturn(TOP_LEVEL_PACKAGE); + assertEquals( + expectedPackage, + converter.convertFromText(text, JavaPackage.class, + optionContext).getFullyQualifiedPackageName()); + } + + /** + * Asserts that when the converter is asked for possible completions, the + * expected completions are provided. + * + * @param projectAvailable + * @param expectedAllComplete + * @param expectedCompletions + */ + private void assertGetAllPossibleValues(final boolean projectAvailable, + final Completion... expectedCompletions) { + // Set up + when(mockProjectOperations.isFocusedProjectAvailable()).thenReturn( + projectAvailable); + final List completions = new ArrayList(); + + // Invoke + final boolean allComplete = converter.getAllPossibleValues(completions, + JavaPackage.class, null, null, null); + + // Check + assertEquals(false, allComplete); + assertEquals(Arrays.asList(expectedCompletions), completions); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + converter = new JavaPackageConverter(); + converter.lastUsed = mockLastUsed; + converter.projectOperations = mockProjectOperations; + converter.typeLocationService = mockTypeLocationService; + } + + private Pom setUpMockPom(final String path, final JavaType... types) { + final Pom mockPom = mock(Pom.class); + when(mockPom.getPath()).thenReturn(path); + when(mockTypeLocationService.getTypesForModule(mockPom)).thenReturn( + Arrays.asList(types)); + return mockPom; + } + + @Test + public void testConvertFromBlankText() { + assertNull(converter + .convertFromText(" \n\r\t", JavaPackage.class, null)); + } + + @Test + public void testConvertFromCompoundPackageNameInUpdateContext() { + assertConvertFromValidText("COM.example", "update", TOP_LEVEL_PACKAGE); + verify(mockLastUsed).setPackage(new JavaPackage(TOP_LEVEL_PACKAGE)); + } + + @Test + public void testConvertFromEmptyText() { + assertNull(converter.convertFromText("", JavaPackage.class, null)); + } + + @Test + public void testConvertFromNullText() { + assertNull(converter.convertFromText(null, JavaPackage.class, null)); + } + + @Test + public void testConvertFromSimplePackageNameInNoContext() { + assertEquals("foo", + converter.convertFromText("FOO", JavaPackage.class, null) + .getFullyQualifiedPackageName()); + verifyNoMoreInteractions(mockLastUsed); + } + + @Test + public void testConvertFromSimplePackageNameWithTrailingDotInNonUpdateContext() { + assertConvertFromValidText("FOO.", "create", "foo"); + verifyNoMoreInteractions(mockLastUsed); + } + + @Test + public void testConvertFromTextThatIsTopLevelPackageSymbol() { + assertConvertFromValidText(TOP_LEVEL_PACKAGE_SYMBOL, null, + TOP_LEVEL_PACKAGE); + verifyNoMoreInteractions(mockLastUsed); + } + + @Test + public void testConvertFromTextThatStartsWithTopLevelPackageSymbolPlusDot() { + assertConvertFromValidText("~.Domain", null, TOP_LEVEL_PACKAGE + + ".domain"); + verifyNoMoreInteractions(mockLastUsed); + } + + @Test + public void testConvertFromTextThatStartsWithTopLevelPackageSymbolPlusNoDot() { + assertConvertFromValidText(TOP_LEVEL_PACKAGE_SYMBOL + "Domain", null, + TOP_LEVEL_PACKAGE + ".domain"); + verifyNoMoreInteractions(mockLastUsed); + } + + @Test + public void testConvertFromTextWithTrailingDot() { + assertConvertFromValidText("~.Domain.", null, TOP_LEVEL_PACKAGE + + ".domain"); + verifyNoMoreInteractions(mockLastUsed); + } + + @Test + public void testGetAllPossibleValuesWhenProjectIsAvailable() { + // Set up + final Pom mockPom1 = setUpMockPom("/path/to/pom/1", new JavaType( + "com.example.domain.Choice"), new JavaType( + "com.example.domain.Vote")); + final Pom mockPom2 = setUpMockPom("/path/to/pom/2", new JavaType( + "com.example.web.ChoiceController"), new JavaType( + "com.example.web.VoteController")); + when(mockProjectOperations.getPoms()).thenReturn( + Arrays.asList(mockPom1, mockPom2)); + + // Invoke and check + assertGetAllPossibleValues(true, new Completion("com.example.domain"), + new Completion("com.example.web")); + } + + @Test + public void testGetAllPossibleValuesWhenProjectNotAvailable() { + assertGetAllPossibleValues(false); + } + + @Test + public void testSupportsJavaPackage() { + assertTrue(converter.supports(JavaPackage.class, null)); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/converters/JavaTypeConverterTest.java b/classpath/src/test/java/org/springframework/roo/classpath/converters/JavaTypeConverterTest.java new file mode 100644 index 000000000..c333ca40d --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/converters/JavaTypeConverterTest.java @@ -0,0 +1,281 @@ +package org.springframework.roo.classpath.converters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.LogicalPath.MODULE_PATH_SEPARATOR; +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_CYAN; +import static org.springframework.roo.support.util.AnsiEscapeCode.decorate; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.OptionContexts; +import org.springframework.roo.support.util.AnsiEscapeCode; + +/** + * Unit test of {@link JavaTypeConverter} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JavaTypeConverterTest { + + // Fixture + private JavaTypeConverter converter; + @Mock FileManager mockFileManager; + @Mock LastUsed mockLastUsed; + @Mock ProjectOperations mockProjectOperations; + @Mock TypeLocationService mockTypeLocationService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + converter = new JavaTypeConverter(); + converter.fileManager = mockFileManager; + converter.lastUsed = mockLastUsed; + converter.projectOperations = mockProjectOperations; + converter.typeLocationService = mockTypeLocationService; + } + + @Test + public void testConvertAsteriskWhenLastUsedTypeIsKnown() { + // Set up + final JavaType mockLastUsedType = mock(JavaType.class); + when(mockLastUsed.getJavaType()).thenReturn(mockLastUsedType); + + // Invoke and check + assertEquals(mockLastUsedType, converter.convertFromText( + JavaTypeConverter.LAST_USED_INDICATOR, null, null)); + } + + @Test(expected = IllegalStateException.class) + public void testConvertAsteriskWhenLastUsedTypeIsUnknown() { + converter.convertFromText(JavaTypeConverter.LAST_USED_INDICATOR, null, + null); + } + + @Test + public void testConvertEmptyString() { + assertNull(converter.convertFromText("", null, null)); + } + + @Test + public void testConvertFullyQualifiedValueWithOneModulePrefix() { + // Set up + final String moduleName = "web"; + final Pom mockWebPom = mock(Pom.class); + when(mockProjectOperations.getPomFromModuleName(moduleName)) + .thenReturn(mockWebPom); + final String topLevelPackage = "com.example.app.mvc"; + when(mockTypeLocationService.getTopLevelPackageForModule(mockWebPom)) + .thenReturn(topLevelPackage); + + // Invoke + final JavaType result = converter.convertFromText(moduleName + + MODULE_PATH_SEPARATOR + topLevelPackage + + ".pet.PetController", null, null); + + // Check + assertEquals("com.example.app.mvc.pet.PetController", + result.getFullyQualifiedTypeName()); + } + + @Test + public void testConvertNullString() { + assertNull(converter.convertFromText(null, null, null)); + } + + @Test + public void testConvertTopLevelPackageWithOneModulePrefix() { + // Set up + final String moduleName = "web"; + final Pom mockWebPom = mock(Pom.class); + when(mockProjectOperations.getPomFromModuleName(moduleName)) + .thenReturn(mockWebPom); + final String topLevelPackage = "com.example.app.mvc"; + when(mockTypeLocationService.getTopLevelPackageForModule(mockWebPom)) + .thenReturn(topLevelPackage); + + // Invoke + final JavaType result = converter.convertFromText(moduleName + + MODULE_PATH_SEPARATOR + topLevelPackage, null, null); + + // Check + assertNull(result); + } + + @Test + public void testConvertToPrimitiveByte() { + assertEquals(JavaType.BYTE_PRIMITIVE, + converter.convertFromText("byte", null, null)); + } + + @Test + public void testConvertToPrimitiveDouble() { + assertEquals(JavaType.DOUBLE_PRIMITIVE, + converter.convertFromText("double", null, null)); + } + + @Test + public void testConvertToPrimitiveFloat() { + assertEquals(JavaType.FLOAT_PRIMITIVE, + converter.convertFromText("float", null, null)); + } + + @Test + public void testConvertToPrimitiveInt() { + assertEquals(JavaType.INT_PRIMITIVE, + converter.convertFromText("int", null, null)); + } + + @Test + public void testConvertToPrimitiveLong() { + assertEquals(JavaType.LONG_PRIMITIVE, + converter.convertFromText("long", null, null)); + } + + @Test + public void testConvertToPrimitiveShort() { + assertEquals(JavaType.SHORT_PRIMITIVE, + converter.convertFromText("short", null, null)); + } + + @Test + public void testConvertWhitespace() { + assertNull(converter.convertFromText(" \n\r\t", null, null)); + } + + @Test + public void testGetAllPossibleValuesInProjectWhenModulePrefixIsUsed() { + // Set up + @SuppressWarnings("unchecked") + final List mockCompletions = mock(List.class); + when(mockProjectOperations.isFocusedProjectAvailable()) + .thenReturn(true); + final String otherModuleName = "core"; + final Pom mockOtherModule = mock(Pom.class); + when(mockOtherModule.getModuleName()).thenReturn(otherModuleName); + when(mockProjectOperations.getPomFromModuleName(otherModuleName)) + .thenReturn(mockOtherModule); + final String topLevelPackage = "com.example"; + when( + mockTypeLocationService + .getTopLevelPackageForModule(mockOtherModule)) + .thenReturn(topLevelPackage); + final String focusedModuleName = "web"; + when(mockProjectOperations.getModuleNames()).thenReturn( + Arrays.asList(focusedModuleName, otherModuleName)); + final String modulePath = "/path/to/it"; + when(mockOtherModule.getPath()).thenReturn(modulePath); + final JavaType type1 = new JavaType("com.example.web.ShouldBeFound"); + final JavaType type2 = new JavaType("com.example.foo.ShouldNotBeFound"); + when(mockTypeLocationService.getTypesForModule(mockOtherModule)) + .thenReturn(Arrays.asList(type1, type2)); + + // Invoke + converter.getAllPossibleValues(mockCompletions, JavaType.class, + otherModuleName + MODULE_PATH_SEPARATOR + "~.web", + OptionContexts.PROJECT, null); + + // Check + verify(mockCompletions).add( + new Completion(focusedModuleName + MODULE_PATH_SEPARATOR, + AnsiEscapeCode + .decorate(focusedModuleName + + MODULE_PATH_SEPARATOR, + AnsiEscapeCode.FG_CYAN), "Modules", 0)); + // prefix + topLevelPackage, formattedPrefix + topLevelPackage, heading + final String formattedPrefix = decorate(otherModuleName + + MODULE_PATH_SEPARATOR, FG_CYAN); + final String prefix = otherModuleName + MODULE_PATH_SEPARATOR; + verify(mockCompletions).add( + new Completion(prefix + topLevelPackage, formattedPrefix + + topLevelPackage, "", 1)); + verify(mockCompletions).add( + new Completion(prefix + "~.web.ShouldBeFound", formattedPrefix + + "~.web.ShouldBeFound", "", 1)); + verifyNoMoreInteractions(mockCompletions); + } + + @Test + public void testGetAllPossibleValuesInProjectWhenNoModuleHasFocus() { + // Set up + @SuppressWarnings("unchecked") + final List mockCompletions = mock(List.class); + + // Invoke + converter.getAllPossibleValues(mockCompletions, JavaType.class, "", + OptionContexts.PROJECT, null); + + // Check + verifyNoMoreInteractions(mockCompletions); + } + + @Test + public void testGetAllPossibleValuesInProjectWhenNoModulePrefixIsUsed() { + // Set up + @SuppressWarnings("unchecked") + final List mockCompletions = mock(List.class); + when(mockProjectOperations.isFocusedProjectAvailable()) + .thenReturn(true); + final Pom mockFocusedModule = mock(Pom.class); + when(mockProjectOperations.getFocusedModule()).thenReturn( + mockFocusedModule); + final String topLevelPackage = "com.example"; + when( + mockTypeLocationService + .getTopLevelPackageForModule(mockFocusedModule)) + .thenReturn(topLevelPackage); + final String focusedModuleName = "web"; + when(mockFocusedModule.getModuleName()).thenReturn(focusedModuleName); + final String modulePath = "/path/to/it"; + when(mockFocusedModule.getPath()).thenReturn(modulePath); + final String otherModuleName = "core"; + when(mockProjectOperations.getModuleNames()).thenReturn( + Arrays.asList(focusedModuleName, otherModuleName)); + final JavaType type1 = new JavaType("com.example.Foo"); + final JavaType type2 = new JavaType("com.example.sub.Bar"); + when(mockTypeLocationService.getTypesForModule(mockFocusedModule)) + .thenReturn(Arrays.asList(type1, type2)); + + // Invoke + converter.getAllPossibleValues(mockCompletions, JavaType.class, "", + OptionContexts.PROJECT, null); + + // Check + verify(mockCompletions).add( + new Completion(otherModuleName + MODULE_PATH_SEPARATOR, + AnsiEscapeCode + .decorate(otherModuleName + + MODULE_PATH_SEPARATOR, + AnsiEscapeCode.FG_CYAN), "Modules", 0)); + verify(mockCompletions).add( + new Completion(topLevelPackage, topLevelPackage, + focusedModuleName, 1)); + verify(mockCompletions).add( + new Completion("~.Foo", "~.Foo", focusedModuleName, 1)); + verify(mockCompletions).add( + new Completion("~.sub.Bar", "~.sub.Bar", focusedModuleName, 1)); + verifyNoMoreInteractions(mockCompletions); + } + + @Test + public void testSupportsJavaType() { + assertTrue(converter.supports(JavaType.class, null)); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetailsBuilderTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetailsBuilderTest.java new file mode 100644 index 000000000..294664d4f --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/AbstractMemberHoldingTypeDetailsBuilderTest.java @@ -0,0 +1,161 @@ +package org.springframework.roo.classpath.details; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link AbstractMemberHoldingTypeDetailsBuilder} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class AbstractMemberHoldingTypeDetailsBuilderTest { + + /** + * Testable subclass of {@link AbstractMemberHoldingTypeDetailsBuilder}, see + * http + * ://stackoverflow.com/questions/1087339/using-mockito-to-test-abstract- + * classes + */ + private static class TestableBuilder + extends + AbstractMemberHoldingTypeDetailsBuilder { + + /** + * Constructor + */ + protected TestableBuilder() { + super(DECLARED_BY_MID); + } + + @Override + public void addImports(final Collection imports) { + } + + public ClassOrInterfaceTypeDetails build() { + // This method can be spied upon, see the StackOverflow link above + return null; + } + } + + private static final String DECLARED_BY_MID = "MID:foo#bar"; + + // Fixture + private AbstractMemberHoldingTypeDetailsBuilder builder; + + @Before + public void setUp() { + builder = new TestableBuilder(); + } + + @Test + public void testAddConstructorAfterSettingUnmodifiableCollection() { + // Set up + final ConstructorMetadataBuilder mockConstructor1 = mock(ConstructorMetadataBuilder.class); + final ConstructorMetadataBuilder mockConstructor2 = mock(ConstructorMetadataBuilder.class); + when(mockConstructor2.getDeclaredByMetadataId()).thenReturn( + DECLARED_BY_MID); + + // Invoke + builder.setDeclaredConstructors(Collections.singleton(mockConstructor1)); + final boolean added = builder.addConstructor(mockConstructor2); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(mockConstructor1, mockConstructor2), + builder.getDeclaredConstructors()); + } + + @Test + public void testAddExtendsTypeAfterSettingUnmodifiableCollection() { + // Set up + final JavaType mockType1 = mock(JavaType.class); + final JavaType mockType2 = mock(JavaType.class); + + // Invoke + builder.setExtendsTypes(Collections.singleton(mockType1)); + final boolean added = builder.addExtendsTypes(mockType2); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(mockType1, mockType2), + builder.getExtendsTypes()); + } + + @Test + public void testAddFieldAfterSettingUnmodifiableCollection() { + // Set up + final FieldMetadataBuilder mockField1 = mock(FieldMetadataBuilder.class); + final FieldMetadataBuilder mockField2 = mock(FieldMetadataBuilder.class); + when(mockField2.getDeclaredByMetadataId()).thenReturn(DECLARED_BY_MID); + + // Invoke + builder.setDeclaredFields(Collections.singleton(mockField1)); + final boolean added = builder.addField(mockField2); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(mockField1, mockField2), + builder.getDeclaredFields()); + } + + @Test + public void testAddImplementsTypeAfterSettingUnmodifiableCollection() { + // Set up + final JavaType mockType1 = mock(JavaType.class); + final JavaType mockType2 = mock(JavaType.class); + + // Invoke + builder.setImplementsTypes(Collections.singleton(mockType1)); + final boolean added = builder.addImplementsType(mockType2); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(mockType1, mockType2), + builder.getImplementsTypes()); + } + + @Test + public void testAddInitializerAfterSettingUnmodifiableCollection() { + // Set up + final InitializerMetadataBuilder mockInitializer1 = mock(InitializerMetadataBuilder.class); + final InitializerMetadataBuilder mockInitializer2 = mock(InitializerMetadataBuilder.class); + when(mockInitializer2.getDeclaredByMetadataId()).thenReturn( + DECLARED_BY_MID); + + // Invoke + builder.setDeclaredInitializers(Collections.singleton(mockInitializer1)); + final boolean added = builder.addInitializer(mockInitializer2); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(mockInitializer1, mockInitializer2), + builder.getDeclaredInitializers()); + } + + @Test + public void testAddInnerTypeAfterSettingUnmodifiableCollection() { + // Set up + final ClassOrInterfaceTypeDetailsBuilder mockInnerType1 = mock(ClassOrInterfaceTypeDetailsBuilder.class); + final ClassOrInterfaceTypeDetailsBuilder mockInnerType2 = mock(ClassOrInterfaceTypeDetailsBuilder.class); + + // Invoke + builder.setDeclaredInnerTypes(Collections.singleton(mockInnerType1)); + final boolean added = builder.addInnerType(mockInnerType2); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(mockInnerType1, mockInnerType2), + builder.getDeclaredInnerTypes()); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/DefaultItdTypeDetailsTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/DefaultItdTypeDetailsTest.java new file mode 100644 index 000000000..5ae6ba4ed --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/DefaultItdTypeDetailsTest.java @@ -0,0 +1,106 @@ +package org.springframework.roo.classpath.details; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.roo.classpath.PhysicalTypeCategory; +import org.springframework.roo.model.CustomData; +import org.springframework.roo.model.DataType; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link DefaultItdTypeDetails} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DefaultItdTypeDetailsTest extends ItdTypeDetailsTestCase { + + private static final String MINIMAL_ITD = "// WARNING: DO NOT EDIT THIS FILE. THIS FILE IS MANAGED BY SPRING ROO.\n" + + "// You may push code into the target .java compilation unit if you wish to edit any member(s).\n" + + "\n" + + "package com.foo.bar;\n" + + "\n" + + "import com.foo.bar.Person;\n" + + "\n" + + "aspect Person_Roo_Extra {\n" + " \n" + "}\n"; + + @Test + public void testMinimalInstance() { + // Set up + final boolean privilegedAspect = false; + final int modifier = 42; + + final ClassOrInterfaceTypeDetails mockGovernor = mock(ClassOrInterfaceTypeDetails.class); + when(mockGovernor.getPhysicalTypeCategory()).thenReturn( + PhysicalTypeCategory.CLASS); + + final JavaPackage mockPackage = mock(JavaPackage.class); + when(mockPackage.getFullyQualifiedPackageName()).thenReturn( + "com.foo.bar"); + + final JavaType mockGovernorType = mock(JavaType.class); + when(mockGovernorType.getPackage()).thenReturn(mockPackage); + when(mockGovernor.getType()).thenReturn(mockGovernorType); + when(mockGovernorType.getSimpleTypeName()).thenReturn("Person"); + when(mockGovernorType.getFullyQualifiedTypeName()).thenReturn( + "com.foo.bar.Person"); + when(mockGovernorType.getDataType()).thenReturn(DataType.TYPE); + + final CustomData mockCustomData = mock(CustomData.class); + + final JavaType mockAspectType = mock(JavaType.class); + when(mockAspectType.getPackage()).thenReturn(mockPackage); + when(mockAspectType.isDefaultPackage()).thenReturn(false); + when(mockAspectType.getSimpleTypeName()).thenReturn("Person_Roo_Extra"); + + final JavaType mockImportType = mock(JavaType.class); + when(mockImportType.getSimpleTypeName()).thenReturn("Person"); + when(mockImportType.getFullyQualifiedTypeName()).thenReturn( + "com.foo.bar.Person"); + when(mockImportType.getDataType()).thenReturn(DataType.TYPE); + + final String declaredByMetadataId = "MID:foo#bar"; + + // Invoke + final DefaultItdTypeDetails itd = new DefaultItdTypeDetails( + mockCustomData, declaredByMetadataId, modifier, mockGovernor, + mockAspectType, privilegedAspect, + Arrays.asList(mockImportType), null, null, null, null, null, + null, null, null, null, null); + + // Check + assertEquals(0, itd.getAnnotations().size()); + assertEquals(0, itd.getDeclaredConstructors().size()); + assertEquals(0, itd.getDeclaredFields().size()); + assertEquals(0, itd.getDeclaredInitializers().size()); + assertEquals(0, itd.getDeclaredInnerTypes().size()); + assertEquals(0, itd.getDeclaredMethods().size()); + assertEquals(0, itd.getExtendsTypes().size()); + assertEquals(0, itd.getFieldAnnotations().size()); + assertEquals(0, itd.getImplementsTypes().size()); + assertEquals(0, itd.getInnerTypes().size()); + assertEquals(0, itd.getDeclarePrecedence().size()); + assertEquals(0, itd.getMethodAnnotations().size()); + assertEquals(1, itd.getRegisteredImports().size()); + + assertEquals(mockAspectType, itd.getAspect()); + assertEquals(mockCustomData, itd.getCustomData()); + assertEquals(declaredByMetadataId, itd.getDeclaredByMetadataId()); + assertEquals(modifier, itd.getModifier()); + assertEquals(mockGovernorType, itd.getType()); + assertEquals(DefaultItdTypeDetails.PHYSICAL_TYPE_CATEGORY, + itd.getPhysicalTypeCategory()); + assertEquals(privilegedAspect, itd.isPrivilegedAspect()); + assertEquals(mockGovernor, itd.getGovernor()); + assertFalse(itd.extendsType(mock(JavaType.class))); + + assertOutput(MINIMAL_ITD, itd); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeMetadataTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeMetadataTest.java new file mode 100644 index 000000000..36dd7022c --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/DefaultPhysicalTypeMetadataTest.java @@ -0,0 +1,72 @@ +package org.springframework.roo.classpath.details; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.PhysicalTypeIdentifier; +import org.springframework.roo.classpath.itd.ItdMetadataProvider; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; + +/** + * Unit test of {@link DefaultPhysicalTypeMetadata} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DefaultPhysicalTypeMetadataTest { + + private static final String CANONICAL_PATH = "/usr/bob/projects/foo/Foo.java"; + private static final String METADATA_ID = PhysicalTypeIdentifier + .createIdentifier(new JavaType("com.example.Bar"), + LogicalPath.getInstance(Path.SRC_MAIN_JAVA, "")); + + // Fixture + private DefaultPhysicalTypeMetadata metadata; + @Mock private ClassOrInterfaceTypeDetails mockClassOrInterfaceTypeDetails; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + metadata = new DefaultPhysicalTypeMetadata(METADATA_ID, CANONICAL_PATH, + mockClassOrInterfaceTypeDetails); + } + + @Test + public void testGetItdCanoncialPath() { + // Set up + final ItdMetadataProvider mockItdMetadataProvider = mock(ItdMetadataProvider.class); + when(mockItdMetadataProvider.getItdUniquenessFilenameSuffix()) + .thenReturn("MySuffix"); + + // Invoke + final String itdCanonicalPath = metadata + .getItdCanoncialPath(mockItdMetadataProvider); + + // Check + assertEquals("/usr/bob/projects/foo/Foo_Roo_MySuffix.aj", + itdCanonicalPath); + } + + @Test + public void testGetItdCanonicalPath() { + // Set up + final ItdMetadataProvider mockItdMetadataProvider = mock(ItdMetadataProvider.class); + when(mockItdMetadataProvider.getItdUniquenessFilenameSuffix()) + .thenReturn("MySuffix"); + + // Invoke + final String itdCanonicalPath = metadata + .getItdCanonicalPath(mockItdMetadataProvider); + + // Check + assertEquals("/usr/bob/projects/foo/Foo_Roo_MySuffix.aj", + itdCanonicalPath); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/ImportMetadataBuilderTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/ImportMetadataBuilderTest.java new file mode 100644 index 000000000..cc38f11b6 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/ImportMetadataBuilderTest.java @@ -0,0 +1,35 @@ +package org.springframework.roo.classpath.details; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +import org.junit.Test; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link ImportMetadataBuilder} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ImportMetadataBuilderTest { + + private static final String CALLER_MID = "MID:foo#bar"; + + @Test + public void testGetImport() { + // Set up + final JavaType mockTypeToImport = mock(JavaType.class); + + // Invoke + final ImportMetadata importMetadata = ImportMetadataBuilder.getImport( + CALLER_MID, mockTypeToImport); + + // Check + assertEquals(mockTypeToImport, importMetadata.getImportType()); + assertEquals(0, importMetadata.getModifier()); + assertFalse(importMetadata.isAsterisk()); + assertFalse(importMetadata.isStatic()); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/ItdTypeDetailsTestCase.java b/classpath/src/test/java/org/springframework/roo/classpath/details/ItdTypeDetailsTestCase.java new file mode 100644 index 000000000..6515411b4 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/ItdTypeDetailsTestCase.java @@ -0,0 +1,32 @@ +package org.springframework.roo.classpath.details; + +import static org.junit.Assert.assertEquals; + +import org.springframework.roo.classpath.itd.ItdSourceFileComposer; + +/** + * Superclass for testing {@link ItdTypeDetails} instances and subclasses + * + * @author Andrew Swan + * @since 1.2.0 + */ +public abstract class ItdTypeDetailsTestCase { + + /** + * Asserts that the given ITD produces the given output + * + * @param expectedOutput the ITD's expected output + * @param itd the ITD to check (required) + */ + protected void assertOutput(final String expectedOutput, + final ItdTypeDetails itd) { + // Set up + final ItdSourceFileComposer composer = new ItdSourceFileComposer(itd); + + // Invoke + final String actualOutput = composer.getOutput(); + + // Check + assertEquals(expectedOutput, actualOutput); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/AnnotatedJavaTypeTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/AnnotatedJavaTypeTest.java new file mode 100644 index 000000000..cf703cfc0 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/AnnotatedJavaTypeTest.java @@ -0,0 +1,37 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link AnnotatedJavaType} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class AnnotatedJavaTypeTest { + + @Test + public void testConvertEmptyArrayOfJavaTypes() { + // Invoke + final List annotatedTypes = AnnotatedJavaType + .convertFromJavaTypes(); + + // Check + assertEquals(0, annotatedTypes.size()); + } + + @Test + public void testConvertNullListOfJavaTypes() { + // Invoke + final List annotatedTypes = AnnotatedJavaType + .convertFromJavaTypes((List) null); + + // Check + assertEquals(0, annotatedTypes.size()); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadataBuilderTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadataBuilderTest.java new file mode 100644 index 000000000..0559cad9b --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/AnnotationMetadataBuilderTest.java @@ -0,0 +1,40 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.junit.Assert.assertEquals; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JpaJavaType.ID; + +import org.junit.Test; + +/** + * Unit test of {@link AnnotationMetadataBuilder} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class AnnotationMetadataBuilderTest { + + @Test + public void testGetInstanceFromClassObject() { + // Invoke + final AnnotationMetadata annotationMetadata = AnnotationMetadataBuilder + .getInstance(String.class); + + // Check + assertEquals(0, annotationMetadata.getAttributeNames().size()); + assertEquals(STRING.getFullyQualifiedTypeName(), annotationMetadata + .getAnnotationType().getFullyQualifiedTypeName()); + } + + @Test + public void testGetInstanceFromFullyQualifiedClassName() { + // Invoke + final AnnotationMetadata annotationMetadata = AnnotationMetadataBuilder + .getInstance(ID); + + // Check + assertEquals(0, annotationMetadata.getAttributeNames().size()); + assertEquals(ID.getFullyQualifiedTypeName(), annotationMetadata + .getAnnotationType().getFullyQualifiedTypeName()); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/BooleanAttributeValueTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/BooleanAttributeValueTest.java new file mode 100644 index 000000000..d7de1198f --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/BooleanAttributeValueTest.java @@ -0,0 +1,27 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Unit test of {@link BooleanAttributeValue} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class BooleanAttributeValueTest { + + @Test + public void testToStringWhenFalse() { + assertEquals("foo -> false", new BooleanAttributeValue( + new JavaSymbolName("foo"), false).toString()); + } + + @Test + public void testToStringWhenTrue() { + assertEquals("bar -> true", new BooleanAttributeValue( + new JavaSymbolName("bar"), true).toString()); + } +} \ No newline at end of file diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/CharAttributeValueTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/CharAttributeValueTest.java new file mode 100644 index 000000000..953effaff --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/CharAttributeValueTest.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Unit test of {@link CharAttributeValue} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class CharAttributeValueTest { + + @Test + public void testToString() { + assertEquals("baz -> q", new CharAttributeValue(new JavaSymbolName( + "baz"), 'q').toString()); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/IntegerAttributeValueTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/IntegerAttributeValueTest.java new file mode 100644 index 000000000..199a0f008 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/IntegerAttributeValueTest.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Unit test of {@link IntegerAttributeValue} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class IntegerAttributeValueTest { + + @Test + public void testToString() { + assertEquals("answer -> 42", new IntegerAttributeValue( + new JavaSymbolName("answer"), 42).toString()); + } +} \ No newline at end of file diff --git a/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/LongAttributeValueTest.java b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/LongAttributeValueTest.java new file mode 100644 index 000000000..a81648c22 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/details/annotations/LongAttributeValueTest.java @@ -0,0 +1,21 @@ +package org.springframework.roo.classpath.details.annotations; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.roo.model.JavaSymbolName; + +/** + * Unit test of {@link LongAttributeValue} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class LongAttributeValueTest { + + @Test + public void testToString() { + assertEquals("beast -> 666", new LongAttributeValue(new JavaSymbolName( + "beast"), 666).toString()); + } +} \ No newline at end of file diff --git a/classpath/src/test/java/org/springframework/roo/classpath/layers/MemberTypeAdditionsTest.java b/classpath/src/test/java/org/springframework/roo/classpath/layers/MemberTypeAdditionsTest.java new file mode 100644 index 000000000..6f0762c06 --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/layers/MemberTypeAdditionsTest.java @@ -0,0 +1,68 @@ +package org.springframework.roo.classpath.layers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link MemberTypeAdditions} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MemberTypeAdditionsTest { + + /** + * Asserts that + * {@link MemberTypeAdditions#buildMethodCall(String, String, java.util.Iterator)} + * builds the expected method call from the given parameters + * + * @param expectedMethodCall + * @param target + * @param method + * @param parameterNames + */ + private void assertMethodCall(final String expectedMethodCall, + final String target, final String method, + final MethodParameter... parameters) { + assertEquals( + expectedMethodCall, + MemberTypeAdditions.buildMethodCall(target, method, + Arrays.asList(parameters))); + } + + @Test + public void testGetInvokedFieldWhenBuilderIsNull() { + // Set up + final MemberTypeAdditions memberTypeAdditions = new MemberTypeAdditions( + null, "foo", "foo()", false, null); + + // Invoke and check + assertNull(memberTypeAdditions.getInvokedField()); + } + + @Test + public void testGetMethodCallWithBlankTargetAndNoParameters() { + assertMethodCall("foo()", null, "foo"); + } + + @Test + public void testGetMethodCallWithBlankTargetAndTwoParameters() { + final MethodParameter firstNameParameter = new MethodParameter( + JavaType.STRING, "firstName"); + final MethodParameter lastNameParameter = new MethodParameter( + JavaType.STRING, "lastName"); + assertMethodCall("matchmakingService.marry(firstName, lastName)", + "matchmakingService", "marry", firstNameParameter, + lastNameParameter); + } + + @Test + public void testGetMethodCallWithTargetAndNoParameters() { + assertMethodCall("Foo.bar()", "Foo", "bar"); + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/operations/ClasspathOperationsImplTest.java b/classpath/src/test/java/org/springframework/roo/classpath/operations/ClasspathOperationsImplTest.java new file mode 100644 index 000000000..688dd9cde --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/operations/ClasspathOperationsImplTest.java @@ -0,0 +1,53 @@ +package org.springframework.roo.classpath.operations; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.classpath.TypeLocationService; +import org.springframework.roo.model.JavaType; + +/** + * Unit test of {@link ClasspathOperationsImpl} + * + * @author Andrew Swan + * @since 1.2.1 + */ +public class ClasspathOperationsImplTest { + + // Fixture + private ClasspathOperationsImpl classpathOperations; + @Mock private TypeLocationService mockTypeLocationService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + classpathOperations = new ClasspathOperationsImpl(); + classpathOperations.typeLocationService = mockTypeLocationService; + } + + @Test + public void testFocusOnTypeThatCannotBeLocated() { + // Set up + final JavaType mockType = mock(JavaType.class); + final String typeName = "com.example.domain.Lost"; + when(mockType.getFullyQualifiedTypeName()).thenReturn(typeName); + when(mockTypeLocationService.getPhysicalTypeIdentifier(mockType)) + .thenReturn(null); + + // Invoke and check + try { + classpathOperations.focus(mockType); + fail("Expected a " + NullPointerException.class); + } + catch (final NullPointerException expected) { + assertEquals("Cannot locate the type " + typeName, + expected.getMessage()); + } + } +} diff --git a/classpath/src/test/java/org/springframework/roo/classpath/preferences/PreferencesTest.java b/classpath/src/test/java/org/springframework/roo/classpath/preferences/PreferencesTest.java new file mode 100644 index 000000000..6dbf8da8e --- /dev/null +++ b/classpath/src/test/java/org/springframework/roo/classpath/preferences/PreferencesTest.java @@ -0,0 +1,50 @@ +package org.springframework.roo.classpath.preferences; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test of the {@link Preferences} class. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PreferencesTest { + + private static final String INVALID_KEY = "this-key-does-not-exist"; + + // Fixture + @Mock private java.util.prefs.Preferences mockPreferences; + private Preferences preferences; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + preferences = new Preferences(mockPreferences); + } + + @Test + public void testGetByteArrayWithNullKey() { + assertNull(preferences.getByteArray(null)); + } + + @Test + public void testGetByteArrayWithUnknownKeyAndNoDefault() { + // Set up + final byte[] expectedValue = { 1, 2, 3 }; // Arbitrary + when(mockPreferences.getByteArray(INVALID_KEY, new byte[0])) + .thenReturn(expectedValue); + + // Invoke + final byte[] actualValue = preferences.getByteArray(INVALID_KEY); + + // Check + assertSame(expectedValue, actualValue); + } +} diff --git a/deployment-support/eclipse-clean-up-config.xml b/deployment-support/eclipse-clean-up-config.xml new file mode 100644 index 000000000..45d9364d0 --- /dev/null +++ b/deployment-support/eclipse-clean-up-config.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deployment-support/eclipse-formatter-config.xml b/deployment-support/eclipse-formatter-config.xml new file mode 100644 index 000000000..d663a75c0 --- /dev/null +++ b/deployment-support/eclipse-formatter-config.xml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deployment-support/misc-bundle-deploy.sh b/deployment-support/misc-bundle-deploy.sh new file mode 100755 index 000000000..a33688508 --- /dev/null +++ b/deployment-support/misc-bundle-deploy.sh @@ -0,0 +1,153 @@ +#/bin/shell + +usage() { +cat << EOF +usage: $0 options + +OPTIONS: + -j JAR Maven JAR to upload (a .pom should be present as well) + -d Dry run + -v Verbose + -h Show this message + +DESCRIPTION: + Copies the designated Maven JAR and POM to a work directory, signs it + using GPG, and then uploads the resulting resources to + http://spring-roo-repository.springsource.org/bundles. + + This simplifies the production of OBR and RooBot compliant bundles, + assuming the designated Maven JAR is already a valid OSGi bundle. + + NOTE: You MUST ensure the -j JAR name is under a "repository" directory. + A valid POM should also exist at the same location. + +REQUIRES: + s3cmd (ls should list the SpringSource buckets) + ~/.m2/settings.xml contains a for the GPG key +EOF +} + +log() { + if [ "$VERBOSE" = "1" ]; then + echo "$@" + fi +} + +l_error() { + echo "### ERROR: $@" >&2 +} + +s3_execute() { + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$DRY_RUN" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS --dry-run" + fi + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi +} + +JAR= +VERBOSE='0' +DRY_RUN='0' +while getopts "j:vdh" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + j) + JAR=$OPTARG + ;; + d) + DRY_RUN=1 + ;; + v) + VERBOSE=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [[ -z $JAR ]]; then + usage + exit 1 +fi + +if [[ ! -f $JAR ]]; then + l_error "$JAR not found" + exit 1 +fi + +type -P gpg &>/dev/null || { l_error "gpg not found. Aborting." >&2; exit 1; } +type -P sha1sum &>/dev/null || { l_error "sha1sum not found. Aborting." >&2; exit 1; } + +PRG="$0" + +# File locations +WORK_DIR="/tmp/misc-bundle-deploy" +log "Work Dir.......: $WORK_DIR" +log "JAR............: $JAR" +POM=`echo "$JAR" | sed "s/.jar/.pom/"` +log "POM............: $POM" + +if [[ ! -f $POM ]]; then + l_error "$POM not found" + exit 1 +fi + +# Take a copy +rm -rf $WORK_DIR +mkdir -p $WORK_DIR +cp $JAR $WORK_DIR/ +cp $POM $WORK_DIR/ + +# Get final names +BASENAME_JAR=`echo $JAR | sed "s/.*\/repository\///"` +BASENAME_POM=`echo $POM | sed "s/.*\/repository\///"` +FINAL_JAR="$WORK_DIR/`basename $JAR`" +FINAL_POM="$WORK_DIR/`basename $POM`" +log "JAR Basename...: $BASENAME_JAR" +log "POM Basename...: $BASENAME_POM" +log "JAR Work.......: $FINAL_JAR" +log "POM Work.......: $FINAL_POM" + +# Sign the JAR +GPG_OPTS='-q' +if [ "$VERBOSE" = "1" ]; then + GPG_OPTS='-v' +fi +grep "" ~/.m2/settings.xml &>/dev/null +EXITED=$? +if [[ ! "$EXITED" = "1" ]]; then + log "Found gpg.passphrase in ~/.m2/settings.xml..." + PASSPHRASE=`grep "" ~/.m2/settings.xml | sed 's///' | sed 's/<\/gpg.passphrase>//' | sed 's/ //g'` + echo "$PASSPHRASE" | gpg $GPG_OPTS --batch --passphrase-fd 0 -a --output "$FINAL_JAR.asc" --detach-sign $FINAL_JAR +else + log "gpg.passphrase NOT found in ~/.m2/settings.xml. Trying with gpg agent." + gpg $GPG_OPTS -a --use-agent --output $ASSEMBLY_ASC --detach-sign $ASSEMBLY_ZIP +fi +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "GPG detached signature creation failed (gpg exit code $EXITED)." >&2; exit 1; +fi + +# Deploy +AWS_PREFIX='s3://spring-roo-repository.springsource.org/bundles' +s3_execute put --acl-public "$FINAL_JAR" "$AWS_PREFIX/$BASENAME_JAR" +s3_execute put --acl-public "$FINAL_POM" "$AWS_PREFIX/$BASENAME_POM" +s3_execute put --acl-public "$FINAL_JAR.asc" "$AWS_PREFIX/$BASENAME_JAR.asc" + +# Clean up +# rm -rf $WORK_DIR + diff --git a/deployment-support/pom.xml b/deployment-support/pom.xml new file mode 100644 index 000000000..40e4af114 --- /dev/null +++ b/deployment-support/pom.xml @@ -0,0 +1,218 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.root + 2.0.0.BUILD-SNAPSHOT + .. + + org.springframework.roo.deployment.support + pom + Spring Roo - Deployment Support + http://static.springframework.org/spring-roo/site/index.html + + + static.springsource.org + scp://static.springsource.org/var/www/domains/springsource.org/static/htdocs/spring-roo + + + + + + + com.agilejava.docbkx + docbkx-maven-plugin + 2.0.8 + + + single-page + + generate-html + + + ${project.basedir}/src/docbkx/resources/xsl/html.xsl + + + + + + + + + + + + + + + + + + + + + + + + pre-site + + + single-pdf + + generate-pdf + + + src/site/docbook/reference/ + src/docbkx/resources/images/ + + + + + pre-site + + + multi-page + + generate-html + + + true + ${project.basedir}/src/docbkx/resources/xsl/html_chunk.xsl + + + + + + + + + + + + + + + + + + + + + + + + pre-site + + + + + org.docbook + docbook-xml + 4.4 + runtime + + + org.apache.xmlgraphics + fop + 1.0 + + + + index.xml + false + + css/html.css + ${project.basedir}/src/site/docbook/reference + ${project.basedir}/src/docbkx/resources/xsl/fopdf.xsl + true + + + version + ${project.version} + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.3 + + + org.apache.maven.wagon + wagon-ssh + 2.2 + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.0 + + + org.asciidoctor + asciidoctorj + 1.5.2 + + + org.asciidoctor + asciidoctorj-pdf + 1.5.0-alpha.6 + + + + + output-html + generate-resources + + process-asciidoc + + + coderay + html5 + coderay + + ./images + left + font + true + + + - + true + + + + + generate-pdf-doc + generate-resources + + process-asciidoc + + + pdf + + coderay + + + + + - + + + + + + src/main/asciidoc + true + target/generated-docs + + + + + + true + + diff --git a/deployment-support/roo-bamboo-ci.sh b/deployment-support/roo-bamboo-ci.sh new file mode 100755 index 000000000..4a80a5122 --- /dev/null +++ b/deployment-support/roo-bamboo-ci.sh @@ -0,0 +1,234 @@ +#/bin/shell + +usage() { +cat << EOF +usage: $0 options + +OPTIONS: + -d Dry run (ie do not deploy or prune older releases) + -t Test assembly with default Maven repo (slow) + -T Test assembly with empty Maven repo (very slow, but thorough) + -v Verbose + -f Force execution, ignoring normal version checks + -h Show this message + +DESCRIPTION: + Drives the overall Hudson continuous integration process. This script + is designed so it can be manually executed by a committer if desired + for testing purposes. The script produces assembly filenames that + reflect local system time and a Git hash for easy identification. The + overall process is as follow, with any failure causing an early exit: + * Aborts if version != *.BUILD-SNAPSHOT (unless -f was indicated) + * Performs a "mvn clean package" from the project root + * Performs a "mvn clean site" from deployment-support + * Performs "roo-deploy-dist.sh assembly" (with -T/t if requested) + * Performs "mvn deploy" from the project root + * Performs "roo-deploy-dist.sh deploy" to release the assembly + * Removes older snapshot releases from S3 + +RETURNS: + 0 if successful or the version was != *.BUILD-SNAPSHOT + 1 if there was a failure of any kind + +REQUIRES: + See requirements in readme.txt for releasing (ie SSH, GPG etc) +EOF +} + +log() { + if [ "$VERBOSE" = "1" ]; then + echo "$@" + fi +} + +l_error() { + echo "### ERROR: $@" +} + +s3_execute() { + if [ "$DRY_RUN" = "0" ]; then + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi + fi +} + +VERBOSE='0' +DRY_RUN='0' +FORCE='0' +TEST='' +while getopts "vdhftT" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + d) + DRY_RUN=1 + ;; + v) + VERBOSE=1 + ;; + t) + TEST="-t" + ;; + T) + TEST="-T" + ;; + f) + FORCE=1 + ;; + ?) + usage + exit + ;; + esac +done + +type -P mvn &>/dev/null || { l_error "mvn not found. Aborting." >&2; exit 1; } + +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done +ROO_HOME=`dirname "$PRG"` + +# Absolute path +ROO_HOME=`cd "$ROO_HOME/.." ; pwd` +log "Location.......: $ROO_HOME" + +# Compute version and Git-related info +GIT_HASH=`git log "--pretty=format:%H" -n1 $ROO_HOME` +log "Git Hash.......: $GIT_HASH" +GIT_TAG=`git tag -l "*\.*\.*\.*" $ROO_HOME | tail -n 1` +log "Git Tag........: $GIT_TAG" +VERSION=`grep "" $ROO_HOME/pom.xml | head -n 1 | sed 's///' | sed 's/<\/version>//' | sed 's/ //g'` +log "Version........: $VERSION" +SHORT_VERSION=`echo $VERSION | sed 's/\([0-9].[0-9].[0-9]\).*\.BUILD-SNAPSHOT/\1/'` +log "Short Version..: $SHORT_VERSION" +TIMESTAMP=$(date --utc +"%Y%m%d.%H%M%S") +log "Timestamp......: $TIMESTAMP" + +# Determine the assembly filename (we don't rely on Hudson variables, as we'd like this to work for normal developers as well) +SUFFIX="_$TIMESTAMP-${GIT_HASH:0:7}" +log "Suffix.........: $SUFFIX" + +# Determine the version as required by the AWS dist.springframework.org "x-amz-meta-release.type" header +case $VERSION in + *BUILD-SNAPSHOT) TYPE=snapshot;; + *RC*) TYPE=milestone;; + *M*) TYPE=milestone;; + *RELEASE) TYPE=release;; + *) l_error "Unsupported release type ($VERSION). Aborting." >&2; exit 1;; +esac +log "Release Type...: $TYPE" + +# Gracefully abort if this isn't a BUILD-SNAPSHOT (unless the user has forced us to continue via -f) +if [[ "$FORCE" = "0" ]]; then + if [[ ! "$TYPE" = "snapshot" ]]; then + l_error "Gracefully aborting as not a snapshot ($VERSION)." >&2; exit 0; # code 0 is correct, this is not a serious error + fi +fi + +# Setup correct options for a dry run vs normal run +if [[ "$DRY_RUN" = "0" ]]; then + MAVEN_MAIN_OPTS='-e -B clean install' + MAVEN_SITE_OPTS='-e -B clean site' + MAVEN_DEPLOY_OPTS='-e -B deploy' + ROO_DEPLOY_OPTS='' +else + MAVEN_MAIN_OPTS='-e -B clean install' + MAVEN_SITE_OPTS='-e -B clean site' + MAVEN_DEPLOY_OPTS='never_invoked' + ROO_DEPLOY_OPTS='-d' +fi + +# Setup correct options for -v (verbose) (we default maven to quiet mode unless -v has been specified, as it's just too noisy) +if [[ "$VERBOSE" = "0" ]]; then + MAVEN_MAIN_OPTS="$MAVEN_MAIN_OPTS -q" + MAVEN_SITE_OPTS="$MAVEN_SITE_OPTS -q" + MAVEN_DEPLOY_OPTS="$MAVEN_DEPLOY_OPTS -q" +else + ROO_DEPLOY_OPTS="$ROO_DEPLOY_OPTS -v" +fi + +pushd $ROO_HOME/ &>/dev/null +# Do the initial mvn packaging (but don't dpeloy) +mvn $MAVEN_MAIN_OPTS -Pbamboo-build +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "Maven main build failed (exit code $EXITED)." >&2; exit 1; +fi + +# Build reference guide docs (and deploy them; it's not a big deal if the later tests fail but the docs were updated) +pushd $ROO_HOME/deployment-support &>/dev/null +mvn $MAVEN_SITE_OPTS -Pbamboo-build +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "Maven site build failed (exit code $EXITED)." >&2; exit 1; +fi + +# Build (and test if user used -T or -t) the assembly +./roo-bamboo-deploy-dist.sh -c assembly -s $SUFFIX $ROO_DEPLOY_OPTS $TEST +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "roo-bamboo-deploy -c assembly failed (exit code $EXITED)." >&2; exit 1; +fi + +# Deploy the Maven JARs (we do this first to avoid people getting the latest snapshot assembly ZIP before the latest annotation JAR is visible) +if [[ "$DRY_RUN" = "0" ]]; then + pushd $ROO_HOME/ &>/dev/null + mvn $MAVEN_DEPLOY_OPTS + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "Maven deploy failed (exit code $EXITED)." >&2; exit 1; + fi + popd &>/dev/null +fi + +# Deploy the assembly so people can download it (note roo-deploy-dist.sh will prune old snapshots from the download site automatically) +if [[ "$DRY_RUN" = "0" ]]; then + ./roo-deploy-dist.sh -c deploy -s $SUFFIX $ROO_DEPLOY_OPTS + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "roo-deploy -c deploy failed (exit code $EXITED)." >&2; exit 1; + fi +fi + +# Prune some old releases. We can rely on the fact CI runs at least every 24 hours and thus we can prune anything older than say 3 days +if [[ "$DRY_RUN" = "0" ]]; then + OK_DATE_0=`date +%Y-%m-%d` + OK_DATE_1=`date --date '-1 day' +%Y-%m-%d` + OK_DATE_2=`date --date '-2 day' +%Y-%m-%d` + log "Obtaining listing of all snapshot resources on S3" + s3_execute ls -r s3://spring-roo-repository.springsource.org/snapshot > /tmp/dist_snapshots.txt + log "Retain Dates...: $OK_DATE_0 $OK_DATE_1 $OK_DATE_2" + log "S3 Found.......: `grep -v "/$" /tmp/dist_snapshots.txt | wc -l`" + grep -v -e $OK_DATE_0 -e $OK_DATE_1 -e $OK_DATE_2 /tmp/dist_snapshots.txt > /tmp/dist_delete.txt + log "S3 To Delete...: `grep -v "/$" /tmp/dist_delete.txt | wc -l`" + cat /tmp/dist_delete.txt | cut -c "30-" > /tmp/dist_delete_cut.txt + for filename in `grep -v "/$" /tmp/dist_delete_cut.txt`; do + s3_execute del "$filename" + done + log "Pruning old snapshots completed successfully" +fi + +# Return to the original directory +popd &>/dev/null +popd &>/dev/null + diff --git a/deployment-support/roo-bamboo-deploy-dist.sh b/deployment-support/roo-bamboo-deploy-dist.sh new file mode 100755 index 000000000..5960f1da1 --- /dev/null +++ b/deployment-support/roo-bamboo-deploy-dist.sh @@ -0,0 +1,644 @@ +#/bin/shell + +usage() { +cat << EOF +usage: $0 options + +OPTIONS: + -c CMD Command name (CMD = assembly|deploy|next) + -n VER Next version number (for "next" command) + -s ID Add suffix to assembly filename (for "assembly" command) + -t Test assembly with default Maven repo (for "assembly" command) + -T Test assembly with empty Maven repo (for "assembly" command) + -d Dry run (for "deploy" command) + -v Verbose + -h Show this message + +COMMAND NAMES: + "assembly" -> creates a release ZIP (use after "mvn site") + "deploy" -> deploys the release ZIP (use after "assembly") + "next" -> modifies Roo version numbers to that given by -n + +DESCRIPTION: + Automates building deployment ZIPs and allow later deployment. + Note "assembly" and "deploy" always test ZIP integrity via GPG and SHA. + The -t and -T options are both slow as they make and run user projects. + The -T option is extremely slow as it forces Maven to download everything. + Use "-c assembly -tv" to build and test the assembly in most cases. + Use "-c assembly -s _12-24" to add "_12-24" to the assembly filename. + Use "-c deploy -vd" to see what will happen, but without uploading. + Use "-c next -n 3.4.5.RC1" to change next version to 3.4.5.RC1. + +REQUIRES: + s3cmd (ls should list the SpringSource buckets) + ~/.m2/settings.xml contains a for the GPG key + mvn and wget (only required if using -t or -T) + sha1sum, zip, unzip and other common *nix commands +EOF +} + +log() { + if [ "$VERBOSE" = "1" ]; then + echo "$@" + fi +} + +l_error() { + echo "### ERROR: $@" +} + +s3_execute() { + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$DRY_RUN" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS --dry-run" + fi + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi +} + +quick_zip_gpg_tests() { + pushd $DIST_DIR &>/dev/null + + # Test the hash worked OK + # (for script testing purposes only:) sed -i 's/0/1/g' $ASSEMBLY_SHA + sha1sum --status --check $ASSEMBLY_SHA + if [[ ! "$?" = "0" ]]; then + l_error "sha1sum verification of $ASSEMBLY_SHA failed" >&2; exit 1; + fi + log "sha1sum test pass: $ASSEMBLY_SHA" + + # Test the signature is OK + # (for script testing purposes only:) sed -i 's/0/1/g' $ASSEMBLY_ASC + if [ "$VERBOSE" = "1" ]; then + gpg -v --batch --verify $ASSEMBLY_ASC + EXITED=$? + else + gpg --batch --verify $ASSEMBLY_ASC &>/dev/null + EXITED=$? + fi + if [[ ! "$EXITED" = "0" ]]; then + l_error "GPG detached signature verification failed (gpg exit code $EXITED)." >&2; exit 1; + fi + log "gpg signature verification test pass: $ASSEMBLY_ASC" + + popd &>/dev/null +} + +load_roo_build_and_test() { + type -P mvn &>/dev/null || { l_error "mvn not found. Aborting." >&2; exit 1; } + log "Beginning test script: $@" + rm -rf /tmp/rootest + mkdir -p /tmp/rootest + pushd /tmp/rootest &>/dev/null + if [ "$VERBOSE" = "1" ]; then + $ROO_CMD $@ + EXITED=$? + else + $ROO_CMD $@ &>/dev/null + EXITED=$? + fi + if [[ ! "$EXITED" = "0" ]]; then + l_error "Test failed: $ROO_CMD $@" >&2; exit 1; + fi + if [ -f /tmp/rootest/src/main/resources/log4j.properties ]; then + sed -i 's/org.apache.log4j.ConsoleAppender/org.apache.log4j.varia.NullAppender/g' /tmp/rootest/src/main/resources/log4j.properties + fi + $MVN_CMD -e -B clean test + if [[ ! "$?" = "0" ]]; then + l_error "Test failed: $MVN_CMD -e -B clean test" >&2; exit 1; + fi + popd &>/dev/null +} + +tomcat_stop_start_get_stop() { + type -P wget &>/dev/null || { l_error "wget not found. Aborting." >&2; exit 1; } + log "Performing MVC testing; expecting GET success for URL: $@" + pushd /tmp/rootest &>/dev/null + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + # doing a kill -9 as it was hanging around for some reason, when it really should have been killed by now + log "kill -9 of old mvn tomcat7:run with PID $MVN_TOMCAT_PID" + kill -9 $MVN_TOMCAT_PID + sleep 5 + fi + log "Invoking mvn tomcat7:run in background" + $MVN_CMD -e -B -Dmaven.tomcat.port=8888 tomcat7:run &>/dev/null 2>&1 & + WGET_OPTS="-q" + if [ "$VERBOSE" = "1" ]; then + WGET_OPTS="-v" + fi + wget $WGET_OPTS --retry-connrefused --tries=30 -O /tmp/rootest/wget.html $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "wget failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + log "Terminating background mvn tomcat7:run process with PID $MVN_TOMCAT_PID" + kill $MVN_TOMCAT_PID + # no need to sleep, as we'll be at least running Roo between now and the next Tomcat start + fi + popd &>/dev/null +} + +jetty_stop_start_get_stop() { + type -P wget &>/dev/null || { l_error "wget not found. Aborting." >&2; exit 1; } + log "Performing JSF testing; expecting GET success for URL: $@" + pushd /tmp/rootest &>/dev/null + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_JETTY_PID=`ps -e | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + else + MVN_JETTY_PID=`ps -eo "%p %c %a" | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_JETTY_PID" = "" ]; then + # doing a kill -9 as it was hanging around for some reason, when it really should have been killed by now + log "kill -9 of old mvn jetty:run-exploded with PID $MVN_JETTY_PID" + kill -9 $MVN_JETTY_PID + sleep 5 + fi + log "Invoking mvn jetty:run-exploded in background" + $MVN_CMD -e -B -Djetty.port=8888 jetty:run-exploded &>/dev/null 2>&1 & + WGET_OPTS="-q" + if [ "$VERBOSE" = "1" ]; then + WGET_OPTS="-v" + fi + wget $WGET_OPTS --retry-connrefused --tries=30 -O /tmp/rootest/wget.html $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "wget failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_JETTY_PID=`ps -e | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + else + MVN_JETTY_PID=`ps -eo "%p %c %a" | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_JETTY_PID" = "" ]; then + log "Terminating background mvn grep jetty:run-exploded process with PID $MVN_JETTY_PID" + kill $MVN_JETTY_PID + # no need to sleep, as we'll be at least running Roo between now and the next Jetty start + fi + popd &>/dev/null +} + +pizzashop_tests() { + type -P curl &>/dev/null || { l_error "curl not found. Aborting." >&2; exit 1; } + log "Performing MVC REST testing;" + pushd /tmp/rootest &>/dev/null + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + # doing a kill -9 as it was hanging around for some reason, when it really should have been killed by now + log "kill -9 of old mvn tomcat7:run with PID $MVN_TOMCAT_PID" + kill -9 $MVN_TOMCAT_PID + sleep 5 + fi + log "Invoking mvn tomcat7:run in background" + $MVN_CMD -e -B -Dmaven.tomcat.port=8888 tomcat7:run &>/dev/null 2>&1 & + + wget --retry-connrefused --tries=30 --header 'Accept: application/json' --quiet http://localhost:8888/pizzashop/bases 2>&1 + + log "Testing RESTful POST to PizzaShop application" + curl -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -i -s -X POST -d "{name: \"Thin Crust\"}" http://localhost:8888/pizzashop/bases + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful POST to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful array data POST to PizzaShop application" + curl -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -i -s -X POST -d "[{name: \"Cheesy Crust\"},{name: \"Thick Crust\"}]" http://localhost:8888/pizzashop/bases/jsonArray + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful array data POST to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful array data POST to PizzaShop application" + curl -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -i -s -X POST -d "[{name: \"Fresh Tomato\"},{name: \"Prawns\"},{name: \"Mozarella\"},{name: \"Bogus\"}]" http://localhost:8888/pizzashop/toppings/jsonArray + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful array data POST to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful PUT to PizzaShop application" + curl -i -s -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -d "{id:6,name:\"Mozzarella\",version:1}" http://localhost:8888/pizzashop/toppings/6 + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "200 OK" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful PUT to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful GET to PizzaShop application" + curl -i -s -H "Accept: application/json" -o /tmp/rootest/curl.txt http://localhost:8888/pizzashop/toppings + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head /tmp/rootest/curl.txt | grep "Tomato" | grep "Prawns" | grep "Mozzarella" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful GET to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful GET to PizzaShop application" + curl -i -s -H "Accept: application/json" -o /tmp/rootest/curl.txt http://localhost:8888/pizzashop/toppings/6 + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head /tmp/rootest/curl.txt | grep "Mozzarella" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful GET to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful complex POST to PizzaShop application" + curl -i -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -d "{name:\"Napolitana\",price:7.5,base:{id:1},toppings:[{name: \"Anchovy fillets\"},{name: \"Mozzarella\"}]}" http://localhost:8888/pizzashop/pizzas + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful complex POST to PizzaShop application failed" >&2; exit 1; + fi + + #log "Testing RESTful complex POST to PizzaShop application" + #curl -i -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -d "{name:\"Stefan\",total:7.5,address:\"Sydney, AU\",deliveryDate:1314595427866,id:{shopCountry:\"AU\",shopCity:\"Sydney\",shopName:\"Pizza Pan 1\"},pizzas:[{id:8,version:1}]}" http://localhost:8888/pizzashop/pizzaorders + #EXITED=$? + #if [[ ! "$EXITED" = "0" ]]; then + # l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + #fi + #head /tmp/rootest/curl.txt | grep "201 Created" + #EXITED=$? + #if [[ ! "$EXITED" = "0" ]]; then + # l_error "RESTful complex POST to PizzaShop application failed" >&2; exit 1; + #fi + + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + log "Terminating background mvn tomcat7:run process with PID $MVN_TOMCAT_PID" + kill $MVN_TOMCAT_PID + # no need to sleep, as we'll be at least running Roo between now and the next Tomcat start + fi + popd &>/dev/null +} + +COMMAND= +NEXT= +VERBOSE='0' +DRY_RUN='0' +TEST='0' +SUFFIX='' +while getopts "s:c:n:tTvdh" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + c) + COMMAND=$OPTARG + ;; + n) + NEXT=$OPTARG + ;; + s) + SUFFIX=$OPTARG + ;; + d) + DRY_RUN=1 + ;; + t) + TEST=1 + ;; + T) + TEST=2 + ;; + v) + VERBOSE=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [[ -z $COMMAND ]]; then + usage + exit 1 +fi + +if [[ "$COMMAND" = "assembly" ]] || [[ "$COMMAND" = "deploy" ]] || [[ "$COMMAND" = "next" ]]; then + log "Command........: $COMMAND" +else + usage + exit 1 +fi + +type -P zip &>/dev/null || { l_error "zip not found. Aborting." >&2; exit 1; } +type -P unzip &>/dev/null || { l_error "unzip not found. Aborting." >&2; exit 1; } +type -P sha1sum &>/dev/null || { l_error "sha1sum not found. Aborting." >&2; exit 1; } + +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done +ROO_HOME=`dirname "$PRG"` + +# Absolute path +ROO_HOME=`cd "$ROO_HOME/.." ; pwd` +log "Location.......: $ROO_HOME" + +# Compute version and Git-related info +GIT_HASH=`git log "--pretty=format:%H" -n1 $ROO_HOME` +log "Git Hash.......: $GIT_HASH" +GIT_TAG=`git tag -l "*\.*\.*\.*" $ROO_HOME | tail -n 1` +log "Git Tag........: $GIT_TAG" +VERSION=`grep "" $ROO_HOME/pom.xml | head -n 1 | sed 's///' | sed 's/<\/version>//' | sed 's/ //g'` +log "Version........: $VERSION" +SHORT_VERSION=`echo $VERSION | sed 's/\([0-9].[0-9].[0-9]\).*\.BUILD-SNAPSHOT/\1/'` +log "Short Version..: $SHORT_VERSION" + +# Determine the version as required by the AWS spring-roo-repository.springsource.org "x-amz-meta-release.type" header +case $VERSION in + *BUILD-SNAPSHOT) TYPE=snapshot;; + *RC*) TYPE=milestone;; + *M*) TYPE=milestone;; + *RELEASE) TYPE=release;; + *) l_error "Unsupported release type ($VERSION). Aborting." >&2; exit 1;; +esac +log "Release Type...: $TYPE" + +# Product release identifier +RELEASE_IDENTIFIER="spring-roo-$VERSION" +log "Release ID.....: $RELEASE_IDENTIFIER" + +# File locations +TARGET_DIR="$ROO_HOME/target/roo-deploy" +log "Target Dir.....: $TARGET_DIR" +WORK_DIR="$ROO_HOME/target/roo-deploy/work/$RELEASE_IDENTIFIER" +log "Work Dir.......: $WORK_DIR" +DIST_DIR="$ROO_HOME/target/roo-deploy/dist" +log "Output Dir.....: $DIST_DIR" +ASSEMBLY_ZIP="$DIST_DIR/$RELEASE_IDENTIFIER$SUFFIX.zip" +log "Assembly Zip...: $ASSEMBLY_ZIP" +ASSEMBLY_SHA="$DIST_DIR/$RELEASE_IDENTIFIER$SUFFIX.zip.sha1" +log "Assembly SHA...: $ASSEMBLY_SHA" +ASSEMBLY_ASC="$DIST_DIR/$RELEASE_IDENTIFIER$SUFFIX.zip.asc" +log "Assembly ASC...: $ASSEMBLY_ASC" + +if [[ "$COMMAND" = "assembly" ]]; then + + if [ ! -f $ROO_HOME/target/all/org.springframework.roo.bootstrap-*.jar ]; then + l_error "JARs missing; you must run mvn package before attempting assembly" + exit 1 + fi + if [ ! -f $ROO_HOME/deployment-support/target/site/reference/pdf/spring-roo-docs.pdf ]; then + l_error "Site docs missing; you must run mvn site before attempting assembly" + exit 1 + fi + log "Cleaning $TARGET_DIR" + rm -rf $TARGET_DIR + log "Cleaning $WORK_DIR" + rm -rf $WORK_DIR + + # Create a directory structure to match the desired assembly ZIP output + mkdir -p $WORK_DIR/annotations + mkdir -p $WORK_DIR/bin + mkdir -p $WORK_DIR/bundle + mkdir -p $WORK_DIR/conf + mkdir -p $WORK_DIR/docs/pdf + mkdir -p $WORK_DIR/docs/html + mkdir -p $WORK_DIR/legal + mkdir -p $WORK_DIR/samples + cp $ROO_HOME/annotations/target/*-$VERSION.jar $WORK_DIR/annotations + cp $ROO_HOME/target/all/*.jar $WORK_DIR/bundle + rm $WORK_DIR/bundle/org.springframework.roo.annotations-$VERSION.jar + rm $WORK_DIR/bundle/*junit*.jar + rm $WORK_DIR/bundle/*jsch*.jar + rm $WORK_DIR/bundle/*jgit*.jar + rm $WORK_DIR/bundle/*git*.jar + rm $WORK_DIR/bundle/*op4j*.jar + rm $WORK_DIR/bundle/*aopalliance-*.jar + rm $WORK_DIR/bundle/jackson-*.jar + rm $WORK_DIR/bundle/jcl-over-slf4j-*.jar + rm $WORK_DIR/bundle/servlet-api-*.jar + rm $WORK_DIR/bundle/slf4j-*.jar + rm $WORK_DIR/bundle/spring-*.jar + # These have to be removed as the Cloud Foundry add-on requires dependencies that are not bundled and thus must be installed via the shell. + rm $WORK_DIR/bundle/*cloud.foundry*.jar + rm $WORK_DIR/bundle/*cloud-foundry-api*.jar + rm $WORK_DIR/bundle/*AppCloudClient*.jar + mv $WORK_DIR/bundle/org.springframework.roo.bootstrap-*.jar $WORK_DIR/bin + mv $WORK_DIR/bundle/org.apache.felix.framework-*.jar $WORK_DIR/bin + cp $ROO_HOME/bootstrap/src/main/bin/* $WORK_DIR/bin + chmod 775 $WORK_DIR/bin/*.sh + cp $ROO_HOME/bootstrap/src/main/conf/* $WORK_DIR/conf + cp $ROO_HOME/bootstrap/readme.txt $WORK_DIR/ + cp `find $ROO_HOME -iname legal-\*.txt` $WORK_DIR/legal + cp `find $ROO_HOME -iname \*.roo | grep -v "/target/"` $WORK_DIR/samples + cp -r $ROO_HOME/deployment-support/target/site/reference/pdf/ $WORK_DIR/docs + cp -r $ROO_HOME/deployment-support/target/site/reference/html/ $WORK_DIR/docs + + # Prepare to write the ZIP + log "Cleaning $DIST_DIR" + rm -rf $DIST_DIR + mkdir -p $DIST_DIR + + # Change directories to avoid absolute paths + pushd $WORK_DIR/.. &>/dev/null + log "Running ZIP from `pwd`" + ZIP_OPTS='-q' + if [ "$VERBOSE" = "1" ]; then + ZIP_OPTS='-v' + fi + log "ZIP command: zip $ZIP_OPTS $ASSEMBLY_ZIP -r $RELEASE_IDENTIFIER" + zip $ZIP_OPTS $ASSEMBLY_ZIP -r $RELEASE_IDENTIFIER + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "ZIP process failed (zip exit code $EXITED)." >&2; exit 1; + fi + + # Hash the ZIP + pushd $DIST_DIR &>/dev/null + sha1sum *.zip > $ASSEMBLY_SHA + + # Return to the original directory + popd &>/dev/null + popd &>/dev/null + + if [ ! "$TEST" = "0" ]; then + log "Unzipping Roo distribution to test area" + rm -rf /tmp/$RELEASE_IDENTIFIER + ZIP_OPTS='-qq' + if [ "$VERBOSE" = "1" ]; then + ZIP_OPTS='' + fi + unzip $ZIP_OPTS $ASSEMBLY_ZIP -d /tmp/ + ROO_CMD="/tmp/$RELEASE_IDENTIFIER/bin/roo.sh" + log "Roo command....: $ROO_CMD" + + # Setup Maven and the active repository (we must ensure the annotation JAR matches that in the assembly; it may not have yet been deployed publicly as it's just being tested at present) + MVN_CMD='mvn' + if [[ "$TEST" = "2" ]]; then + MVN_CMD="mvn -gs /tmp/settings.xml" + echo "/tmp/repository" > /tmp/settings.xml + rm -rf /tmp/repository + mkdir -p /tmp/repository/org/springframework/roo/org.springframework.roo.annotations/$VERSION/ + cp /tmp/$RELEASE_IDENTIFIER/annotations/* /tmp/repository/org/springframework/roo/org.springframework.roo.annotations/$VERSION/ + else + cp /tmp/$RELEASE_IDENTIFIER/annotations/* ~/.m2/repository/org/springframework/roo/org.springframework.roo.annotations/$VERSION/ + fi + if [ "$VERBOSE" = "0" ]; then + MVN_CMD="$MVN_CMD -q" + fi + + load_roo_build_and_test script vote.roo + tomcat_stop_start_get_stop http://localhost:8888/vote + + load_roo_build_and_test script clinic.roo + tomcat_stop_start_get_stop http://localhost:8888/petclinic + + load_roo_build_and_test script wedding.roo + tomcat_stop_start_get_stop http://localhost:8888/wedding + + load_roo_build_and_test script pizzashop.roo + tomcat_stop_start_get_stop http://localhost:8888/pizzashop + pizzashop_tests + + load_roo_build_and_test script bikeshop.roo + jetty_stop_start_get_stop http://localhost:8888/bikeshop/pages/main.jsf + + load_roo_build_and_test script multimodule.roo + tomcat_stop_start_get_stop http://localhost:8888/mvc + + load_roo_build_and_test script embedding.roo + tomcat_stop_start_get_stop http://localhost:8888/embedding + + log "Removing Roo distribution from test area" + rm -rf /tmp/$RELEASE_IDENTIFIER + log "Completed tests successfully" + fi +fi + +if [[ "$COMMAND" = "deploy" ]]; then + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + + if [ ! -f $ASSEMBLY_ZIP ]; then + l_error "Unable to find $ASSEMBLY_ZIP" + exit 1 + fi + if [ ! -f $ASSEMBLY_SHA ]; then + l_error "Unable to find $ASSEMBLY_SHA" + exit 1 + fi + + quick_zip_gpg_tests + + ZIP_FILENAME=`basename $ASSEMBLY_ZIP` + PROJECT_NAME="Spring Roo" + AWS_PATH="s3://spring-roo-repository.springsource.org/$TYPE/ROO/" + log "AWS bundle.ver.: $VERSION" + log "AWS rel.type...: $TYPE" + log "AWS pkg.f.name.: $ZIP_FILENAME" + log "AWS proj.name..: $PROJECT_NAME" + log "AWS Path.......: $AWS_PATH" + + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$DRY_RUN" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS --dry-run" + fi + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS put --acl-public \ + "--add-header=x-amz-meta-bundle.version:$VERSION" \ + "--add-header=x-amz-meta-release.type:$TYPE" \ + "--add-header=x-amz-meta-package.file.name:$ZIP_FILENAME" \ + "--add-header=x-amz-meta-project.name:$PROJECT_NAME" \ + $ASSEMBLY_ZIP $AWS_PATH + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi + + s3_execute put --acl-public $ASSEMBLY_SHA $AWS_PATH + s3_execute put --acl-public $ASSEMBLY_ASC $AWS_PATH + + # Clean up old snapshot releases (if we just performed a snapshot release) + if [[ "$TYPE" = "snapshot" ]]; then + s3_execute ls s3://spring-roo-repository.springsource.org/snapshot/ROO/ | grep '.zip$' | cut -c "30-"> /tmp/dist_all.txt + tail -n 5 /tmp/dist_all.txt > /tmp/dist_to_keep.txt + cat /tmp/dist_all.txt /tmp/dist_to_keep.txt | sort | uniq -u > /tmp/dist_to_delete.txt + for url in `cat /tmp/dist_to_delete.txt`; do + s3_execute del "$url" + s3_execute del "$url.asc" + s3_execute del "$url.sha1" + done + rm /tmp/dist_*.txt + fi +fi + +if [[ "$COMMAND" = "next" ]]; then + # We only need to change the _first_ element in each pom.xml to complete a version update + find $ROO_HOME -iname pom.xml -print0 | while read -d $'\0' file + do + log "Updating $file" + sed -i "0,/.*<\/version>/s//$NEXT<\/version>/" $file + done + log "Updating project templates" + sed -i "s/.*<\/roo.version>/$NEXT<\/roo.version>/" `find $ROO_HOME -iname *-template.xml` + log "Updating documentation" + sed -i "s/.*<\/releaseinfo>/$NEXT<\/releaseinfo>/" $ROO_HOME/deployment-support/src/site/docbook/reference/index.xml +fi + diff --git a/deployment-support/roo-ci.sh b/deployment-support/roo-ci.sh new file mode 100755 index 000000000..4b05c319c --- /dev/null +++ b/deployment-support/roo-ci.sh @@ -0,0 +1,236 @@ +#/bin/shell + +usage() { +cat << EOF +usage: $0 options + +OPTIONS: + -d Dry run (ie do not deploy or prune older releases) + -t Test assembly with default Maven repo (slow) + -T Test assembly with empty Maven repo (very slow, but thorough) + -v Verbose + -f Force execution, ignoring normal version checks + -h Show this message + +DESCRIPTION: + Drives the overall Hudson continuous integration process. This script + is designed so it can be manually executed by a committer if desired + for testing purposes. The script produces assembly filenames that + reflect local system time and a Git hash for easy identification. The + overall process is as follow, with any failure causing an early exit: + * Aborts if version != *.BUILD-SNAPSHOT (unless -f was indicated) + * Performs a "mvn clean package" from the project root + * Performs a "mvn clean site" from deployment-support + * Performs "roo-deploy-dist.sh assembly" (with -T/t if requested) + * Performs "mvn deploy" from the project root + * Performs "roo-deploy-dist.sh deploy" to release the assembly + * Removes older snapshot releases from S3 + +RETURNS: + 0 if successful or the version was != *.BUILD-SNAPSHOT + 1 if there was a failure of any kind + +REQUIRES: + See requirements in readme.txt for releasing (ie SSH, GPG etc) +EOF +} + +log() { + if [ "$VERBOSE" = "1" ]; then + echo "$@" + fi +} + +l_error() { + echo "### ERROR: $@" +} + +s3_execute() { + if [ "$DRY_RUN" = "0" ]; then + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi + fi +} + +VERBOSE='0' +DRY_RUN='0' +FORCE='0' +TEST='' +while getopts "vdhftT" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + d) + DRY_RUN=1 + ;; + v) + VERBOSE=1 + ;; + t) + TEST="-t" + ;; + T) + TEST="-T" + ;; + f) + FORCE=1 + ;; + ?) + usage + exit + ;; + esac +done + +type -P mvn &>/dev/null || { l_error "mvn not found. Aborting." >&2; exit 1; } +type -P gpg &>/dev/null || { l_error "gpg not found. Aborting." >&2; exit 1; } +type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done +ROO_HOME=`dirname "$PRG"` + +# Absolute path +ROO_HOME=`cd "$ROO_HOME/.." ; pwd` +log "Location.......: $ROO_HOME" + +# Compute version and Git-related info +GIT_HASH=`git log "--pretty=format:%H" -n1 $ROO_HOME` +log "Git Hash.......: $GIT_HASH" +GIT_TAG=`git tag -l "*\.*\.*\.*" $ROO_HOME | tail -n 1` +log "Git Tag........: $GIT_TAG" +VERSION=`grep "" $ROO_HOME/pom.xml | head -n 1 | sed 's///' | sed 's/<\/version>//' | sed 's/ //g'` +log "Version........: $VERSION" +SHORT_VERSION=`echo $VERSION | sed 's/\([0-9].[0-9].[0-9]\).*\.BUILD-SNAPSHOT/\1/'` +log "Short Version..: $SHORT_VERSION" +TIMESTAMP=$(date --utc +"%Y%m%d.%H%M%S") +log "Timestamp......: $TIMESTAMP" + +# Determine the assembly filename (we don't rely on Hudson variables, as we'd like this to work for normal developers as well) +SUFFIX="_$TIMESTAMP-${GIT_HASH:0:7}" +log "Suffix.........: $SUFFIX" + +# Determine the version as required by the AWS dist.springframework.org "x-amz-meta-release.type" header +case $VERSION in + *BUILD-SNAPSHOT) TYPE=snapshot;; + *RC*) TYPE=milestone;; + *M*) TYPE=milestone;; + *RELEASE) TYPE=release;; + *) l_error "Unsupported release type ($VERSION). Aborting." >&2; exit 1;; +esac +log "Release Type...: $TYPE" + +# Gracefully abort if this isn't a BUILD-SNAPSHOT (unless the user has forced us to continue via -f) +if [[ "$FORCE" = "0" ]]; then + if [[ ! "$TYPE" = "snapshot" ]]; then + l_error "Gracefully aborting as not a snapshot ($VERSION)." >&2; exit 0; # code 0 is correct, this is not a serious error + fi +fi + +# Setup correct options for a dry run vs normal run +if [[ "$DRY_RUN" = "0" ]]; then + MAVEN_MAIN_OPTS='-e -B clean install' + MAVEN_SITE_OPTS='-e -B clean site' + MAVEN_DEPLOY_OPTS='-e -B deploy' + ROO_DEPLOY_OPTS='' +else + MAVEN_MAIN_OPTS='-e -B clean install' + MAVEN_SITE_OPTS='-e -B clean site' + MAVEN_DEPLOY_OPTS='never_invoked' + ROO_DEPLOY_OPTS='-d' +fi + +# Setup correct options for -v (verbose) (we default maven to quiet mode unless -v has been specified, as it's just too noisy) +if [[ "$VERBOSE" = "0" ]]; then + MAVEN_MAIN_OPTS="$MAVEN_MAIN_OPTS -q" + MAVEN_SITE_OPTS="$MAVEN_SITE_OPTS -q" + MAVEN_DEPLOY_OPTS="$MAVEN_DEPLOY_OPTS -q" +else + ROO_DEPLOY_OPTS="$ROO_DEPLOY_OPTS -v" +fi + +pushd $ROO_HOME/ &>/dev/null +# Do the initial mvn packaging (but don't dpeloy) +mvn $MAVEN_MAIN_OPTS +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "Maven main build failed (exit code $EXITED)." >&2; exit 1; +fi + +# Build reference guide docs (and deploy them; it's not a big deal if the later tests fail but the docs were updated) +pushd $ROO_HOME/deployment-support &>/dev/null +mvn $MAVEN_SITE_OPTS +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "Maven site build failed (exit code $EXITED)." >&2; exit 1; +fi + +# Build (and test if user used -T or -t) the assembly +./roo-deploy-dist.sh -c assembly -s $SUFFIX $ROO_DEPLOY_OPTS $TEST +EXITED=$? +if [[ ! "$EXITED" = "0" ]]; then + l_error "roo-deploy -c assembly failed (exit code $EXITED)." >&2; exit 1; +fi + +# Deploy the Maven JARs (we do this first to avoid people getting the latest snapshot assembly ZIP before the latest annotation JAR is visible) +if [[ "$DRY_RUN" = "0" ]]; then + pushd $ROO_HOME/ &>/dev/null + mvn $MAVEN_DEPLOY_OPTS + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "Maven deploy failed (exit code $EXITED)." >&2; exit 1; + fi + popd &>/dev/null +fi + +# Deploy the assembly so people can download it (note roo-deploy-dist.sh will prune old snapshots from the download site automatically) +if [[ "$DRY_RUN" = "0" ]]; then + ./roo-deploy-dist.sh -c deploy -s $SUFFIX $ROO_DEPLOY_OPTS + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "roo-deploy -c deploy failed (exit code $EXITED)." >&2; exit 1; + fi +fi + +# Prune some old releases. We can rely on the fact CI runs at least every 24 hours and thus we can prune anything older than say 3 days +if [[ "$DRY_RUN" = "0" ]]; then + OK_DATE_0=`date +%Y-%m-%d` + OK_DATE_1=`date --date '-1 day' +%Y-%m-%d` + OK_DATE_2=`date --date '-2 day' +%Y-%m-%d` + log "Obtaining listing of all snapshot resources on S3" + s3_execute ls -r s3://spring-roo-repository.springsource.org/snapshot > /tmp/dist_snapshots.txt + log "Retain Dates...: $OK_DATE_0 $OK_DATE_1 $OK_DATE_2" + log "S3 Found.......: `grep -v "/$" /tmp/dist_snapshots.txt | wc -l`" + grep -v -e $OK_DATE_0 -e $OK_DATE_1 -e $OK_DATE_2 /tmp/dist_snapshots.txt > /tmp/dist_delete.txt + log "S3 To Delete...: `grep -v "/$" /tmp/dist_delete.txt | wc -l`" + cat /tmp/dist_delete.txt | cut -c "30-" > /tmp/dist_delete_cut.txt + for filename in `grep -v "/$" /tmp/dist_delete_cut.txt`; do + s3_execute del "$filename" + done + log "Pruning old snapshots completed successfully" +fi + +# Return to the original directory +popd &>/dev/null +popd &>/dev/null + diff --git a/deployment-support/roo-deploy-dist.sh b/deployment-support/roo-deploy-dist.sh new file mode 100755 index 000000000..c98935e6a --- /dev/null +++ b/deployment-support/roo-deploy-dist.sh @@ -0,0 +1,667 @@ +#/bin/shell + +usage() { +cat << EOF +usage: $0 options + +OPTIONS: + -c CMD Command name (CMD = assembly|deploy|next) + -n VER Next version number (for "next" command) + -s ID Add suffix to assembly filename (for "assembly" command) + -t Test assembly with default Maven repo (for "assembly" command) + -T Test assembly with empty Maven repo (for "assembly" command) + -d Dry run (for "deploy" command) + -v Verbose + -h Show this message + +COMMAND NAMES: + "assembly" -> creates a release ZIP (use after "mvn site") + "deploy" -> deploys the release ZIP (use after "assembly") + "next" -> modifies Roo version numbers to that given by -n + +DESCRIPTION: + Automates building deployment ZIPs and allow later deployment. + Note "assembly" and "deploy" always test ZIP integrity via GPG and SHA. + The -t and -T options are both slow as they make and run user projects. + The -T option is extremely slow as it forces Maven to download everything. + Use "-c assembly -tv" to build and test the assembly in most cases. + Use "-c assembly -s _12-24" to add "_12-24" to the assembly filename. + Use "-c deploy -vd" to see what will happen, but without uploading. + Use "-c next -n 3.4.5.RC1" to change next version to 3.4.5.RC1. + +REQUIRES: + s3cmd (ls should list the SpringSource buckets) + ~/.m2/settings.xml contains a for the GPG key + mvn and wget (only required if using -t or -T) + sha1sum, zip, unzip and other common *nix commands +EOF +} + +log() { + if [ "$VERBOSE" = "1" ]; then + echo "$@" + fi +} + +l_error() { + echo "### ERROR: $@" +} + +s3_execute() { + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$DRY_RUN" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS --dry-run" + fi + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi +} + +quick_zip_gpg_tests() { + pushd $DIST_DIR &>/dev/null + + # Test the hash worked OK + # (for script testing purposes only:) sed -i 's/0/1/g' $ASSEMBLY_SHA + sha1sum --status --check $ASSEMBLY_SHA + if [[ ! "$?" = "0" ]]; then + l_error "sha1sum verification of $ASSEMBLY_SHA failed" >&2; exit 1; + fi + log "sha1sum test pass: $ASSEMBLY_SHA" + + # Test the signature is OK + # (for script testing purposes only:) sed -i 's/0/1/g' $ASSEMBLY_ASC + if [ "$VERBOSE" = "1" ]; then + gpg -v --batch --verify $ASSEMBLY_ASC + EXITED=$? + else + gpg --batch --verify $ASSEMBLY_ASC &>/dev/null + EXITED=$? + fi + if [[ ! "$EXITED" = "0" ]]; then + l_error "GPG detached signature verification failed (gpg exit code $EXITED)." >&2; exit 1; + fi + log "gpg signature verification test pass: $ASSEMBLY_ASC" + + popd &>/dev/null +} + +load_roo_build_and_test() { + type -P mvn &>/dev/null || { l_error "mvn not found. Aborting." >&2; exit 1; } + log "Beginning test script: $@" + rm -rf /tmp/rootest + mkdir -p /tmp/rootest + pushd /tmp/rootest &>/dev/null + if [ "$VERBOSE" = "1" ]; then + $ROO_CMD $@ + EXITED=$? + else + $ROO_CMD $@ &>/dev/null + EXITED=$? + fi + if [[ ! "$EXITED" = "0" ]]; then + l_error "Test failed: $ROO_CMD $@" >&2; exit 1; + fi + if [ -f /tmp/rootest/src/main/resources/log4j.properties ]; then + sed -i 's/org.apache.log4j.ConsoleAppender/org.apache.log4j.varia.NullAppender/g' /tmp/rootest/src/main/resources/log4j.properties + fi + $MVN_CMD -e -B clean test + if [[ ! "$?" = "0" ]]; then + l_error "Test failed: $MVN_CMD -e -B clean test" >&2; exit 1; + fi + popd &>/dev/null +} + +tomcat_stop_start_get_stop() { + type -P wget &>/dev/null || { l_error "wget not found. Aborting." >&2; exit 1; } + log "Performing MVC testing; expecting GET success for URL: $@" + pushd /tmp/rootest &>/dev/null + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + # doing a kill -9 as it was hanging around for some reason, when it really should have been killed by now + log "kill -9 of old mvn tomcat7:run with PID $MVN_TOMCAT_PID" + kill -9 $MVN_TOMCAT_PID + sleep 5 + fi + log "Invoking mvn tomcat7:run in background" + $MVN_CMD -e -B -Dmaven.tomcat.port=8888 tomcat7:run &>/dev/null 2>&1 & + WGET_OPTS="-q" + if [ "$VERBOSE" = "1" ]; then + WGET_OPTS="-v" + fi + wget $WGET_OPTS --retry-connrefused --tries=30 -O /tmp/rootest/wget.html $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "wget failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + log "Terminating background mvn tomcat7:run process with PID $MVN_TOMCAT_PID" + kill $MVN_TOMCAT_PID + # no need to sleep, as we'll be at least running Roo between now and the next Tomcat start + fi + popd &>/dev/null +} + +jetty_stop_start_get_stop() { + type -P wget &>/dev/null || { l_error "wget not found. Aborting." >&2; exit 1; } + log "Performing JSF testing; expecting GET success for URL: $@" + pushd /tmp/rootest &>/dev/null + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_JETTY_PID=`ps -e | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + else + MVN_JETTY_PID=`ps -eo "%p %c %a" | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_JETTY_PID" = "" ]; then + # doing a kill -9 as it was hanging around for some reason, when it really should have been killed by now + log "kill -9 of old mvn jetty:run-exploded with PID $MVN_JETTY_PID" + kill -9 $MVN_JETTY_PID + sleep 5 + fi + log "Invoking mvn jetty:run-exploded in background" + $MVN_CMD -e -B -Djetty.port=8888 jetty:run-exploded &>/dev/null 2>&1 & + WGET_OPTS="-q" + if [ "$VERBOSE" = "1" ]; then + WGET_OPTS="-v" + fi + wget $WGET_OPTS --retry-connrefused --tries=30 -O /tmp/rootest/wget.html $@ + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "wget failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_JETTY_PID=`ps -e | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + else + MVN_JETTY_PID=`ps -eo "%p %c %a" | grep Launcher | grep jetty:run-exploded | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_JETTY_PID" = "" ]; then + log "Terminating background mvn grep jetty:run-exploded process with PID $MVN_JETTY_PID" + kill $MVN_JETTY_PID + # no need to sleep, as we'll be at least running Roo between now and the next Jetty start + fi + popd &>/dev/null +} + +pizzashop_tests() { + type -P curl &>/dev/null || { l_error "curl not found. Aborting." >&2; exit 1; } + log "Performing MVC REST testing;" + pushd /tmp/rootest &>/dev/null + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + # doing a kill -9 as it was hanging around for some reason, when it really should have been killed by now + log "kill -9 of old mvn tomcat7:run with PID $MVN_TOMCAT_PID" + kill -9 $MVN_TOMCAT_PID + sleep 5 + fi + log "Invoking mvn tomcat7:run in background" + $MVN_CMD -e -B -Dmaven.tomcat.port=8888 tomcat7:run &>/dev/null 2>&1 & + + wget --retry-connrefused --tries=30 --header 'Accept: application/json' --quiet http://localhost:8888/pizzashop/bases 2>&1 + + log "Testing RESTful POST to PizzaShop application" + curl -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -i -s -X POST -d "{name: \"Thin Crust\"}" http://localhost:8888/pizzashop/bases + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful POST to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful array data POST to PizzaShop application" + curl -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -i -s -X POST -d "[{name: \"Cheesy Crust\"},{name: \"Thick Crust\"}]" http://localhost:8888/pizzashop/bases/jsonArray + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful array data POST to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful array data POST to PizzaShop application" + curl -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -i -s -X POST -d "[{name: \"Fresh Tomato\"},{name: \"Prawns\"},{name: \"Mozarella\"},{name: \"Bogus\"}]" http://localhost:8888/pizzashop/toppings/jsonArray + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful array data POST to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful PUT to PizzaShop application" + curl -i -s -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -d "{id:6,name:\"Mozzarella\",version:1}" http://localhost:8888/pizzashop/toppings/6 + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head -n 1 /tmp/rootest/curl.txt | grep "200 OK" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful PUT to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful GET to PizzaShop application" + curl -i -s -H "Accept: application/json" -o /tmp/rootest/curl.txt http://localhost:8888/pizzashop/toppings + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head /tmp/rootest/curl.txt | grep "Tomato" | grep "Prawns" | grep "Mozzarella" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful GET to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful GET to PizzaShop application" + curl -i -s -H "Accept: application/json" -o /tmp/rootest/curl.txt http://localhost:8888/pizzashop/toppings/6 + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head /tmp/rootest/curl.txt | grep "Mozzarella" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful GET to PizzaShop application failed" >&2; exit 1; + fi + + log "Testing RESTful complex POST to PizzaShop application" + curl -i -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -d "{name:\"Napolitana\",price:7.5,base:{id:1},toppings:[{name: \"Anchovy fillets\"},{name: \"Mozzarella\"}]}" http://localhost:8888/pizzashop/pizzas + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + fi + head /tmp/rootest/curl.txt | grep "201 Created" + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "RESTful complex POST to PizzaShop application failed" >&2; exit 1; + fi + + #log "Testing RESTful complex POST to PizzaShop application" + #curl -i -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -o /tmp/rootest/curl.txt -d "{name:\"Stefan\",total:7.5,address:\"Sydney, AU\",deliveryDate:1314595427866,id:{shopCountry:\"AU\",shopCity:\"Sydney\",shopName:\"Pizza Pan 1\"},pizzas:[{id:8,version:1}]}" http://localhost:8888/pizzashop/pizzaorders + #EXITED=$? + #if [[ ! "$EXITED" = "0" ]]; then + # l_error "curl failed: $@ (returned code $EXITED)" >&2; exit 1; + #fi + #head /tmp/rootest/curl.txt | grep "201 Created" + #EXITED=$? + #if [[ ! "$EXITED" = "0" ]]; then + # l_error "RESTful complex POST to PizzaShop application failed" >&2; exit 1; + #fi + + if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then + MVN_TOMCAT_PID=`ps -e | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + else + MVN_TOMCAT_PID=`ps -eo "%p %c %a" | grep Launcher | grep tomcat7:run | cut -b "1-6" | sed "s/ //g"` + fi + if [ ! "$MVN_TOMCAT_PID" = "" ]; then + log "Terminating background mvn tomcat7:run process with PID $MVN_TOMCAT_PID" + kill $MVN_TOMCAT_PID + # no need to sleep, as we'll be at least running Roo between now and the next Tomcat start + fi + popd &>/dev/null +} + +COMMAND= +NEXT= +VERBOSE='0' +DRY_RUN='0' +TEST='0' +SUFFIX='' +while getopts "s:c:n:tTvdh" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + c) + COMMAND=$OPTARG + ;; + n) + NEXT=$OPTARG + ;; + s) + SUFFIX=$OPTARG + ;; + d) + DRY_RUN=1 + ;; + t) + TEST=1 + ;; + T) + TEST=2 + ;; + v) + VERBOSE=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [[ -z $COMMAND ]]; then + usage + exit 1 +fi + +if [[ "$COMMAND" = "assembly" ]] || [[ "$COMMAND" = "deploy" ]] || [[ "$COMMAND" = "next" ]]; then + log "Command........: $COMMAND" +else + usage + exit 1 +fi + +type -P gpg &>/dev/null || { l_error "gpg not found. Aborting." >&2; exit 1; } +type -P zip &>/dev/null || { l_error "zip not found. Aborting." >&2; exit 1; } +type -P unzip &>/dev/null || { l_error "unzip not found. Aborting." >&2; exit 1; } +type -P sha1sum &>/dev/null || { l_error "sha1sum not found. Aborting." >&2; exit 1; } + +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done +ROO_HOME=`dirname "$PRG"` + +# Absolute path +ROO_HOME=`cd "$ROO_HOME/.." ; pwd` +log "Location.......: $ROO_HOME" + +# Compute version and Git-related info +GIT_HASH=`git log "--pretty=format:%H" -n1 $ROO_HOME` +log "Git Hash.......: $GIT_HASH" +GIT_TAG=`git tag -l "*\.*\.*\.*" $ROO_HOME | tail -n 1` +log "Git Tag........: $GIT_TAG" +VERSION=`grep "" $ROO_HOME/pom.xml | head -n 1 | sed 's///' | sed 's/<\/version>//' | sed 's/ //g'` +log "Version........: $VERSION" +SHORT_VERSION=`echo $VERSION | sed 's/\([0-9].[0-9].[0-9]\).*\.BUILD-SNAPSHOT/\1/'` +log "Short Version..: $SHORT_VERSION" + +# Determine the version as required by the AWS spring-roo-repository.springsource.org "x-amz-meta-release.type" header +case $VERSION in + *BUILD-SNAPSHOT) TYPE=snapshot;; + *RC*) TYPE=milestone;; + *M*) TYPE=milestone;; + *RELEASE) TYPE=release;; + *) l_error "Unsupported release type ($VERSION). Aborting." >&2; exit 1;; +esac +log "Release Type...: $TYPE" + +# Product release identifier +RELEASE_IDENTIFIER="spring-roo-$VERSION" +log "Release ID.....: $RELEASE_IDENTIFIER" + +# File locations +TARGET_DIR="$ROO_HOME/target/roo-deploy" +log "Target Dir.....: $TARGET_DIR" +WORK_DIR="$ROO_HOME/target/roo-deploy/work/$RELEASE_IDENTIFIER" +log "Work Dir.......: $WORK_DIR" +DIST_DIR="$ROO_HOME/target/roo-deploy/dist" +log "Output Dir.....: $DIST_DIR" +ASSEMBLY_ZIP="$DIST_DIR/$RELEASE_IDENTIFIER$SUFFIX.zip" +log "Assembly Zip...: $ASSEMBLY_ZIP" +ASSEMBLY_SHA="$DIST_DIR/$RELEASE_IDENTIFIER$SUFFIX.zip.sha1" +log "Assembly SHA...: $ASSEMBLY_SHA" +ASSEMBLY_ASC="$DIST_DIR/$RELEASE_IDENTIFIER$SUFFIX.zip.asc" +log "Assembly ASC...: $ASSEMBLY_ASC" + +if [[ "$COMMAND" = "assembly" ]]; then + + if [ ! -f $ROO_HOME/target/all/org.springframework.roo.bootstrap-*.jar ]; then + l_error "JARs missing; you must run mvn package before attempting assembly" + exit 1 + fi + if [ ! -f $ROO_HOME/deployment-support/target/site/reference/pdf/spring-roo-docs.pdf ]; then + l_error "Site docs missing; you must run mvn site before attempting assembly" + exit 1 + fi + log "Cleaning $TARGET_DIR" + rm -rf $TARGET_DIR + log "Cleaning $WORK_DIR" + rm -rf $WORK_DIR + + # Create a directory structure to match the desired assembly ZIP output + mkdir -p $WORK_DIR/annotations + mkdir -p $WORK_DIR/bin + mkdir -p $WORK_DIR/bundle + mkdir -p $WORK_DIR/conf + mkdir -p $WORK_DIR/docs/pdf + mkdir -p $WORK_DIR/docs/html + mkdir -p $WORK_DIR/legal + mkdir -p $WORK_DIR/samples + cp $ROO_HOME/annotations/target/*-$VERSION.jar $WORK_DIR/annotations + cp $ROO_HOME/target/all/*.jar $WORK_DIR/bundle + rm $WORK_DIR/bundle/org.springframework.roo.annotations-$VERSION.jar + rm $WORK_DIR/bundle/*junit*.jar + rm $WORK_DIR/bundle/*jsch*.jar + rm $WORK_DIR/bundle/*jgit*.jar + rm $WORK_DIR/bundle/*git*.jar + rm $WORK_DIR/bundle/*op4j*.jar + rm $WORK_DIR/bundle/*aopalliance-*.jar + rm $WORK_DIR/bundle/jackson-*.jar + rm $WORK_DIR/bundle/jcl-over-slf4j-*.jar + rm $WORK_DIR/bundle/servlet-api-*.jar + rm $WORK_DIR/bundle/slf4j-*.jar + rm $WORK_DIR/bundle/spring-*.jar + # These have to be removed as the Cloud Foundry add-on requires dependencies that are not bundled and thus must be installed via the shell. + rm $WORK_DIR/bundle/*cloud.foundry*.jar + rm $WORK_DIR/bundle/*cloud-foundry-api*.jar + rm $WORK_DIR/bundle/*AppCloudClient*.jar + mv $WORK_DIR/bundle/org.springframework.roo.bootstrap-*.jar $WORK_DIR/bin + mv $WORK_DIR/bundle/org.apache.felix.framework-*.jar $WORK_DIR/bin + cp $ROO_HOME/bootstrap/src/main/bin/* $WORK_DIR/bin + chmod 775 $WORK_DIR/bin/*.sh + cp $ROO_HOME/bootstrap/src/main/conf/* $WORK_DIR/conf + cp $ROO_HOME/bootstrap/readme.txt $WORK_DIR/ + cp `find $ROO_HOME -iname legal-\*.txt` $WORK_DIR/legal + cp `find $ROO_HOME -iname \*.roo | grep -v "/target/"` $WORK_DIR/samples + cp -r $ROO_HOME/deployment-support/target/site/reference/pdf/ $WORK_DIR/docs + cp -r $ROO_HOME/deployment-support/target/site/reference/html/ $WORK_DIR/docs + + # Prepare to write the ZIP + log "Cleaning $DIST_DIR" + rm -rf $DIST_DIR + mkdir -p $DIST_DIR + + # Change directories to avoid absolute paths + pushd $WORK_DIR/.. &>/dev/null + log "Running ZIP from `pwd`" + ZIP_OPTS='-q' + if [ "$VERBOSE" = "1" ]; then + ZIP_OPTS='-v' + fi + log "ZIP command: zip $ZIP_OPTS $ASSEMBLY_ZIP -r $RELEASE_IDENTIFIER" + zip $ZIP_OPTS $ASSEMBLY_ZIP -r $RELEASE_IDENTIFIER + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "ZIP process failed (zip exit code $EXITED)." >&2; exit 1; + fi + + # Hash the ZIP + pushd $DIST_DIR &>/dev/null + sha1sum *.zip > $ASSEMBLY_SHA + + # Sign the ZIP + GPG_OPTS='-q' + if [ "$VERBOSE" = "1" ]; then + GPG_OPTS='-v' + fi + grep "" ~/.m2/settings.xml &>/dev/null + EXITED=$? + if [[ ! "$EXITED" = "1" ]]; then + log "Found gpg.passphrase in ~/.m2/settings.xml..." + PASSPHRASE=`grep "" ~/.m2/settings.xml | sed 's/.*//' | sed 's/<\/gpg.passphrase>.*//'` + echo "$PASSPHRASE" | gpg $GPG_OPTS --batch --passphrase-fd 0 -a --output $ASSEMBLY_ASC --detach-sign $ASSEMBLY_ZIP + else + log "gpg.passphrase NOT found in ~/.m2/settings.xml. Trying with gpg agent." + gpg $GPG_OPTS -a --use-agent --output $ASSEMBLY_ASC --detach-sign $ASSEMBLY_ZIP + fi + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "GPG detached signature creation failed (gpg exit code $EXITED)." >&2; exit 1; + fi + + # Return to the original directory + popd &>/dev/null + popd &>/dev/null + + quick_zip_gpg_tests + + if [ ! "$TEST" = "0" ]; then + log "Unzipping Roo distribution to test area" + rm -rf /tmp/$RELEASE_IDENTIFIER + ZIP_OPTS='-qq' + if [ "$VERBOSE" = "1" ]; then + ZIP_OPTS='' + fi + unzip $ZIP_OPTS $ASSEMBLY_ZIP -d /tmp/ + ROO_CMD="/tmp/$RELEASE_IDENTIFIER/bin/roo.sh" + log "Roo command....: $ROO_CMD" + + # Setup Maven and the active repository (we must ensure the annotation JAR matches that in the assembly; it may not have yet been deployed publicly as it's just being tested at present) + MVN_CMD='mvn' + if [[ "$TEST" = "2" ]]; then + MVN_CMD="mvn -gs /tmp/settings.xml" + echo "/tmp/repository" > /tmp/settings.xml + rm -rf /tmp/repository + mkdir -p /tmp/repository/org/springframework/roo/org.springframework.roo.annotations/$VERSION/ + cp /tmp/$RELEASE_IDENTIFIER/annotations/* /tmp/repository/org/springframework/roo/org.springframework.roo.annotations/$VERSION/ + else + cp /tmp/$RELEASE_IDENTIFIER/annotations/* ~/.m2/repository/org/springframework/roo/org.springframework.roo.annotations/$VERSION/ + fi + if [ "$VERBOSE" = "0" ]; then + MVN_CMD="$MVN_CMD -q" + fi + + load_roo_build_and_test script vote.roo + tomcat_stop_start_get_stop http://localhost:8888/vote + + load_roo_build_and_test script clinic.roo + tomcat_stop_start_get_stop http://localhost:8888/petclinic + + load_roo_build_and_test script wedding.roo + tomcat_stop_start_get_stop http://localhost:8888/wedding + + load_roo_build_and_test script pizzashop.roo + tomcat_stop_start_get_stop http://localhost:8888/pizzashop + pizzashop_tests + + load_roo_build_and_test script bikeshop.roo + jetty_stop_start_get_stop http://localhost:8888/bikeshop/pages/main.jsf + + load_roo_build_and_test script multimodule.roo + tomcat_stop_start_get_stop http://localhost:8888/mvc + + load_roo_build_and_test script embedding.roo + tomcat_stop_start_get_stop http://localhost:8888/embedding + + log "Removing Roo distribution from test area" + rm -rf /tmp/$RELEASE_IDENTIFIER + log "Completed tests successfully" + fi +fi + +if [[ "$COMMAND" = "deploy" ]]; then + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + + if [ ! -f $ASSEMBLY_ZIP ]; then + l_error "Unable to find $ASSEMBLY_ZIP" + exit 1 + fi + if [ ! -f $ASSEMBLY_SHA ]; then + l_error "Unable to find $ASSEMBLY_SHA" + exit 1 + fi + + quick_zip_gpg_tests + + ZIP_FILENAME=`basename $ASSEMBLY_ZIP` + PROJECT_NAME="Spring Roo" + AWS_PATH="s3://spring-roo-repository.springsource.org/$TYPE/ROO/" + log "AWS bundle.ver.: $VERSION" + log "AWS rel.type...: $TYPE" + log "AWS pkg.f.name.: $ZIP_FILENAME" + log "AWS proj.name..: $PROJECT_NAME" + log "AWS Path.......: $AWS_PATH" + + type -P s3cmd &>/dev/null || { l_error "s3cmd not found. Aborting." >&2; exit 1; } + S3CMD_OPTS='' + if [ "$DRY_RUN" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS --dry-run" + fi + if [ "$VERBOSE" = "1" ]; then + S3CMD_OPTS="$S3CMD_OPTS -v" + fi + s3cmd $S3CMD_OPTS put --acl-public \ + "--add-header=x-amz-meta-bundle.version:$VERSION" \ + "--add-header=x-amz-meta-release.type:$TYPE" \ + "--add-header=x-amz-meta-package.file.name:$ZIP_FILENAME" \ + "--add-header=x-amz-meta-project.name:$PROJECT_NAME" \ + $ASSEMBLY_ZIP $AWS_PATH + EXITED=$? + if [[ ! "$EXITED" = "0" ]]; then + l_error "s3cmd failed (exit code $EXITED)." >&2; exit 1; + fi + + s3_execute put --acl-public $ASSEMBLY_SHA $AWS_PATH + s3_execute put --acl-public $ASSEMBLY_ASC $AWS_PATH + + # Clean up old snapshot releases (if we just performed a snapshot release) + if [[ "$TYPE" = "snapshot" ]]; then + s3_execute ls s3://spring-roo-repository.springsource.org/snapshot/ROO/ | grep '.zip$' | cut -c "30-"> /tmp/dist_all.txt + tail -n 5 /tmp/dist_all.txt > /tmp/dist_to_keep.txt + cat /tmp/dist_all.txt /tmp/dist_to_keep.txt | sort | uniq -u > /tmp/dist_to_delete.txt + for url in `cat /tmp/dist_to_delete.txt`; do + s3_execute del "$url" + s3_execute del "$url.asc" + s3_execute del "$url.sha1" + done + rm /tmp/dist_*.txt + fi +fi + +if [[ "$COMMAND" = "next" ]]; then + # We only need to change the _first_ element in each pom.xml to complete a version update + find $ROO_HOME -iname pom.xml -print0 | while read -d $'\0' file + do + log "Updating $file" + sed -i "0,/.*<\/version>/s//$NEXT<\/version>/" $file + done + log "Updating project templates" + sed -i "s/.*<\/roo.version>/$NEXT<\/roo.version>/" `find $ROO_HOME -iname *-template.xml` + log "Updating documentation" + sed -i "s/.*<\/releaseinfo>/$NEXT<\/releaseinfo>/" $ROO_HOME/deployment-support/src/site/docbook/reference/index.xml +fi + diff --git a/deployment-support/src/docbkx/resources/css/html.css b/deployment-support/src/docbkx/resources/css/html.css new file mode 100644 index 000000000..10936f337 --- /dev/null +++ b/deployment-support/src/docbkx/resources/css/html.css @@ -0,0 +1,421 @@ +body { + text-align: justify; + margin-right: 2em; + margin-left: 2em; +} + +a, + a[accesskey^ + += +"h" +] +, +a[accesskey^ + += +"n" +] +, +a[accesskey^ + += +"u" +] +, +a[accesskey^ + += +"p" +] +{ +font-family: Verdana, Arial, helvetica, sans-serif + +; +font-size: + +12 +px + +; +color: #003399 + +; +} + +a:active { + color: #003399; +} + +a:visited { + color: #888888; +} + +p { + font-family: Verdana, Arial, sans-serif; +} + +dt { + font-family: Verdana, Arial, sans-serif; + font-size: 12px; +} + +p, dl, dt, dd, blockquote { + color: #000000; + margin-bottom: 3px; + margin-top: 3px; + padding-top: 0px; +} + +ol, ul, p { + margin-top: 6px; + margin-bottom: 6px; +} + +p, blockquote { + font-size: 90%; +} + +p.releaseinfo { + font-size: 100%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; + padding-top: 10px; +} + +p.pubdate { + font-size: 120%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +td { + font-size: 80%; +} + +td, th, span { + color: #000000; +} + +td[width^ + += +"40%" +] +{ +font-family: Verdana, Arial, helvetica, sans-serif + +; +font-size: + +12 +px + +; +color: #003399 + +; +} + +table[summary^ + += +"Navigation header" +] +tbody tr th[colspan^ + += +"3" +] +{ +font-family: Verdana, Arial, helvetica, sans-serif + +; +} + +blockquote { + margin-right: 0px; +} + +h1, h2, h3, h4, h6, H6 { + color: #000000; + font-weight: 500; + margin-top: 0px; + padding-top: 14px; + font-family: Verdana, Arial, helvetica, sans-serif; + margin-bottom: 0px; +} + +h2.title { + font-weight: 800; + margin-bottom: 8px; +} + +h2.subtitle { + font-weight: 800; + margin-bottom: 20px; +} + +.firstname, .surname { + font-size: 12px; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +table { + border-collapse: collapse; + border-spacing: 0; + border: 1px black; + empty-cells: hide; + margin: 10px 0px 30px 50px; + width: 90%; +} + +div.table { + margin: 30px 0px 30px 0px; + border: 1px dashed gray; + padding: 10px; +} + +div .table-contents table { + border: 1px solid black; +} + +div.table > p.title { + padding-left: 10px; +} + +table[summary^ + += +"Navigation footer" +] +{ +border-collapse: collapse + +; +border-spacing: + +0 +; +border: + +1 +px black + +; +empty-cells: hide + +; +margin: + +0 +px + +; +width: + +100 +% +; +} + +table[summary^ + += +"Note" +] +, +table[summary^ + += +"Warning" +] +, +table[summary^ + += +"Tip" +] +{ +border-collapse: collapse + +; +border-spacing: + +0 +; +border: + +1 +px black + +; +empty-cells: hide + +; +margin: + +10 +px + +0 +px + +10 +px + +- +20 +px + +; +width: + +100 +% +; +} + +td { + padding: 4pt; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +div.warning TD { + text-align: justify; +} + +h1 { + font-size: 150%; +} + +h2 { + font-size: 110%; +} + +h3 { + font-size: 100%; + font-weight: bold; +} + +h4 { + font-size: 90%; + font-weight: bold; +} + +h5 { + font-size: 90%; + font-style: italic; +} + +h6 { + font-size: 100%; + font-style: italic; +} + +tt { + font-size: 110%; + font-family: "Courier New", Courier, monospace; + color: #000000; +} + +.navheader, .navfooter { + border: none; +} + +div.navfooter table { + border: dashed gray; + border-width: 1px 1px 1px 1px; + background-color: #cde48d; +} + +pre { + font-size: 110%; + padding: 5px; + border-style: solid; + border-width: 1px; + border-color: #CCCCCC; + background-color: #f3f5e9; +} + +ul, ol, li { + list-style: disc; +} + +hr { + width: 100%; + height: 1px; + background-color: #CCCCCC; + border-width: 0px; + padding: 0px; +} + +.variablelist { + padding-top: 10px; + padding-bottom: 10px; + margin: 0; +} + +.term { + font-weight: bold; +} + +.mediaobject { + padding-top: 30px; + padding-bottom: 30px; +} + +.legalnotice { + font-family: Verdana, Arial, helvetica, sans-serif; + font-size: 12px; + font-style: italic; +} + +.sidebar { + float: right; + margin: 10px 0px 10px 30px; + padding: 10px 20px 20px 20px; + width: 33%; + border: 1px solid black; + background-color: #F4F4F4; + font-size: 14px; +} + +.property { + font-family: "Courier New", Courier, monospace; +} + +a code { + font-family: Verdana, Arial, monospace; + font-size: 12px; +} + +td code { + font-size: 110%; +} + +div.note * td, + div.tip * td, + div.warning * td, + div.calloutlist * td { + text-align: justify; + font-size: 100%; +} + +.programlisting .interfacename, + .programlisting .literal, + .programlisting .classname { + font-size: 95%; +} + +.title .interfacename, + .title .literal, + .title .classname { + font-size: 130%; +} + +/* everything in a is displayed in a coloured, comment-like font */ +.programlisting * .lineannotation, + .programlisting * .lineannotation * { + color: green; +} diff --git a/deployment-support/src/docbkx/resources/images/banner.png b/deployment-support/src/docbkx/resources/images/banner.png new file mode 100644 index 000000000..8c778a579 Binary files /dev/null and b/deployment-support/src/docbkx/resources/images/banner.png differ diff --git a/deployment-support/src/docbkx/resources/images/important.png b/deployment-support/src/docbkx/resources/images/important.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/deployment-support/src/docbkx/resources/images/important.png differ diff --git a/deployment-support/src/docbkx/resources/images/note.png b/deployment-support/src/docbkx/resources/images/note.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/deployment-support/src/docbkx/resources/images/note.png differ diff --git a/deployment-support/src/docbkx/resources/images/tip.png b/deployment-support/src/docbkx/resources/images/tip.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/deployment-support/src/docbkx/resources/images/tip.png differ diff --git a/deployment-support/src/docbkx/resources/images/xdev-spring_logo.jpg b/deployment-support/src/docbkx/resources/images/xdev-spring_logo.jpg new file mode 100644 index 000000000..622962ee3 Binary files /dev/null and b/deployment-support/src/docbkx/resources/images/xdev-spring_logo.jpg differ diff --git a/deployment-support/src/docbkx/resources/xsl/fopdf.xsl b/deployment-support/src/docbkx/resources/xsl/fopdf.xsl new file mode 100644 index 000000000..0ef965b37 --- /dev/null +++ b/deployment-support/src/docbkx/resources/xsl/fopdf.xsl @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bold + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 1 + + + + + + + + book toc + + + + 2 + + + + + + + + + + 0 + 0 + 0 + + + 5mm + 10mm + 10mm + + 15mm + 10mm + 0mm + + 18mm + 18mm + + + 0pc + + + + + justify + false + + + 11 + 8 + + + 1.4 + + + + + + + 0.8em + + + + + + 17.4cm + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + 1 + + + + + + + + left + bold + + + pt + + + + + + + + + + + + + + + 0.8em + 0.8em + 0.8em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.6em + 0.6em + 0.6em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.4em + 0.4em + 0.4em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + bold + + + pt + + false + 0.4em + 0.6em + 0.8em + + + + + + + + + pt + + + + + 1em + 1em + 1em + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + + + 0 + 1 + + + 90 + + + + + '1' + + + + + + + figure after + example before + equation before + table before + procedure before + + + + 1 + + + + 0.8em + 0.8em + 0.8em + 0.1em + 0.1em + 0.1em + + + + + + + + + + + + + + + + + diff --git a/deployment-support/src/docbkx/resources/xsl/html.xsl b/deployment-support/src/docbkx/resources/xsl/html.xsl new file mode 100644 index 000000000..aa7930bab --- /dev/null +++ b/deployment-support/src/docbkx/resources/xsl/html.xsl @@ -0,0 +1,91 @@ + + + + + + + + + html.css + + + 1 + 0 + 1 + 0 + + + + + + book toc + + + + 3 + + + + + 1 + + + + + + + 0 + + + 90 + + + + + 0 + + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + + + + + +

    +

    Authors

    +

    + +

    + + + diff --git a/deployment-support/src/docbkx/resources/xsl/html_chunk.xsl b/deployment-support/src/docbkx/resources/xsl/html_chunk.xsl new file mode 100644 index 000000000..d0fc426dd --- /dev/null +++ b/deployment-support/src/docbkx/resources/xsl/html_chunk.xsl @@ -0,0 +1,208 @@ + + + + + + + '5' + '1' + html.css + + 1 + 0 + 1 + 0 + + + + book toc + + + 3 + + + 1 + + + + + 90 + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + + + + + +
    +

    Authors

    +

    + +

    +
    + + + +
    + + + + 1 + + + + + + + + + + + + + + diff --git a/deployment-support/src/main/asciidoc/index.adoc b/deployment-support/src/main/asciidoc/index.adoc new file mode 100644 index 000000000..54d2d62ac --- /dev/null +++ b/deployment-support/src/main/asciidoc/index.adoc @@ -0,0 +1,102 @@ +// +// Prerequisites: +// +// ruby 1.9.3+ +// asciidoctor (use gem to install) +// asciidoctor-pdf (use gem to install) +// +// Build the document: +// +// HTML5 +// +// $ asciidoctor -b html5 index.adoc +// +// PDF +// +// $ asciidoctor-pdf index.adoc +// += Spring Roo - Reference Documentation +:author: DISID Corporation S.L. - Pivotal Software, Inc +:description: Spring Roo - Reference Documentation +:copyright: CC BY-NC-SA 3.0 +:doctype: book +:toc: +:toclevels: 3 +:backend: docbook + +[abstract] +_Copyright 2009-2014 VMware, Inc. All Rights Reserved._ +_Copies of this document may be made for your own use and for + distribution to others, provided that you do not charge any fee for such + copies and further provided that each copy contains this Copyright + Notice, whether distributed in print or electronically._ + +== Welcome to Spring Roo [[welcome]] + +Welcome to Spring Roo! In this part of the reference guide we will explore everything you need to know in order to use Roo effectively. We've designed this part so that you can read each chapter consecutively and stop at any time. However, the more you read, the more you'll learn and the easier you'll find it to work with Roo. + +Parts <>, <> and <> of this manual are more designed for reference usage and people who wish to extend Roo itself. + +include::welcome-intro.adoc[] + +include::welcome-beginning.adoc[] + +include::welcome-architecture.adoc[] + +include::welcome-usage.adoc[] + +include::welcome-existing.adoc[] + +include::welcome-removing.adoc[] + +== Base Add-Ons [[base]] + +This part of the reference guide provides a detailed reference to the major Roo base add-ons and how they work. This part goes into more detail than the <> and offers a "bigger picture" discussion than the <> appendix. + +include::base-overview.xml.adoc[] + +include::base-persistence.xml.adoc[] + +include::base-dbre.xml.adoc[] + +include::base-layers.xml.adoc[] + +include::base-web.xml.adoc[] + +include::base-jsf.xml.adoc[] + +include::base-cloud-foundry.xml.adoc[] + +include::base-json.xml.adoc[] + +include::base-solr.xml.adoc[] + +== Internals and Add-On Development [[internals]] + +In this part of the guide we reveal how Roo works internally. With this knowledge you'll be well-positioned to be able to check out the Roo codebase, build a development release, and write add-ons to extend Roo. + +You should be familiar with <> of this reference guide and ideally have used Roo for a period of time to gain the most value from this part. + +include::internals-development.adoc[] + +include::internals-simple-add-ons.adoc[] + +include::internals-advanced-add-ons.adoc[] + +== External Add-Ons [[external-addons]] + +In this part of the guide we detail external Roo add-ons. + +include::add-on-tailor.adoc[] + +== Appendices [[appendices]] + +The fourth and final part of the reference guide provides appendices and background information that does not neatly belong within the other parts. The information is intended to be treated as a reference and not read consecutively. + +include::appendix-command-index.adoc[] + +include::appendix-upgrade.adoc[] + +include::appendix-background.adoc[] + +include::appendix-resources.adoc[] diff --git a/deployment-support/src/site/apt/index.apt b/deployment-support/src/site/apt/index.apt new file mode 100644 index 000000000..6227e69fa --- /dev/null +++ b/deployment-support/src/site/apt/index.apt @@ -0,0 +1,10 @@ + ------ + Spring Roo Staging Server + ------ + + + Welcome to the {{{http://projects.spring.io/spring-roo/}Spring Roo}} build system staging server. + + Please visit the {{{http://projects.spring.io/spring-roo/}official home page}} for actual content. + + diff --git a/deployment-support/src/site/apt/reference/index.apt b/deployment-support/src/site/apt/reference/index.apt new file mode 100644 index 000000000..618906a8c --- /dev/null +++ b/deployment-support/src/site/apt/reference/index.apt @@ -0,0 +1,8 @@ + ----- + Spring Roo Documentation + ----- + + This is no longer the correct location to access Spring Roo documentation. + + Please visit the {{{http://projects.spring.io/spring-roo/}official home page}} for links to the latest documentation site. Please update your links accordingly. + diff --git a/deployment-support/src/site/docbook/reference/add-on-tailor.xml b/deployment-support/src/site/docbook/reference/add-on-tailor.xml new file mode 100644 index 000000000..7a120eec3 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/add-on-tailor.xml @@ -0,0 +1,494 @@ + + + Tailor Add-On + +
    + Introduction + + Roo has been become more and more powerful and offers more options + for users on how to use Roo. This in turn makes it more challenging in + some scenarios to use Roo in a consistent way throughout a project. + + The tailor addon enables: + + + + Teams working on large projects to ensure streamlined Roo usage + according to their project's standards and guidelines + + + + + + Single users to define the approach they usually take in one + file to reuse it over multiple projects + + + + Examples of use cases: + + + + A team does not want to use the Active Record pattern for + entities, but always wants developers to specify "--activeRecord + false", and create a JPA repository based on every new entity. + + + + + + A developer always uses a certain project structure to create + web projects, for exmple a Maven project with 2 modules called + "domain" and "web". The developer wants to be able to reuse this + structure with the project command, and make sure that the shell + automatically focuses on the correct module for certain commands (e.g. + entity > domain, web mvc > web). + + +
    + +
    + How it works + + When tailoring is activated, Roo commands are intercepted by the + shell and transformed to a new set of commands according to user + specifications obtained from configuration file, if any exist for that + particular command. The shell then executes this transformed set of + commands instead of the initial command. A user can define one or multiple + tailor configurations and activate and deactivate them while working with + the shell. + + With the tailor add-on, you can + define: + + + + Reusable project structures to use with the "project" + command + + + + + + Default target modules for commands. + + + + + + Default values for command arguments. + + + + + + Chains of commands, either triggered by an existing command or + composed by an alias + + + + Note that although a tailor configuration can save you a lot of time + and effort, it cancels out some of the shell's command completion benefits + at the same time. For example, some commands are only available in certain + modules (e.g. JPA commands are only available in modules with JPA setup). + Tailoring a default module for JPA commands like "entity jpa" means that + you can execute those commands while focused on modules without JPA setup. + But the tailoring only kicks in at execution time, so the shell won't know + about it while you are typing. Thus, the shell won't offer command + completion for these commands because it thinks they are not + available. +
    + +
    + Tailor Add-On Commands + + tailor list - Shows the list of + available tailor configurations. A tailor configuration defines the set of + transformation you want executed for certain commands (see next section + "Tailor Configuration"). + + roo> tailor list +Available tailor configurations: + o webstyle - Web project with 2 modules, DOMAIN and PRESENTATION + + tailor activate – Activate one of + the available configurations. + + roo> tailor activate --name webstyle + + "tailor list" indicates which configuration is currently + activated: + + roo> tailor list +Available tailor configurations: + o webstyle [ ACTIVE ] - Web project with 2 modules, DOMAIN and PRESENTATION + + + tailor deactivate – Deactivate the + tailor mode. There is no active configuration after this command + + roo> tailor deactivate +
    + +
    + Tailor Configuration + + A tailor configuration can be created in two ways: + + + + XML configuration file (no add-on development required) + + + + + + Directly in Java (requires creation and installation of a new + add-on) + + + + Each tailor configuration has one or more command configurations. A + command configuration defines a set of Actions that are triggered whenever + a certain command is executed. Execution of those actions results in a new + list of output commands that will eventually be executed by the shell. A + command configuration is triggered whenever a command that starts with a + defined string is executed. E.g., if a command configuration defines "web + mvc" as a trigger, then it will be used by the tailor every time a "web + mvc" subcommand is executed. The order in which you define the command + configurations might matter, the tailor will always take the first command + configuration that matches a command. + + An action is a transformation step + to be applied to the command defined in a command configuration. Each + action type defines a set of parameters that can be set in a tailor + definition. The tailor addon can be extended with more action types by the + community. + + Actions are executed sequentially by the tailor, so the order in + which they are declared matters. + + The following actions are currently available: + +
    + Actions + +
    + execute + + Adds a command to the list of commands to be executed. Note that + each command configuration should have at least one execute action, + otherwise the tailor will not lead to any command executions. + + + + command + + + Command line to be executed. If empty, this action will + add the original command to the list of output commands at this + point. (optional) + + + + + exclude + + + A comma separated list of arguments that should be removed + from the command before execution. This can be useful if the + original command is executed ("command" argument not set), and + it was enhanced with additional arguments for the benefit of the + tailoring. (optional) + + + +
    + +
    + defaultvalue + + If the Roo user does not provide a value for an argument with + the given name on the shell, this default value will be chosen. + + + + argument + + + Name of the Roo command's argument that will get a default + value. (mandatory) + + + + + value + + + Default value for the argument. (mandatory) + + + + + force + + + If "true", the default value will be chosen even if the + user specified an alternative value in the command. (optional, + defaults to "false") + + + +
    + +
    + focus + + + + module + + + Focus on a module, in form of a simple pattern to match + against the module names. Does not support regular expressions, + just a simple "contains" match. Use this instead of an "execute + command 'module focus...'" if you do not want to hard code your + module names into the reusable tailor configuration. + (mandatory) + + Advanced usage: Use a comma-separated + list of strings to look for in module names. The comma will be + interpreted as "AND" by the search for a module. Use a slash "/" + before a string in the list to indicate that this next string + must "NOT" be contained in the module name. + + + +
    +
    + +
    + XML Configuration + + This section describes how to create a tailor configuration with + XML by examples. + + The XML configuration file “tailor.xml” must be placed into the + root project folder. Alternatively, you can put a "tailor.xml" into your + system's user folder, to maintain tailor configurations that you want to + reuse over several projects. The tailor addon will only look for this + file if it does not find a tailor.xml file in the project root. + +
    + Example 1: Tailor the "project" command + + The following configuration defines a chain of commands that + will be triggered by the project command, to create a parent project + with packaging “pom” with two modules named “projectname-domain” and + “projectname-data”. + + Note how you can use argument values from the input command as + placeholders by using “${argumentname}”. + + tailor.xml: + + <tailor name="mywebstyle" description="Standards for web projects with 2 modules"> + <config command="project"> + <action type="defaultvalue" argument="packaging" value="pom" /> + <action type="execute" /> + <action type="execute" command="module create --moduleName ${projectName}-domain --topLevelPackage ${topLevelPackage}"/> + <action type="focus" module="~"/> + <action type="execute" command="module create --moduleName ${projectName}-web --topLevelPackage ${topLevelPackage} --packaging war"/> + <action type="focus" module="${projectName}-domain"/> + </config> +</tailor> + + + Shell: + + tailor activate --name mywebstyle +project --topLevelPackage com.foo.sample --projectName myapp + + + Will result in: + + project --topLevelPackage com.foo.sample --projectName mywebapp --packaging pom +module create --moduleName myapp-domain --topLevelPackage com.foo.sample +module focus --moduleName ~ +module create --moduleName myapp-web --topLevelPackage com.foo.sample --packaging war +module focus --moduleName myapp-domain + +
    + +
    + Example 2: Default target modules and default values + + The following example shows how to tailor the “entity jpa” + command with a default value for the "activeRecord" argument, and a + default module to put all entities in. + + Note that the module name value for the "focus" action is + interpreted as "module name contains x". That is + why this example works with the project setup described in the + previous example, which sets up a module named + "${projectName]-domain". + + tailor.xml: + + <config command="entity jpa"> + <action type="focus" module="domain"/> + <action type="defaultvalue" argument="--activeRecord" value="false"/> + <action type="execute"/> +</config> + + + Shell: + + entity jpa --class ~.Customer + + Results in: + + module focus --moduleName webapp-domain +entity jpa --class ~.Customer --activeRecord false +
    + +
    + Example 3: Alias command to create layers + + In this example, the tailor configuration defines a new alias + command that will trigger a set of other commands to scaffold + repository, service and web layer for an entity. Note that this + configuration does not define the "execute" action to execute the + original "layer" command. + + Although "layer" is not a command known to the shell, it won’t + produce an error, because the tailor will transform it into a set of + different commands, excluding the original. The downside is that you + won’t get command completion support for this alias from the + shell. + + tailor.xml: + + <config command="layer"> + <action type="focus" module="domain"/> + <!-- Create spring data JPA repository --> + <action type="execute" command="repository jpa --interface ${entity}Repository --entity ${entity}"/> + <!-- Create service interface and implementation class--> + <action type="execute" command="service --interface ${entity}Service --class ${entity}ServiceImpl --entity ${entity}"/> + <action type="focus" module="web"/> + <action type="execute" command="web mvc scaffold --class ${entity}Controller --backingType ${entity}"/> +</config> + + + Shell: + + layer --entity ~.Customer + + Results in: + + module focus --moduleName webapp-domain +repository jpa --interface ~.CustomerRepository --entity ~.Customer +service --interface ~.CustomerService --class ~.CustomerServiceImpl --entity ~.Customer +module focus --moduleName webapp-web +web mvc scaffold --class ~.CustomerController --backingType ~.Customer + +
    +
    + +
    + Configuration Addon + + A new tailor configuration can also be defined in Java, instead of + XML. This requires the creation of a new simple addon that you would + need to build and install as a bundle in your Roo installation. Once + your tailor extension bundle is running, the “tailor” commands will + recognize all tailor configurations you implemented in that + addon. + + This is a more static and elaborate way of creating tailor + configurations. However, it might be useful if you want to distribute a + configuration to a large group of users. + + After you created a new (simple) addon, you need to do the + following: + + + + Add dependency to addon-tailor + + + <dependency> + <groupId>org.springframework.roo</groupId> + <artifactId>org.springframework.roo.addon.tailor</artifactId> + </dependency> + + + + + + Create a class that implements + TailorConfigurationFactory + + + @Component +@Service +public class TailorWebSimpleConfiguration implements TailorConfigurationFactory { + ... +} + + + + + Override createTailorConfiguration() + + + @Override +public TailorConfiguration createTailorConfiguration() { + String description = "Web project with 2 modules DOMAIN-PRESENTATION"; + TailorConfiguration configuration = new TailorConfiguration("webstyle-simple", description); + configuration.addCommandConfig(createCommandConfigProject()); + configuration.addCommandConfig(createCommandConfigJpaSetup()); + return configuration; +} + + + + + + Implement and add the CommandConfiguration objects you want to + support. + + + Add a chain of actions similar to how you would do in an XML + configuration file, as described above. + + private CommandConfiguration createCommandConfigJpaSetup() { + CommandConfiguration config = new CommandConfiguration(); + config.setCommandName("jpa setup"); + config.addAction(ActionConfigFactory.focusAction( + "domain")); + config.addAction(ActionConfigFactory.defaultArgumentAction( + "database", "HYPERSONIC_IN_MEMORY")); + config.addAction(ActionConfigFactory.defaultArgumentAction( + "provider", "HIBERNATE")); + config.addAction(ActionConfigFactory.executeAction()); + return config; +} + + + + + + +
    +
    +
    diff --git a/deployment-support/src/site/docbook/reference/appendix-background.xml b/deployment-support/src/site/docbook/reference/appendix-background.xml new file mode 100644 index 000000000..5a76d643b --- /dev/null +++ b/deployment-support/src/site/docbook/reference/appendix-background.xml @@ -0,0 +1,209 @@ + + + Project Background + + This chapter briefly covers the history of the Spring Roo project, and + also explains its mission + statement in detail. + +
    + History + + The Spring Roo available today is the result of relatively recent + engineering, but the inspiration for the project can be found several + years earlier. + + The historical motivation for "ROO" can be traced back to 2005. At + that time the project's founder, Ben Alex, was working on several + enterprise applications and had noticed he was repeating the same steps + time and time again. Back in 2005 it was common to use a traditional + layering involving DAOs, services layer and web tier. A good deal of + attention was also focused around that time on avoiding anaemic domain + objects and instead pursuing Domain Driven Design + principles. + + Pursuing a rich domain model led to domain objects that reflected + proper object oriented principles, such as careful application of + encapsulation, immutability and properly defining the role of domain + objects within the enterprise application layering. Rich behaviour was + added to these entities via AspectJ and + Spring + Framework's recently-created @Configurable annotation (which + enabled dependency injection on entities irrespective of how the entities + were instantiated). Naturally the web frameworks of the era didn't work + well with these rich domain objects (due to the lack of accessors, + mutators and no-argument constructors), and as such data transfer objects + (DTOs) were created. The mapping between DTOs and domain objects was + approached with assembly technologies like Dozer. To make all of this + work nicely together, a code generator called Real Object Oriented - or + "ROO" - was created. The Real Object Oriented name reflected the rich + domain object principles that underpinned the productivity tool. + + ROO was presented to audiences at the SpringOne Americas 2006 and + TSSJS Europe 2007 conferences, plus the Stockholm Spring User Group and + Enterprise Java Association of Australia. The audiences were enthusiastic + about the highly productive solution, with remarks like "it is + the really neatest and newest stuff I've seen in this conference" + and "if + ROO ever becomes an open source project, I'm guessing it will be very + polished and well-received". Nonetheless, other priorities (like + the existing Spring + Security project) prevented the code from becoming release-ready. + More than twelve months later Ben was still regularly being asked by + people, "whatever happened to the ROO framework?" and as such he set out + about resuming the project around August 2008. + + By October 2008 a large amount of research and development had been + undertaken on the new-and-improved ROO. The original productivity ideas + within ROO had been augmented with considerable feedback from real-life + use of ROO and the earlier conferences. In particular a number of projects + in Australia had used the unreleased ROO technology and these projects + provided a great deal of especially useful feedback. It was recognised + from this feedback that the original ROO model suffered from two main + problems. First, it did not provide a highly usable interface and as such + developers required a reasonable amount of training to fully make use of + Roo. Second, it imposed a high level of architectural purity on all + applications - such as the forced use of DTOs - and many people simply + didn't want such purity. While there were valid engineering reasons to + pursue such an architecture, it was the productivity that motivated people + to use ROO and they found the added burden of issues like DTO mapping + cancelled out some of the gains that ROO provided. A mission statement was drafted that + concisely reflected the vision of the project, and this was used to guide + the technical design. + + In early December 2008 Ben took a completely rewritten ROO with him + to SpringOne Americas 2008 and showed it to a number of SpringSource + colleagues and community members. The response was overwhelming. Not only + had the earlier feedback been addressed, but many new ideas had been + incorporated into the Java-only framework. Furthermore, recent + improvements to AspectJ and Spring had made the entire solution far more + effective and efficient than the earlier ROO model (such as + annotation-based component scanning, considerable enhancements to AJDT + etc). + + Feedback following the December 2008 demonstrations led to + considerable focus on bringing the ROO technology to the open source + community. The name "ROO" was preserved as a temporary codename, given + that we planned to select a final name closer to official release. The + "ROO" project was then publicly presented on 27 April 2009 during Rod + Johnson's SpringOne Europe keynote, "The + Future of Java Innovation". As part of the keynote the ROO system + was used to build a voting application that would allow the community to + select a final name for the new project. The "ROO" name was left as an + option, although the case was changed to "Roo" to reflect the fact it no + longer represented any acronym. The resulting votes were Spring Roo (467), + Spring Boost (180), Spring Spark (179), Spring HyperDrive (64) and Spring + Dart (62). As such "Spring Roo" became the official, community-selected + name for the project. + + Roo 1.0.0.A1 was released during the SpringOne Europe 2009 + conference, along with initial tooling for SpringSource Tool + Suite. The Roo talk at the SpringOne Europe 2009 conference was the + most highly attended session and there was enormous enthusiasm for the + solution. Roo 1.0.0.A2 was published a few weeks later, followed by + several milestones. By SpringOne/2GX North America in October 2009, Roo + 1.0.0 had reached Release Candidate 2 stage, and again the Roo session was + the most highly attended session of the entire conference. SpringSource also started + hosting the highly popular Spring Discovery + Days and showing people around the world what they could do with + the exciting new Roo tool. Coupled with Twitter, by this stage many members of + the Java community had caught a glimpse of Roo and it was starting to + appear in a large number of conferences, user group meetings and + development projects - all before it had even reached 1.0.0 General + Availability! +
    + +
    + Mission Statement + + Spring Roo's mission is to "fundamentally and sustainably + improve Java developer productivity without compromising engineering + integrity or flexibility". + + Here's exactly what we mean by this: + + + + "fundamentally": We believe a fundamental + improvement in developer productivity is attainable. Tools, + methodologies and frameworks that offer incidental improvement are + nowhere near enough. + + + + "and sustainably improve": A one-off + improvement in productivity isn't enough. The productivity improvement + needs to sustain beyond the initial jump-start, and continue unabated + over a multi-year period. Productivity must remain high even in the + face of radically changing requirements, evolving project team + membership, and new platform versions + + + + "Java developer productivity": Our focus is + unashamedly on developers who work with the most popular programming + language in the world, Java. We don't expect Java developers to learn + new programming languages and frameworks simply to enjoy a + productivity gain. We want to harness their existing Java knowledge, + skills and experience, rather than expect them to unlearn what they + already know. The conceptual weight must be attainable and reasonable. + We always favour evolution over revolution, and provide a solution + that is as fun, flexible and intuitive as possible. + + + + "without compromising": Other tools, + methodologies and frameworks claim to create solutions that provide + these benefits. However, they impose a serious cost in critical areas. + We refuse to make this compromise. + + + + "engineering integrity": We embrace OO and + language features the way Java language designers intended, greatly + simplifying understanding, refactoring, testing and debugging. We + don't force projects with significant performance requirements to + choose between developer productivity or deployment cost. We move + processing to Generation IV web clients where possible, embrace + database capabilities, and offer an optimal approach to runtime + considerations. + + + + "or flexibility": Projects are similar, but + not identical. Developers need the flexibility to use a different + technology, pattern or framework when required. While we don't lock + developers into particular approaches, we certainly provide an optimal + experience when following our recommendations. We ensure that our + technology is interface agnostic, gracefully supporting both + mainstream IDEs plus the command line. Of course, we support any + reasonable deployment scenario, and particularly the emerging class of + Generation IV web clients. + + + + We believe that Spring Roo today represents a successful embodiment + of this mission statement. While we still have work to do in identified + feature areas such as Generation IV web clients, these are easily-achieved + future directions upon the existing Roo foundation. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/appendix-command-index.xml b/deployment-support/src/site/docbook/reference/appendix-command-index.xml new file mode 100644 index 000000000..8c61ee8df --- /dev/null +++ b/deployment-support/src/site/docbook/reference/appendix-command-index.xml @@ -0,0 +1,4312 @@ + + + Command Index + This appendix was automatically built from Roo 1.3.0.RELEASE [rev 770570e]. + Commands are listed in alphabetic order, and are shown in monospaced font with any mandatory options you must specify when using the command. Most commands accept a large number of options, and all of the possible options for each command are presented in this appendix. +
    + Add On Commands + Add On Commands are contained in org.springframework.roo.addon.roobot.client.AddOnCommands. +
    + addon feedback bundle + Provide anonymous ratings and comments on a Spring Roo Add-on (your feedback will be published publicly) + + + + --bundleSymbolicName + + The bundle symbolic name for the add-on of interest; default: '__NULL__' (mandatory) + + + + --rating + + How much did you like this add-on?; default: '__NULL__' (mandatory) + + + + --comment + + Your comments on this add-on eg "this is my comment!"; limit of 140 characters; default: '__NULL__' + + + +
    +
    + addon info bundle + Provide information about a specific Spring Roo Add-on + + + + --bundleSymbolicName + + The bundle symbolic name for the add-on of interest; default: '__NULL__' (mandatory) + + + +
    +
    + addon info id + Provide information about a specific Spring Roo Add-on + + + + --searchResultId + + The bundle ID as presented via the addon list or addon search command; default: '__NULL__' (mandatory) + + + +
    +
    + addon install bundle + Install Spring Roo Add-on + + + + --bundleSymbolicName + + The bundle symbolic name for the add-on of interest; default: '__NULL__' (mandatory) + + + +
    +
    + addon install id + Install Spring Roo Add-on + + + + --searchResultId + + The bundle ID as presented via the addon list or addon search command; default: '__NULL__' (mandatory) + + + +
    +
    + addon list + List all known Spring Roo Add-ons (up to the maximum number displayed on a single page) + + + + --refresh + + Refresh the add-on index from the Internet; default if option present: 'true'; default if option not present: 'false' + + + + --linesPerResult + + The maximum number of lines displayed per add-on; default: '2' + + + + --maxResults + + The maximum number of add-ons to list; default: '99' + + + + --trustedOnly + + Only display trusted add-ons in search results; default if option present: 'true'; default if option not present: 'false' + + + + --communityOnly + + Only display community provided add-ons in search results; default if option present: 'true'; default if option not present: 'false' + + + + --compatibleOnly + + Only display compatible add-ons in search results; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + addon remove + Remove Spring Roo Add-on + + + + --bundleSymbolicName + + The bundle symbolic name for the add-on of interest; default: '__NULL__' (mandatory) + + + +
    +
    + addon search + Search all known Spring Roo Add-ons + + + + --requiresDescription + + A comma separated list of search terms; default: '*' + + + + --refresh + + Refresh the add-on index from the Internet; default if option present: 'true'; default if option not present: 'false' + + + + --linesPerResult + + The maximum number of lines displayed per add-on; default: '2' + + + + --maxResults + + The maximum number of add-ons to list; default: '20' + + + + --trustedOnly + + Only display trusted add-ons in search results; default if option present: 'true'; default if option not present: 'false' + + + + --compatibleOnly + + Only display compatible add-ons in search results; default if option present: 'true'; default if option not present: 'false' + + + + --communityOnly + + Only display community provided add-ons in search results; default if option present: 'true'; default if option not present: 'false' + + + + --requiresCommand + + Only display add-ons in search results that offer this command; default: '__NULL__' + + + +
    +
    + addon upgrade all + Upgrade all relevant Spring Roo Add-ons / Components for the current stability level + + This command does not accept any options. +
    +
    + addon upgrade available + List available Spring Roo Add-on / Component upgrades + + + + --addonStabilityLevel + + The stability level of add-ons or components which are presented for upgrading (default: ANY); default: '__NULL__' + + + +
    +
    + addon upgrade bundle + Upgrade a specific Spring Roo Add-on / Component + + + + --bundleSymbolicName + + The bundle symbolic name for the add-on to upgrade; default: '__NULL__' (mandatory) + + + +
    +
    + addon upgrade id + Upgrade a specific Spring Roo Add-on / Component from a search result ID + + + + --searchResultId + + The bundle ID as presented via the addon list or addon search command; default: '__NULL__' (mandatory) + + + +
    +
    + addon upgrade settings + Settings for Add-on upgrade operations + + + + --addonStabilityLevel + + The stability level of add-ons or components which are presented for upgrading; default: '__NULL__' + + + +
    +
    +
    + Backup Commands + Backup Commands are contained in org.springframework.roo.addon.backup.BackupCommands. +
    + backup + Backup your project to a zip file + + This command does not accept any options. +
    +
    +
    + Classpath Commands + Classpath Commands are contained in org.springframework.roo.classpath.operations.ClasspathCommands. +
    + class + Creates a new Java class source file in any project path + + + + --class + + The name of the class to create; default: '__NULL__' (mandatory) + + + + --rooAnnotations + + Whether the generated class should have common Roo annotations; default if option present: 'true'; default if option not present: 'false' + + + + --path + + Source directory to create the class in; default: 'FOCUSED|SRC_MAIN_JAVA' + + + + --extends + + The superclass (defaults to java.lang.Object); default if option not present: 'java.lang.Object' + + + + --implements + + The interface to implement; default: '__NULL__' + + + + --abstract + + Whether the generated class should be marked as abstract; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + constructor + Creates a class constructor + + + + --class + + The name of the class to receive this constructor; default if option not present: '*' + + + + --fields + + The fields to include in the constructor. Multiple field names must be a double-quoted list separated by spaces + + + +
    +
    + enum constant + Inserts a new enum constant into an enum + + + + --class + + The name of the enum class to receive this field; default if option not present: '*' + + + + --name + + The name of the constant; default: '__NULL__' (mandatory) + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + enum type + Creates a new Java enum source file in any project path + + + + --class + + The name of the enum to create; default: '__NULL__' (mandatory) + + + + --path + + Source directory to create the enum in; default: 'FOCUSED|SRC_MAIN_JAVA' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + focus + Changes focus to a different type + + + + --class + + The type to focus on; default: '__NULL__' (mandatory) + + + +
    +
    + interface + Creates a new Java interface source file in any project path + + + + --class + + The name of the interface to create; default: '__NULL__' (mandatory) + + + + --path + + Source directory to create the interface in; default: 'FOCUSED|SRC_MAIN_JAVA' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + Cloud Commands + Cloud Commands are contained in org.springframework.roo.addon.cloud.CloudCommands. +
    + cloud setup + Setup Cloud Provider on Spring Roo Project + + + + --provider + + Cloud Provider's Name; default: '__NULL__' (mandatory) + + + + --configuration + + Plugin Configuration. Add configuration by command like 'key=value,key2=value2,key3=value3'; default: '__NULL__' + + + +
    +
    +
    + Controller Commands + Controller Commands are contained in org.springframework.roo.addon.web.mvc.controller.ControllerCommands. +
    + controller all + Scaffold controllers for all project entities without an existing controller - deprecated, use 'web mvc setup' + 'web mvc all' instead + + + + --package + + The package in which new controllers will be placed; default: '__NULL__' (mandatory) + + + +
    +
    + controller scaffold + Create a new scaffold Controller (ie where we maintain CRUD automatically) - deprecated, use 'web mvc scaffold' instead + + + + --class + + The path and name of the controller object to be created; default: '__NULL__' (mandatory) + + + + --entity + + The name of the entity object which the controller exposes to the web tier; default if option not present: '*' + + + + --path + + The base path under which the controller listens for RESTful requests (defaults to the simple name of the form backing object); default: '__NULL__' + + + + --disallowedOperations + + A comma separated list of operations (only create, update, delete allowed) that should not be generated in the controller; default: '__NULL__' + + + +
    +
    + web mvc all + Scaffold Spring MVC controllers for all project entities without an existing controller + + + + --package + + The package in which new controllers will be placed; default: '__NULL__' (mandatory) + + + +
    +
    + web mvc scaffold + Create a new scaffold Controller (ie where Roo maintains CRUD functionality automatically) + + + + --class + + The path and name of the controller object to be created; default: '__NULL__' (mandatory) + + + + --backingType + + The name of the form backing type which the controller exposes to the web tier; default if option not present: '*' + + + + --path + + The base path under which the controller listens for RESTful requests (defaults to the simple name of the form backing object); default: '__NULL__' + + + + --disallowedOperations + + A comma separated list of operations (only create, update, delete allowed) that should not be generated in the controller; default: '__NULL__' + + + +
    +
    +
    + Creator Commands + Creator Commands are contained in org.springframework.roo.addon.creator.CreatorCommands. +
    + addon create advanced + Create a new advanced add-on for Spring Roo (commands + operations + metadata + trigger annotation + dependencies) + + + + --topLevelPackage + + The top level package of the new addon; default: '__NULL__' (mandatory) + + + + --description + + Description of your addon (surround text with double quotes); default: '__NULL__' + + + + --projectName + + Provide a custom project name (if not provided the top level package name will be used instead); default: '__NULL__' + + + +
    +
    + addon create i18n + Create a new Internationalization add-on for Spring Roo + + + + --topLevelPackage + + The top level package of the new addon; default: '__NULL__' (mandatory) + + + + --locale + + The locale abbreviation (ie: en, or more specific like en_AU, or de_DE); default: '__NULL__' (mandatory) + + + + --messageBundle + + Fully qualified path to the messages_xx.properties file; default: '__NULL__' (mandatory) + + + + --language + + The full name of the language (used as a label for the UI); default: '__NULL__' + + + + --flagGraphic + + Fully qualified path to flag xx.png file; default: '__NULL__' + + + + --description + + Description of your addon (surround text with double quotes); default: '__NULL__' + + + + --projectName + + Provide a custom project name (if not provided the top level package name will be used instead); default: '__NULL__' + + + +
    +
    + addon create simple + Create a new simple add-on for Spring Roo (commands + operations) + + + + --topLevelPackage + + The top level package of the new addon; default: '__NULL__' (mandatory) + + + + --description + + Description of your addon (surround text with double quotes); default: '__NULL__' + + + + --projectName + + Provide a custom project name (if not provided the top level package name will be used instead); default: '__NULL__' + + + +
    +
    + addon create wrapper + Create a new add-on for Spring Roo which wraps a maven artifact to create a OSGi compliant bundle + + + + --topLevelPackage + + The top level package of the new wrapper bundle; default: '__NULL__' (mandatory) + + + + --groupId + + Dependency group id; default: '__NULL__' (mandatory) + + + + --artifactId + + Dependency artifact id); default: '__NULL__' (mandatory) + + + + --version + + Dependency version; default: '__NULL__' (mandatory) + + + + --vendorName + + Dependency vendor name); default: '__NULL__' (mandatory) + + + + --licenseUrl + + Dependency license URL; default: '__NULL__' (mandatory) + + + + --docUrl + + Dependency documentation URL; default: '__NULL__' + + + + --description + + Description of the bundle (use keywords with #-tags for better search integration); default: '__NULL__' + + + + --projectName + + Provide a custom project name (if not provided the top level package name will be used instead); default: '__NULL__' + + + + --osgiImports + + Contents of Import-Package in OSGi manifest; default: '__NULL__' + + + +
    +
    +
    + Data On Demand Commands + Data On Demand Commands are contained in org.springframework.roo.addon.dod.DataOnDemandCommands. +
    + dod + Creates a new data on demand for the specified entity + + + + --entity + + The entity which this data on demand class will create and modify as required; default if option not present: '*' + + + + --class + + The class which will be created to hold this data on demand provider (defaults to the entity name + 'DataOnDemand'); default: '__NULL__' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + Dbre Commands + Dbre Commands are contained in org.springframework.roo.addon.dbre.DbreCommands. +
    + database introspect + Displays database metadata + + + + --schema + + The database schema names. Multiple schema names must be a double-quoted list separated by spaces; default: '__NULL__' (mandatory) + + + + --file + + The file to save the metadata to; default: '__NULL__' + + + + --enableViews + + Display database views; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + database reverse engineer + Create and update entities based on database metadata + + + + --schema + + The database schema names. Multiple schema names must be a double-quoted list separated by spaces; default: '__NULL__' (mandatory) + + + + --package + + The package in which new entities will be placed; default: '__NULL__' + + + + --testAutomatically + + Create automatic integration tests for entities; default if option present: 'true'; default if option not present: 'false' + + + + --enableViews + + Reverse engineer database views; default if option present: 'true'; default if option not present: 'false' + + + + --includeTables + + The tables to include in reverse engineering. Multiple table names must be a double-quoted list separated by spaces + + + + --excludeTables + + The tables to exclude from reverse engineering. Multiple table names must be a double-quoted list separated by spaces + + + + --includeNonPortableAttributes + + Include non-portable JPA @Column attributes such as 'columnDefinition'; default if option present: 'true'; default if option not present: 'false' + + + + --disableVersionFields + + Disable 'version' field; default if option present: 'true'; default if option not present: 'false' + + + + --disableGeneratedIdentifiers + + Disable identifier auto generation; default if option present: 'true'; default if option not present: 'false' + + + + --activeRecord + + Generate CRUD active record methods for each entity; default: 'true' + + + + --repository + + Generate a repository for each entity; default if option present: 'true'; default if option not present: 'false' + + + + --service + + Generate a service for each entity; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + Embedded Commands + Embedded Commands are contained in org.springframework.roo.addon.web.mvc.embedded.EmbeddedCommands. +
    + web mvc embed document + Embed a document for your WEB MVC application + + + + --provider + + The id of the document; default: '__NULL__' (mandatory) + + + + --documentId + + The id of the document; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed generic + Embed media by URL into your WEB MVC application + + + + --url + + The url of the source to be embedded; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed map + Embed a map for your WEB MVC application + + + + --location + + The location of the map (ie "Sydney, Australia"); default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed photos + Embed a photo gallery for your WEB MVC application + + + + --provider + + The provider of the photo gallery; default: '__NULL__' (mandatory) + + + + --userId + + The user id; default: '__NULL__' (mandatory) + + + + --albumId + + The album id; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed stream video + Embed a video stream into your WEB MVC application + + + + --provider + + The provider of the video stream; default: '__NULL__' (mandatory) + + + + --streamId + + The stream id; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed twitter + Embed twitter messages into your WEB MVC application + + + + --searchTerm + + The search term to display results for; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed video + Embed a video for your WEB MVC application + + + + --provider + + The id of the video; default: '__NULL__' (mandatory) + + + + --videoId + + The id of the video; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    + web mvc embed wave + Embed Google wave integration for your WEB MVC application + + + + --waveId + + The key of the wave; default: '__NULL__' (mandatory) + + + + --viewName + + The name of the jspx view; default: '__NULL__' + + + +
    +
    +
    + Equals Commands + Equals Commands are contained in org.springframework.roo.addon.equals.EqualsCommands. +
    + equals + Add equals and hashCode methods to a class + + + + --class + + The name of the class; default if option not present: '*' + + + + --appendSuper + + Whether to call the super class equals and hashCode methods; default if option present: 'true'; default if option not present: 'false' + + + + --excludeFields + + The fields to exclude in the equals and hashcode methods. Multiple field names must be a double-quoted list separated by spaces + + + +
    +
    +
    + Felix Delegator + Felix Delegator are contained in org.springframework.roo.felix.FelixDelegator. +
    + exit + Exits the shell + + This command does not accept any options. +
    +
    + osgi framework command + Passes a command directly through to the Felix shell infrastructure + + + + --[default] + + The command to pass to Felix (WARNING: no validation or security checks are performed); default: 'help' + + + +
    +
    + osgi headers + Display headers for a specific bundle + + + + --bundleSymbolicName + + Limit results to a specific bundle symbolic name; default: '__NULL__' + + + +
    +
    + osgi install + Installs a bundle JAR from a given URL + + + + --url + + The URL to obtain the bundle from; default: '__NULL__' (mandatory) + + + +
    +
    + osgi log + Displays the OSGi log information + + + + --maximumEntries + + The maximum number of log messages to display; default: '__NULL__' + + + + --level + + The minimum level of messages to display; default: '__NULL__' + + + +
    +
    + osgi obr deploy + Deploys a specific OSGi Bundle Repository (OBR) bundle + + + + --bundleSymbolicName + + The specific bundle to deploy; default: '__NULL__' (mandatory) + + + +
    +
    + osgi obr info + Displays information on a specific OSGi Bundle Repository (OBR) bundle + + + + --bundleSymbolicName + + The specific bundle to display information for; default: '__NULL__' (mandatory) + + + +
    +
    + osgi obr list + Lists all available bundles from the OSGi Bundle Repository (OBR) system + + + + --keywords + + Keywords to locate; default: '__NULL__' + + + +
    +
    + osgi obr start + Starts a specific OSGi Bundle Repository (OBR) bundle + + + + --bundleSymbolicName + + The specific bundle to start; default: '__NULL__' (mandatory) + + + +
    +
    + osgi ps + Displays OSGi bundle information + + + + --format + + The format of bundle information; default: 'BUNDLE_NAME' + + + +
    +
    + osgi resolve + Resolves a specific bundle ID + + + + --bundleSymbolicName + + The specific bundle to resolve; default: '__NULL__' (mandatory) + + + +
    +
    + osgi scr config + Lists the current SCR configuration + + This command does not accept any options. +
    +
    + osgi scr disable + Disables a specific SCR-defined component + + + + --componentId + + The specific component identifier (use 'osgi scr list' to list component identifiers); default: '__NULL__' (mandatory) + + + +
    +
    + osgi scr enable + Enables a specific SCR-defined component + + + + --componentId + + The specific component identifier (use 'osgi scr list' to list component identifiers); default: '__NULL__' (mandatory) + + + +
    +
    + osgi scr info + Lists information about a specific SCR-defined component + + + + --componentId + + The specific component identifier (use 'osgi scr list' to list component identifiers); default: '__NULL__' (mandatory) + + + +
    +
    + osgi scr list + Lists all SCR-defined components + + + + --bundleId + + Limit results to a specific bundle; default: '__NULL__' + + + +
    +
    + osgi start + Starts a bundle JAR from a given URL + + + + --url + + The URL to obtain the bundle from; default: '__NULL__' (mandatory) + + + +
    +
    + osgi uninstall + Uninstalls a specific bundle + + + + --bundleSymbolicName + + The specific bundle to uninstall; default: '__NULL__' (mandatory) + + + +
    +
    + osgi update + Updates a specific bundle + + + + --bundleSymbolicName + + The specific bundle to update ; default: '__NULL__' (mandatory) + + + + --url + + The URL to obtain the updated bundle from; default: '__NULL__' + + + +
    +
    +
    + Field Commands + Field Commands are contained in org.springframework.roo.classpath.operations.FieldCommands. +
    + field boolean + Adds a private boolean field to an existing Java source file + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --assertFalse + + Whether this value must assert false; default if option present: 'true'; default if option not present: 'false' + + + + --assertTrue + + Whether this value must assert true; default if option present: 'true'; default if option not present: 'false' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --value + + Inserts an optional Spring @Value annotation with the given content; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --primitive + + Indicates to use a primitive type; default if option present: 'true'; default if option not present: 'false' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field date + Adds a private date field to an existing Java source file + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The Java type of the entity; default: '__NULL__' (mandatory) + + + + --persistenceType + + The type of persistent storage to be used; default: '__NULL__' + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --future + + Whether this value must be in the future; default if option present: 'true'; default if option not present: 'false' + + + + --past + + Whether this value must be in the past; default if option present: 'true'; default if option not present: 'false' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --value + + Inserts an optional Spring @Value annotation with the given content; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + + --dateFormat + + Indicates the style of the date format (ignored if dateTimeFormatPattern is specified); default: 'MEDIUM' + + + + --timeFormat + + Indicates the style of the time format (ignored if dateTimeFormatPattern is specified); default: 'NONE' + + + + --dateTimeFormatPattern + + Indicates a DateTime format pattern such as yyyy-MM-dd hh:mm:ss a; default: '__NULL__' + + + +
    +
    + field embedded + Adds a private @Embedded field to an existing Java source file + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The Java type of the @Embeddable class; default: '__NULL__' (mandatory) + + + + --class + + The name of the @Entity class to receive this field; default if option not present: '*' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field enum + Adds a private enum field to an existing Java source file + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The enum type of this field; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --enumType + + The fetch semantics at a JPA level; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field file + Adds a byte array field for storing uploaded file contents (JSF-scaffolded UIs only) + + + + --fieldName + + The name of the file upload field to add; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --contentType + + The content type of the file; default: '__NULL__' (mandatory) + + + + --autoUpload + + Whether the file is uploaded automatically when selected; default if option present: 'true'; default if option not present: 'false' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field list + Adds a private List field to an existing Java source file (eg the 'one' side of a many-to-one) + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The entity which will be contained within the Set; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --mappedBy + + The field name on the referenced type which owns the relationship; default: '__NULL__' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --sizeMin + + The minimum number of elements in the collection; default: '__NULL__' + + + + --sizeMax + + The maximum number of elements in the collection; default: '__NULL__' + + + + --cardinality + + The relationship cardinality at a JPA level; default: 'MANY_TO_MANY' + + + + --fetch + + The fetch semantics at a JPA level; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field number + Adds a private numeric field to an existing Java source file + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The Java type of the entity; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --decimalMin + + The BigDecimal string-based representation of the minimum value; default: '__NULL__' + + + + --decimalMax + + The BigDecimal string based representation of the maximum value; default: '__NULL__' + + + + --digitsInteger + + Maximum number of integral digits accepted for this number; default: '__NULL__' + + + + --digitsFraction + + Maximum number of fractional digits accepted for this number; default: '__NULL__' + + + + --min + + The minimum value; default: '__NULL__' + + + + --max + + The maximum value; default: '__NULL__' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --value + + Inserts an optional Spring @Value annotation with the given content; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --primitive + + Indicates to use a primitive type if possible; default if option present: 'true'; default if option not present: 'false' + + + + --unique + + Indicates whether to mark the field with a unique constraint; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field other + Inserts a private field into the specified file + + + + --fieldName + + The name of the field; default: '__NULL__' (mandatory) + + + + --type + + The Java type of this field; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --value + + Inserts an optional Spring @Value annotation with the given content; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field reference + Adds a private reference field to an existing Java source file (eg the 'many' side of a many-to-one) + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The Java type of the entity to reference; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --joinColumnName + + The JPA @JoinColumn name; default: '__NULL__' + + + + --referencedColumnName + + The JPA @JoinColumn referencedColumnName; default: '__NULL__' + + + + --cardinality + + The relationship cardinality at a JPA level; default: 'MANY_TO_ONE' + + + + --fetch + + The fetch semantics at a JPA level; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field set + Adds a private Set field to an existing Java source file (eg the 'one' side of a many-to-one) + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --type + + The entity which will be contained within the Set; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --mappedBy + + The field name on the referenced type which owns the relationship; default: '__NULL__' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --sizeMin + + The minimum number of elements in the collection; default: '__NULL__' + + + + --sizeMax + + The maximum number of elements in the collection; default: '__NULL__' + + + + --cardinality + + The relationship cardinality at a JPA level; default: 'MANY_TO_MANY' + + + + --fetch + + The fetch semantics at a JPA level; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + field string + Adds a private string field to an existing Java source file + + + + --fieldName + + The name of the field to add; default: '__NULL__' (mandatory) + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --notNull + + Whether this value cannot be null; default if option present: 'true'; default if option not present: 'false' + + + + --nullRequired + + Whether this value must be null; default if option present: 'true'; default if option not present: 'false' + + + + --decimalMin + + The BigDecimal string-based representation of the minimum value; default: '__NULL__' + + + + --decimalMax + + The BigDecimal string based representation of the maximum value; default: '__NULL__' + + + + --sizeMin + + The minimum string length; default: '__NULL__' + + + + --sizeMax + + The maximum string length; default: '__NULL__' + + + + --regexp + + The required regular expression pattern; default: '__NULL__' + + + + --column + + The JPA @Column name; default: '__NULL__' + + + + --value + + Inserts an optional Spring @Value annotation with the given content; default: '__NULL__' + + + + --comment + + An optional comment for JavaDocs; default: '__NULL__' + + + + --transient + + Indicates to mark the field as transient; default if option present: 'true'; default if option not present: 'false' + + + + --unique + + Indicates whether to mark the field with a unique constraint; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + + --lob + + Indicates that this field is a Large Object; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + Finder Commands + Finder Commands are contained in org.springframework.roo.addon.finder.FinderCommands. +
    + finder add + Install finders in the given target (must be an entity) + + + + --class + + The controller or entity for which the finders are generated; default if option not present: '*' + + + + --finderName + + The finder string as generated with the 'finder list' command; default: '__NULL__' (mandatory) + + + +
    +
    + finder list + List all finders for a given target (must be an entity) + + + + --class + + The controller or entity for which the finders are generated; default if option not present: '*' + + + + --depth + + The depth of attribute combinations to be generated for the finders; default: '1' + + + + --filter + + A comma separated list of strings that must be present in a filter to be included; default: '__NULL__' + + + +
    +
    +
    + Help Commands + Help Commands are contained in org.springframework.roo.felix.help.HelpCommands. +
    + help + Shows system help + + + + --command + + Command name to provide help for; default: '__NULL__' + + + +
    +
    + reference guide + Writes the reference guide XML fragments (in DocBook format) into the current working directory + + This command does not accept any options. +
    +
    +
    + Hint Commands + Hint Commands are contained in org.springframework.roo.classpath.operations.HintCommands. +
    + hint + Provides step-by-step hints and context-sensitive guidance + + + + --topic + + The topic for which advice should be provided + + + +
    +
    +
    + Integration Test Commands + Integration Test Commands are contained in org.springframework.roo.addon.test.IntegrationTestCommands. +
    + test integration + Creates a new integration test for the specified entity + + + + --entity + + The name of the entity to create an integration test for; default if option not present: '*' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + + --transactional + + Indicates whether the created test cases should be run withing a Spring transaction; default: 'true' + + + +
    +
    + test mock + Creates a mock test for the specified entity + + + + --entity + + The name of the entity this mock test is targeting; default if option not present: '*' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + test stub + Creates a test stub for the specified class + + + + --class + + The name of the class this mock test is targeting; default if option not present: '*' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + J Line Shell Component + J Line Shell Component are contained in org.springframework.roo.shell.jline.osgi.JLineShellComponent. +
    + */ + End of block comment + + This command does not accept any options. +
    +
    + /* + Start of block comment + + This command does not accept any options. +
    +
    + // + Inline comment markers (start of line only) + + This command does not accept any options. +
    +
    + date + Displays the local date and time + + This command does not accept any options. +
    +
    + flash test + Tests message flashing + + This command does not accept any options. +
    +
    + script + Parses the specified resource file and executes its commands + + + + --file + + The file to locate and execute; default: '__NULL__' (mandatory) + + + + --lineNumbers + + Display line numbers when executing the script; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + system properties + Shows the shell's properties + + This command does not accept any options. +
    +
    + version + Displays shell version + + + + --[default] + + Special version flags; default: '__NULL__' + + + +
    +
    +
    + Jms Commands + Jms Commands are contained in org.springframework.roo.addon.jms.JmsCommands. +
    + field jms template + Insert a JmsOperations field into an existing type + + + + --fieldName + + The name of the field to add; default: 'jmsOperations' + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --async + + Indicates if the injected method should be executed asynchronously; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + jms listener class + Create an asynchronous JMS consumer + + + + --class + + The name of the class to create; default: '__NULL__' (mandatory) + + + + --destinationName + + The name of the destination; default: 'myDestination' + + + + --destinationType + + The type of the destination; default: 'QUEUE' + + + +
    +
    + jms setup + Install a JMS provider into your project + + + + --provider + + The persistence provider to support; default: '__NULL__' (mandatory) + + + + --destinationName + + The name of the destination; default: 'myDestination' + + + + --destinationType + + The type of the destination; default: 'QUEUE' + + + +
    +
    +
    + Jpa Commands + Jpa Commands are contained in org.springframework.roo.addon.jpa.JpaCommands. +
    + database properties list + Shows database configuration details + + This command does not accept any options. +
    +
    + database properties remove + Removes a particular database property + + + + --key + + The property key that should be removed; default: '__NULL__' (mandatory) + + + +
    +
    + database properties set + Changes a particular database property + + + + --key + + The property key that should be changed; default: '__NULL__' (mandatory) + + + + --value + + The new vale for this property key; default: '__NULL__' (mandatory) + + + +
    +
    + embeddable + Creates a new Java class source file with the JPA @Embeddable annotation in SRC_MAIN_JAVA + + + + --class + + The name of the class to create; default: '__NULL__' (mandatory) + + + + --serializable + + Whether the generated class should implement java.io.Serializable; default if option present: 'true'; default if option not present: 'false' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + entity jpa + Creates a new JPA persistent entity in SRC_MAIN_JAVA + + + + --class + + Name of the entity to create; default: '__NULL__' (mandatory) + + + + --extends + + The superclass (defaults to java.lang.Object); default if option not present: 'java.lang.Object' + + + + --implements + + The interface to implement; default: '__NULL__' + + + + --abstract + + Whether the generated class should be marked as abstract; default if option present: 'true'; default if option not present: 'false' + + + + --testAutomatically + + Create automatic integration tests for this entity; default if option present: 'true'; default if option not present: 'false' + + + + --table + + The JPA table name to use for this entity; default: '__NULL__' + + + + --schema + + The JPA table schema name to use for this entity; default: '__NULL__' + + + + --catalog + + The JPA table catalog name to use for this entity; default: '__NULL__' + + + + --identifierField + + The JPA identifier field name to use for this entity; default: '__NULL__' + + + + --identifierColumn + + The JPA identifier field column to use for this entity; default: '__NULL__' + + + + --identifierType + + The data type that will be used for the JPA identifier field (defaults to java.lang.Long); default: 'java.lang.Long' + + + + --versionField + + The JPA version field name to use for this entity; default: '__NULL__' + + + + --versionColumn + + The JPA version field column to use for this entity; default: '__NULL__' + + + + --versionType + + The data type that will be used for the JPA version field (defaults to java.lang.Integer); default if option not present: 'java.lang.Integer' + + + + --inheritanceType + + The JPA @Inheritance value (apply to base class); default: '__NULL__' + + + + --mappedSuperclass + + Apply @MappedSuperclass for this entity; default if option present: 'true'; default if option not present: 'false' + + + + --equals + + Whether the generated class should implement equals and hashCode methods; default if option present: 'true'; default if option not present: 'false' + + + + --serializable + + Whether the generated class should implement java.io.Serializable; default if option present: 'true'; default if option not present: 'false' + + + + --persistenceUnit + + The persistence unit name to be used in the persistence.xml file; default: '__NULL__' + + + + --transactionManager + + The transaction manager name; default: '__NULL__' + + + + --permitReservedWords + + Indicates whether reserved words are ignored by Roo; default if option present: 'true'; default if option not present: 'false' + + + + --entityName + + The name used to refer to the entity in queries; default: '__NULL__' + + + + --sequenceName + + The name of the sequence for incrementing sequence-driven primary keys; default: '__NULL__' + + + + --activeRecord + + Generate CRUD active record methods for this entity; default: 'true' + + + +
    +
    + jpa setup + Install or updates a JPA persistence provider in your project + + + + --provider + + The persistence provider to support; default: '__NULL__' (mandatory) + + + + --database + + The database to support; default: '__NULL__' (mandatory) + + + + --applicationId + + The Google App Engine application identifier to use; default if option not present: 'the project's name' + + + + --jndiDataSource + + The JNDI datasource to use; default: '__NULL__' + + + + --hostName + + The host name to use; default: '__NULL__' + + + + --databaseName + + The database name to use; default: '__NULL__' + + + + --userName + + The username to use; default: '__NULL__' + + + + --password + + The password to use; default: '__NULL__' + + + + --transactionManager + + The transaction manager name; default: '__NULL__' + + + + --persistenceUnit + + The persistence unit name to be used in the persistence.xml file; default: '__NULL__' + + + +
    +
    + persistence setup + Install or updates a JPA persistence provider in your project - deprecated, use 'jpa setup' instead + + + + --provider + + The persistence provider to support; default: '__NULL__' (mandatory) + + + + --database + + The database to support; default: '__NULL__' (mandatory) + + + + --applicationId + + The Google App Engine application identifier to use; default if option not present: 'the project's name' + + + + --jndiDataSource + + The JNDI datasource to use; default: '__NULL__' + + + + --hostName + + The host name to use; default: '__NULL__' + + + + --databaseName + + The database name to use; default: '__NULL__' + + + + --userName + + The username to use; default: '__NULL__' + + + + --password + + The password to use; default: '__NULL__' + + + + --transactionManager + + The transaction manager name; default: '__NULL__' + + + + --persistenceUnit + + The persistence unit name to be used in the persistence.xml file; default: '__NULL__' + + + +
    +
    +
    + Jsf Commands + Jsf Commands are contained in org.springframework.roo.addon.jsf.JsfCommands. +
    + web jsf all + Create JSF managed beans for all entities + + + + --package + + The package in which new JSF managed beans will be placed; default: '__NULL__' (mandatory) + + + +
    +
    + web jsf media + Add a cross-browser generic player to embed multimedia content + + + + --url + + The url of the media source; default: '__NULL__' (mandatory) + + + + --player + + The name of the media player; default: '__NULL__' + + + +
    +
    + web jsf scaffold + Create JSF managed bean for an entity + + + + --class + + The path and name of the JSF managed bean to be created; default: '__NULL__' (mandatory) + + + + --entity + + The entity which this JSF managed bean class will create and modify as required; default if option not present: '*' + + + + --beanName + + The name of the managed bean to use in the 'name' attribute of the @ManagedBean annotation; default: '__NULL__' + + + + --includeOnMenu + + Include this entity on the generated JSF menu; default: 'true' + + + +
    +
    + web jsf setup + Set up JSF environment + + + + --implementation + + The JSF implementation to use; default: '__NULL__' + + + + --library + + The JSF component library to use; default: '__NULL__' + + + + --theme + + The name of the theme; default: '__NULL__' + + + +
    +
    +
    + Json Commands + Json Commands are contained in org.springframework.roo.addon.json.JsonCommands. +
    + json add + Adds @RooJson annotation to target type + + + + --class + + The java type to apply this annotation to; default if option not present: '*' + + + + --rootName + + The root name which should be used to wrap the JSON document; default: '__NULL__' + + + + --deepSerialize + + Indication if deep serialization should be enabled.; default if option present: 'true'; default if option not present: 'false' + + + + --iso8601Dates + + Indication if dates should be formatted according to ISO 8601; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + json all + Adds @RooJson annotation to all types annotated with @RooJavaBean + + + + --deepSerialize + + Indication if deep serialization should be enabled; default if option present: 'true'; default if option not present: 'false' + + + + --iso8601Dates + + Indication if dates should be formatted according to ISO 8601; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + Jsp Commands + Jsp Commands are contained in org.springframework.roo.addon.web.mvc.jsp.JspCommands. +
    + controller class + Create a new manual Controller (ie where you write the methods) - deprecated, use 'web mvc controller' instead + + + + --class + + The path and name of the controller object to be created; default: '__NULL__' (mandatory) + + + + --preferredMapping + + Indicates a specific request mapping path for this controller (eg /foo/); default: '__NULL__' + + + +
    +
    + web mvc controller + Create a new manual Controller (ie where you write the methods) + + + + --class + + The path and name of the controller object to be created; default: '__NULL__' (mandatory) + + + + --preferredMapping + + Indicates a specific request mapping path for this controller (eg /foo/); default: '__NULL__' + + + +
    +
    + web mvc install language + Install new internationalization bundle for MVC scaffolded UI. + + + + --code + + The language code for the desired bundle; default: '__NULL__' (mandatory) + + + +
    +
    + web mvc install view + Create a new static view. + + + + --path + + The path the static view to create in (required, ie '/foo/blah'); default: '__NULL__' (mandatory) + + + + --viewName + + The view name the mapping this view should adopt (required, ie 'index'); default: '__NULL__' (mandatory) + + + + --title + + The title of the view; default: '__NULL__' (mandatory) + + + +
    +
    + web mvc language + Install new internationalization bundle for MVC scaffolded UI. + + + + --code + + The language code for the desired bundle; default: '__NULL__' (mandatory) + + + +
    +
    + web mvc setup + Setup a basic project structure for a Spring MVC / JSP application + + This command does not accept any options. +
    +
    + web mvc update tags + Replace an existing application tagx library with the latest version (use --backup option to backup your application first) + + + + --backup + + Backup your application before replacing your existing tag library; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + web mvc view + Create a new static view. + + + + --path + + The path the static view to create in (required, ie '/foo/blah'); default: '__NULL__' (mandatory) + + + + --viewName + + The view name the mapping this view should adopt (required, ie 'index'); default: '__NULL__' (mandatory) + + + + --title + + The title of the view; default: '__NULL__' (mandatory) + + + +
    +
    +
    + Logging Commands + Logging Commands are contained in org.springframework.roo.addon.logging.LoggingCommands. +
    + logging setup + Configure logging in your project + + + + --level + + The log level to configure; default: '__NULL__' (mandatory) + + + + --package + + The package to append the logging level to (all by default); default: '__NULL__' + + + +
    +
    +
    + Mail Commands + Mail Commands are contained in org.springframework.roo.addon.email.MailCommands. +
    + email sender setup + Install a Spring JavaMailSender in your project + + + + --hostServer + + The host server; default: '__NULL__' (mandatory) + + + + --protocol + + The protocol used by mail server; default: '__NULL__' + + + + --port + + The port used by mail server; default: '__NULL__' + + + + --encoding + + The encoding used for mail; default: '__NULL__' + + + + --username + + The mail account username; default: '__NULL__' + + + + --password + + The mail account password; default: '__NULL__' + + + +
    +
    + email template setup + Configures a template for a SimpleMailMessage + + + + --from + + The 'from' email (optional); default: '__NULL__' + + + + --subject + + The message subject (obtional); default: '__NULL__' + + + +
    +
    + field email template + Inserts a MailTemplate field into an existing type + + + + --fieldName + + The name of the field to add; default: 'mailTemplate' + + + + --class + + The name of the class to receive this field; default if option not present: '*' + + + + --async + + Indicates if the injected method should be executed asynchronously; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    +
    + Maven Commands + Maven Commands are contained in org.springframework.roo.project.MavenCommands. +
    + dependency add + Adds a new dependency to the Maven project object model (POM) + + + + --groupId + + The group ID of the dependency; default: '__NULL__' (mandatory) + + + + --artifactId + + The artifact ID of the dependency; default: '__NULL__' (mandatory) + + + + --version + + The version of the dependency; default: '__NULL__' (mandatory) + + + + --classifier + + The classifier of the dependency; default: '__NULL__' + + + + --scope + + The scope of the dependency; default: '__NULL__' + + + +
    +
    + dependency remove + Removes an existing dependency from the Maven project object model (POM) + + + + --groupId + + The group ID of the dependency; default: '__NULL__' (mandatory) + + + + --artifactId + + The artifact ID of the dependency; default: '__NULL__' (mandatory) + + + + --version + + The version of the dependency; default: '__NULL__' (mandatory) + + + + --classifier + + The classifier of the dependency; default: '__NULL__' + + + +
    +
    + maven repository add + Adds a new repository to the Maven project object model (POM) + + + + --id + + The ID of the repository; default: '__NULL__' (mandatory) + + + + --name + + The name of the repository; default: '__NULL__' + + + + --url + + The URL of the repository; default: '__NULL__' (mandatory) + + + +
    +
    + maven repository remove + Removes an existing repository from the Maven project object model (POM) + + + + --id + + The ID of the repository; default: '__NULL__' (mandatory) + + + + --url + + The URL of the repository; default: '__NULL__' (mandatory) + + + +
    +
    + module create + Creates a new Maven module + + + + --moduleName + + The name of the module; default: '__NULL__' (mandatory) + + + + --topLevelPackage + + The uppermost package name (this becomes the <groupId> in Maven and also the '~' value when using Roo's shell); default: '__NULL__' (mandatory) + + + + --java + + Forces a particular major version of Java to be used (will be auto-detected if unspecified; specify 6 or 7 only); default: '__NULL__' + + + + --parent + + The Maven coordinates of the parent POM, in the form "groupId:artifactId:version"; default: '__NULL__' + + + + --packaging + + The Maven packaging of this module; default if option not present: 'jar' + + + + --artifactId + + The artifact ID of this module (defaults to moduleName if not specified); default: '__NULL__' + + + +
    +
    + module focus + Changes focus to a different project module + + + + --moduleName + + The module to focus on; default: '__NULL__' (mandatory) + + + +
    +
    + perform assembly + Executes the assembly goal via Maven + + This command does not accept any options. +
    +
    + perform clean + Executes a full clean (including Eclipse files) via Maven + + This command does not accept any options. +
    +
    + perform command + Executes a user-specified Maven command + + + + --mavenCommand + + User-specified Maven command (eg test:test); default: '__NULL__' (mandatory) + + + +
    +
    + perform eclipse + Sets up Eclipse configuration via Maven (only necessary if you have not installed the m2eclipse plugin in Eclipse) + + This command does not accept any options. +
    +
    + perform package + Packages the application using Maven, but does not execute any tests + + This command does not accept any options. +
    +
    + perform tests + Executes the tests via Maven + + This command does not accept any options. +
    +
    + project + Creates a new Maven project + + + + --topLevelPackage + + The uppermost package name (this becomes the <groupId> in Maven and also the '~' value when using Roo's shell); default: '__NULL__' (mandatory) + + + + --projectName + + The name of the project (last segment of package name used as default); default: '__NULL__' + + + + --java + + Forces a particular major version of Java to be used (will be auto-detected if unspecified; specify 5 or 6 or 7 only); default: '__NULL__' + + + + --parent + + The Maven coordinates of the parent POM, in the form "groupId:artifactId:version"; default: '__NULL__' + + + + --packaging + + The Maven packaging of this project; default if option not present: 'jar' + + + +
    +
    +
    + Metadata Commands + Metadata Commands are contained in org.springframework.roo.classpath.MetadataCommands. +
    + metadata cache + Shows detailed metadata for the indicated type + + + + --maximumCapacity + + The maximum number of metadata items to cache; default: '__NULL__' (mandatory) + + + +
    +
    + metadata for id + Shows detailed information about the metadata item + + + + --metadataId + + The metadata ID (should start with MID:); default: '__NULL__' (mandatory) + + + +
    +
    + metadata for module + Shows the ProjectMetadata for the indicated project module + + + + --module + + The module for which to retrieve the metadata (defaults to the focused module); default: '__NULL__' + + + +
    +
    + metadata for type + Shows detailed metadata for the indicated type + + + + --type + + The Java type for which to display metadata; default: '__NULL__' (mandatory) + + + +
    +
    + metadata status + Shows metadata statistics + + This command does not accept any options. +
    +
    + metadata trace + Traces metadata event delivery notifications + + + + --level + + The verbosity of notifications (0=none, 1=some, 2=all); default: '__NULL__' (mandatory) + + + +
    +
    +
    + Mongo Commands + Mongo Commands are contained in org.springframework.roo.addon.layers.repository.mongo.MongoCommands. +
    + entity mongo + Creates a domain entity which can be backed by a MongoDB repository + + + + --class + + Implementation class for the specified interface; default: '__NULL__' (mandatory) + + + + --identifierType + + The ID type to be used for this domain type (defaults to BigInteger); default: '__NULL__' + + + + --testAutomatically + + Create automatic integration tests for this entity; default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + mongo setup + Configures the project for MongoDB peristence. + + + + --username + + Username for accessing the database (defaults to ''); default: '__NULL__' + + + + --password + + Password for accessing the database (defaults to ''); default: '__NULL__' + + + + --databaseName + + Name of the database (defaults to project name); default: '__NULL__' + + + + --port + + Port for the database (defaults to '27017'); default: '__NULL__' + + + + --host + + Host for the database (defaults to '127.0.0.1'); default: '__NULL__' + + + + --cloudFoundry + + Deploy to CloudFoundry (defaults to 'false'); default if option present: 'true'; default if option not present: 'false' + + + +
    +
    + repository mongo + Adds @RooMongoRepository annotation to target type + + + + --interface + + The java interface to apply this annotation to; default: '__NULL__' (mandatory) + + + + --entity + + The domain entity this repository should expose; default if option not present: '*' + + + +
    +
    +
    + Os Commands + Os Commands are contained in org.springframework.roo.addon.oscommands.OsCommands. +
    + ! + Allows execution of operating system (OS) commands. + + + + --command + + The command to execute; default: '' + + + +
    +
    +
    + Pgp Commands + Pgp Commands are contained in org.springframework.roo.felix.pgp.PgpCommands. +
    + pgp automatic trust + Indicates to automatically trust all keys encountered until the command is invoked again + + This command does not accept any options. +
    +
    + pgp key view + Downloads a remote key and displays it to the user (does not change any trusts) + + + + --keyId + + The key ID to view (eg 00B5050F or 0x00B5050F); default: '__NULL__' (mandatory) + + + +
    +
    + pgp list trusted keys + Lists the keys you currently trust and have not been revoked at the time last downloaded from a public key server + + This command does not accept any options. +
    +
    + pgp refresh all + Refreshes all keys from public key servers + + This command does not accept any options. +
    +
    + pgp status + Displays the status of the PGP environment + + This command does not accept any options. +
    +
    + pgp trust + Grants trust to a particular key ID + + + + --keyId + + The key ID to trust (eg 00B5050F or 0x00B5050F); default: '__NULL__' (mandatory) + + + +
    +
    + pgp untrust + Revokes your trust for a particular key ID + + + + --keyId + + The key ID to remove trust from (eg 00B5050F or 0x00B5050F); default: '__NULL__' (mandatory) + + + +
    +
    +
    + Process Manager Commands + Process Manager Commands are contained in org.springframework.roo.process.manager.ProcessManagerCommands. +
    + development mode + Switches the system into development mode (greater diagnostic information) + + + + --enabled + + Activates development mode; default: 'true' + + + +
    +
    + poll now + Perform a manual file system poll + + This command does not accept any options. +
    +
    + poll speed + Changes the file system polling speed + + + + --ms + + The number of milliseconds between each poll; default: '__NULL__' (mandatory) + + + +
    +
    + poll status + Display file system polling information + + This command does not accept any options. +
    +
    +
    + Process Manager Diagnostics Listener + Process Manager Diagnostics Listener are contained in org.springframework.roo.process.manager.internal.ProcessManagerDiagnosticsListener. +
    + process manager debug + Indicates if process manager debugging is desired + + + + --enabled + + Activates debug mode; default: 'true' + + + +
    +
    +
    + Prop File Commands + Prop File Commands are contained in org.springframework.roo.addon.propfiles.PropFileCommands. +
    + properties list + Shows the details of a particular properties file + + + + --name + + Property file name (including .properties suffix); default: '__NULL__' (mandatory) + + + + --path + + Source path to property file; default: '__NULL__' (mandatory) + + + +
    +
    + properties remove + Removes a particular properties file property + + + + --name + + Property file name (including .properties suffix); default: '__NULL__' (mandatory) + + + + --path + + Source path to property file; default: '__NULL__' (mandatory) + + + + --key + + The property key that should be removed; default: '__NULL__' (mandatory) + + + +
    +
    + properties set + Changes a particular properties file property + + + + --name + + Property file name (including .properties suffix); default: '__NULL__' (mandatory) + + + + --path + + Source path to property file; default: '__NULL__' (mandatory) + + + + --key + + The property key that should be changed; default: '__NULL__' (mandatory) + + + + --value + + The new vale for this property key; default: '__NULL__' (mandatory) + + + +
    +
    +
    + Proxy Configuration Commands + Proxy Configuration Commands are contained in org.springframework.roo.url.stream.jdk.ProxyConfigurationCommands. +
    + proxy configuration + Shows the proxy server configuration + + This command does not accept any options. +
    +
    +
    + Repository Jpa Commands + Repository Jpa Commands are contained in org.springframework.roo.addon.layers.repository.jpa.RepositoryJpaCommands. +
    + repository jpa + Adds @RooJpaRepository annotation to target type + + + + --interface + + The java interface to apply this annotation to; default: '__NULL__' (mandatory) + + + + --entity + + The domain entity this repository should expose; default if option not present: '*' + + + +
    +
    +
    + Security Commands + Security Commands are contained in org.springframework.roo.addon.security.SecurityCommands. +
    + permissionEvaluator + Create a permission evaluator + + + + --package + + The package to add the permission evaluator to; default: '__NULL__' (mandatory) + + + +
    +
    + security setup + Install Spring Security into your project + + This command does not accept any options. +
    +
    +
    + Selenium Commands + Selenium Commands are contained in org.springframework.roo.addon.web.selenium.SeleniumCommands. +
    + selenium test + Creates a new Selenium test for a particular controller + + + + --controller + + Controller to create a Selenium test for; default: '__NULL__' (mandatory) + + + + --name + + Name of the test; default: '__NULL__' + + + + --serverUrl + + URL of the server where the web application is available, including protocol, port and hostname; default: 'http://localhost:8080/' + + + +
    +
    +
    + Service Commands + Service Commands are contained in org.springframework.roo.addon.layers.service.ServiceCommands. +
    + service all + Adds @RooService annotation to all entities + + + + --interfacePackage + + The java interface package; default: '__NULL__' (mandatory) + + + + --classPackage + + The java package of the implementation classes for the interfaces; default: '__NULL__' + + + + --useXmlConfiguration + + When true, Spring Roo will configure services using XML. This is the default behavior for services using GAE; default: '__NULL__' + + + +
    +
    + service secure all + Adds @RooService annotation to all entities with options for authentication, authorization, and a permission evaluator + + + + --interfacePackage + + The java interface package; default: '__NULL__' (mandatory) + + + + --classPackage + + The java package of the implementation classes for the interfaces; default: '__NULL__' + + + + --requireAuthentication + + Whether or not users must be authenticated to use the service; default if option present: 'true'; default if option not present: 'false' + + + + --authorizedRole + + The role authorized the use the methods in the service (additional roles can be added after creation); default: '__NULL__' + + + + --usePermissionEvaluator + + Whether or not to use a PermissionEvaluator; default if option present: 'true'; default if option not present: 'false' + + + + --useXmlConfiguration + + When true, Spring Roo will configure services using XML.; default: '__NULL__' + + + +
    +
    + service secure type + Adds @RooService annotation to target type with options for authentication, authorization, and a permission evaluator + + + + --interface + + The java interface to apply this annotation to; default: '__NULL__' (mandatory) + + + + --class + + Implementation class for the specified interface; default: '__NULL__' + + + + --entity + + The domain entity this service should expose; default if option not present: '*' + + + + --requireAuthentication + + Whether or not users must be authenticated to use the service; default if option present: 'ture'; default if option not present: 'false' + + + + --authorizedRoles + + The role authorized the use the methods in the service; default: '__NULL__' + + + + --usePermissionEvaluator + + Whether or not to use a PermissionEvaluator; default if option present: 'true'; default if option not present: 'false' + + + + --useXmlConfiguration + + When true, Spring Roo will configure services using XML.; default: '__NULL__' + + + +
    +
    + service type + Adds @RooService annotation to target type + + + + --interface + + The java interface to apply this annotation to; default: '__NULL__' (mandatory) + + + + --class + + Implementation class for the specified interface; default: '__NULL__' + + + + --entity + + The domain entity this service should expose; default if option not present: '*' + + + + --useXmlConfiguration + + When true, Spring Roo will configure services using XML.; default: '__NULL__' + + + +
    +
    +
    + Solr Commands + Solr Commands are contained in org.springframework.roo.addon.solr.SolrCommands. +
    + solr add + Make target type searchable + + + + --class + + The type to be made searchable; default if option not present: '*' + + + +
    +
    + solr all + Make all eligible project types searchable + + This command does not accept any options. +
    +
    + solr setup + Install support for Solr search integration + + + + --searchServerUrl + + The URL of the Solr search server; default: 'http://localhost:8983/solr' + + + +
    +
    +
    + Tailor Commands + Tailor Commands are contained in org.springframework.roo.addon.tailor.TailorCommands. +
    + tailor activate + Activate a tailor configuration. + + + + --name + + The name of the tailor configuration; default: '__NULL__' (mandatory) + + + +
    +
    + tailor deactivate + Deactivate the tailor. + + This command does not accept any options. +
    +
    + tailor list + List available tailor configurations. + + This command does not accept any options. +
    +
    +
    + Uaa Commands + Uaa Commands are contained in org.springframework.roo.uaa.UaaCommands. +
    + download accept terms of use + Accepts the Spring User Agent Analysis (UAA) Terms of Use + + This command does not accept any options. +
    +
    + download privacy level + Changes the Spring User Agent Analysis (UAA) privacy level + + + + --privacyLevel + + The new UAA privacy level to use; default: '__NULL__' (mandatory) + + + +
    +
    + download reject terms of use + Rejects the Spring User Agent Analysis (UAA) Terms of Use + + This command does not accept any options. +
    +
    + download status + Provides a summary of the Spring User Agent Analysis (UAA) status and commands + + This command does not accept any options. +
    +
    + download view + Displays the Spring User Agent Analysis (UAA) header content in plain text + + + + --file + + The file to save the UAA JSON content to; default: '__NULL__' + + + +
    +
    +
    + Web Finder Commands + Web Finder Commands are contained in org.springframework.roo.addon.web.mvc.controller.finder.WebFinderCommands. +
    + web mvc finder add + Adds @RooWebFinder annotation to MVC controller type + + + + --formBackingType + + The finder-enabled type; default: '__NULL__' (mandatory) + + + + --class + + The controller java type to apply this annotation to; default if option not present: '*' + + + +
    +
    + web mvc finder all + Adds @RooWebFinder annotation to existing MVC controllers + + This command does not accept any options. +
    +
    +
    + Web Flow Commands + Web Flow Commands are contained in org.springframework.roo.addon.web.flow.WebFlowCommands. +
    + web flow + Install Spring Web Flow configuration artifacts into your project + + + + --flowName + + The name for your web flow; default: '__NULL__' + + + +
    +
    +
    + Web Json Commands + Web Json Commands are contained in org.springframework.roo.addon.web.mvc.controller.json.WebJsonCommands. +
    + web mvc json add + Adds @RooJson annotation to target type + + + + --jsonObject + + The JSON-enabled object which backs this Spring MVC controller.; default: '__NULL__' (mandatory) + + + + --class + + The java type to apply this annotation to; default if option not present: '*' + + + +
    +
    + web mvc json all + Adds or creates MVC controllers annotated with @RooWebJson annotation + + + + --package + + The package in which new controllers will be placed; default: '__NULL__' + + + +
    +
    + web mvc json setup + Set up Spring MVC to support JSON + + This command does not accept any options. +
    +
    +
    diff --git a/deployment-support/src/site/docbook/reference/appendix-resources.xml b/deployment-support/src/site/docbook/reference/appendix-resources.xml new file mode 100644 index 000000000..837f0e191 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/appendix-resources.xml @@ -0,0 +1,236 @@ + + + Roo Resources + + As an open source project, Spring Roo offers a large number of + resources to assist the community learn, interact with one another and + become more involved in the project. Below you'll find a short summary of + the official project resources. + +
    + Project Home Page + + Web: http://projects.spring.io/spring-roo/ + + The project home page provides a brief summary of Roo's main + features and links to most of the other project resources. Please use this + URI if you are referring other people to the Spring Roo project, as it is + the main landing point for the project. + + From the main Roo web site you'll also find links to our "resources + index". The resources index provides convenient, up-to-date links to all + of the services shown below, as well as third-party add-ons you are able + to install. +
    + +
    + Downloads and Maven Repositories + + Web: http://www.springsource.com/download/community?project=Spring%20Roo + + You can always access the latest Spring Roo release ZIP by visiting + the above URI. The download site not only provides the download itself, + but also provides access to all historically released versions plus SHA1 + hash codes of those files. + + We publish all Roo modules to a Maven repository at http://spring-roo-repository.springsource.org/release. + This Maven repository is automatically included in user project so that + the annotation library can be downloaded. It is also automatically + included in the POM for add-ons created via the add-on creator. +
    + +
    + Community Forums + + Web: http://forum.springsource.org/forumdisplay.php?f=67 + + For fast and free end user support for all official Spring projects, + the Spring Community Forum is an excellent place to visit. Because Roo is + an official top-level Spring project, of course you'll find there is a + dedicated "Spring Roo forum" for all your questions, comments and + experiences. + + The Roo project does not have a "mailing list" or "newsgroup" as you + might be familiar with from other open source projects, although commercial support options are + available. + + Extensive search facilities are provided on the community forums, + and the Roo developers routinely answer user questions. One excellent way + of contributing to the Roo project is to simply keep an eye on the forum + messages and help other people. Even recommendations along the lines of, + "I don't know how to do what you're trying to do, but we usually tackle + the problem this way instead...." are very helpful to other community + members. + + When you ask a question on the forum, it's highly recommended you + include a small Roo sample script that + can be used to reproduce your problem. If that's infeasible, using Roo's + "backup" command is another + alternative and you can attach the resulting ZIP file to your post. Other + tips include always specifying the version of Roo that you're running (as + can be obtained from the "version" command), and if you're + having trouble with IDE integration, the exact version of the IDE you are + using (and, if an Eclipse-based IDE, the version of AspectJ Development Tools in use). Another good + source of advice on how to ask questions on the forum can be found in Eric + Raymond's often-cited essay, "How to Ask + Smart Questions". + + If you believe you have found a bug or are experiencing an issue, it + is recommended you first log a message on the forum. This allows other + experienced users to comment on whether it appears there is a problem with + Roo or perhaps just needs to be used a different way. Someone will usually + offer a solution or recommend you log a bug report (usually by saying + "please log this in Jira"). When you do log a bug report, please ensure + you link to the fully-qualified URI to the forum post. That way the + developer who attempts to solve your bug will have background information. + Please also post the issue tracking link back in thread you started on the + forum, as it will help other people cross-reference the two + systems. +
    + +
    + Twitter + + Roo Hash Code (please include in your tweets, and also follow for + low-volume announcements): @SpringRoo + + Follow the core Roo development team for interesting Roo news and + progress (higher volume than just following @SpringRoo, but only a few + Tweets per week): @alankstewart. + + Many people who use Roo also use Twitter, including the core Roo + development team. If you're a Twitter user, you're welcome to follow the + Roo development team (using the Twitter IDs above) to receive + up-to-the-minute Tweets on Roo activities, usage and events. + + The Roo team also monitors Tweets that include @SpringRoo, so if + you're Tweeting about Roo, please remember to include @SpringRoo somewhere + in the Tweet. If you like Roo or have found it helpful on a project, + please Tweet about it and help spread the word! + + We do request that you use the Community Forums if you have a question + or issue with Roo, as 140 characters doesn't allow us to provide in-depth + technical support or provide a growing archive of historical answers that + people can search against. +
    + +
    + Issue Tracking + + Web: https://jira.springsource.org/browse/ROO + + Spring projects use Atlassian Jira for tracking bugs, improvements, + feature requests and tasks. Roo uses a public Jira instance you're welcome + to use in order to log issues, watch existing issues, vote for existing + issues and review the changes made between particular versions. + + As discussed in the Community + Forums section, we ask that you refrain from logging bug reports + until you've first discussed them on the forum. This allows others to + comment on whether a bug actually exists. When logging an issue in Jira, + there is a field explicitly provided so you can link the forum discussion + to the Jira issue. + + Please note that every commit into the Roo source repository will be + prefixed with a particular Jira issue number. All Jira issue numbers for + the Roo project commence with "ROO-", providing you an easy way to + determine the rationale of any change. + + Because open source projects receive numerous enhancement requests, + we generally prioritise enhancements that have patches included, are quick + to complete or those which have received a large number of votes. You can + vote for a particular issue by logging into Jira (it's fast, easy and free + to create an account) and click the "vote" link against any issue. + Similarly you can monitor the progress on any issue you're interested in + by clicking "watch". + + Enhancement requests are easier to complete (and therefore more + probable to be actioned) if they represent fine-grained units of work that + include as much detail as possible. Enhancement requests should describe a + specific use case or user story that is trying to be achieved. It is + usually helpful to provide a Roo sample + script that can be used to explain the issue. You should also + consider whether a particular enhancement is likely to appeal to most Roo + users, and if not, whether perhaps writing it as an add-on would be a good alternative. +
    + +
    + Source Repository + + Read repository: https://github.com/spring-projects/spring-roo.git + + The Git source control system is currently used by Roo for mainline + development. + + Historical releases of Roo can be accessed by browsing the tags + branches within our Git repository. The mainline development of Roo occurs + on the "master" branch. + + To detailed information about how to check out and build Roo from + Subversion, please refer to the Development + Processes chapter. +
    + +
    + Source Web Browsing + + Web: https://github.com/spring-projects/spring-roo.git + + To assist those who wish to simply review the current Roo code but + not check it out fully onto their own computer, Spring Roo offers a public + Atlassian FishEye instance. You can use this to not only view the current + source code, but also access old releases, perform sophisticated searches + and even build graphs and reports. + + If you need to link to source code from an issue report or forum + post, please use the FishEye service to provide a fully-qualified + URI. +
    + +
    + Commercial Products and Services + + Web: http://spring.io/ + + Pivitol Software employs the Roo + development team and offers a wide range of products and professional + services around Roo and the technologies which Roo enables. Available + professional services include training, consulting, design reviews and + mentoring, with products including service level agreement (SLA) backed + support subscriptions, certified builds, indemnification and integration + with various commercial products. Please visit the above URI to learn more + about SpringSource products and services and how these can add value to + your build-run-manage application lifecycle. +
    + +
    + Other + + Please let us know if you believe it would be helpful to list any + other resources in this documentation. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/appendix-upgrade.xml b/deployment-support/src/site/docbook/reference/appendix-upgrade.xml new file mode 100644 index 000000000..bde0dd643 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/appendix-upgrade.xml @@ -0,0 +1,768 @@ + + + Upgrade Notes and Known Issues + +
    + Known Issues + + Because Spring Roo integrates a large number of other technologies, + invariably some people using Roo may experience issues when using certain + combinations of technologies together. This section aims to list such + known issues in an effort to help you avoid experiencing any problems. If + you are able to contribute further information, a solution or workaround + to any of these known issues, we'd certainly appreciate hearing from you + via the community forums. + + + + JDK compatibility: Spring Roo has been + tested with Sun, IBM, JRockit and Apache Harmony JVMs for Java 5 and + Java 6. We do not formally support other JVMs or other versions of + JVMs. We have also had an issue + reported with versions of Java 6 before 1.6.0_17 due to Java bug 6506304 + and therefore recommend you always use the latest released version of + Java 6 for your platform. There is also a known issue with OpenJDK. + You can read about our testing of different JDKs in issue ROO-106. + + + + Human language support: Pluralisation + within Roo delegates to the Inflector library. + Due to some issues with Inflector, only English pluralisation is + supported. If you wish to override the plural selected by Inflector + (and in turn used by Roo), you can specify a particular plural for + either a Java type or Java field by using the @RooPlural + annotation. Longer term it would be nice if someone ported the + Inflector code into the Roo pluralisation add-on so that we can fix + these issues and support other languages. We are receptive to + contributions from the community along these lines. + + + + Shell wrapping: In certain cases typing a + long command into the shell that wraps over a single line may prevent + you from being able to backspace to the prior line. This is caused by + the JLine library (not Roo). We expect to rewrite the shell at some + future time and will likely stop using JLine at that point. + + + + Hibernate issues: Hibernate is one of the + JPA providers we test with, however, Hibernate has issues with + --mappedSuperclass as detailed in ROO-292 + and ROO-747. + We recommend you do not use --mappedSuperclass in + combination with Hibernate. We have found OpenJPA works reliably in + all cases, so you might want to consider switching to OpenJPA if you + are seriously impacted by this issue (the "jpa setup" command can be + used multiple times, which is useful for experimentally switching + between different JPA providers). + + + + Integration testing limitations: The data + on demand mechanism (which is used for integration tests) has limited + JSR 303 (Bean Validator) compatibility. Roo supports fields using + @NotNull, @Past and @Future, @Size, @Min, and @Max. No other validator + annotations are formally supported, although many will work. To use + other validator annotations, you may need to edit your + DataOnDemand.java file and add a manual + getNewTransientEntity(int) method. Refer to a generated + *_Roo_DataOnDemand.aj file for an example. Alternately, + do not use the integration test functionality in Roo unless you have + relatively simple validation constraints or you are willing to provide + this data on demand method. + + + + Tomcat 5.5: Tomcat 5.5 can not be supported + by the scaffolded Spring MVC Web UI. Tomcat 5.5 does not support the + JSP 2.1 API. Roo makes extensive use of the JSP 2.1 API in the + scaffolded Web UI (specifically expression language features). + Furthermore, the JSP 2.0 API does not support JDK 5 enums (a feature + that Roo would need). See ROO-680 + for more details. The following + forum post offers a workaround for the JSP 2.1 incompatibility + issue. Please be aware that this has not been tested by the Roo team + and Tomcat 5.5 does officially support the JSP 2.0 API. + + + + Applications with a scaffolded Spring MVC UI are currently not + deployable to Google App Engine due to incompatibilities in the JSP + support and JSTL. See ROO-1006 + for details. + + + + Applications with a scaffolded GWT UI require a manual + adjustment in + src/main/webapp/WEB-INF/spring/webmvc-config.xml (this + will not be required when using Spring Framework 3.0.5+): + + <mvc:default-servlet-handler default-servlet-name="_ah_default" /> + + +
    + +
    + Version Numbering Approach + + Spring Roo observes version number standards based on the Apache Portable Runtime + (APR) versioning guidelines as well as the OSGi specifications. In summary + this means all Roo releases adopt the format of MAJOR.MINOR.PATCH.TYPE. + Each segment is separated by a period without any spaces. The + MAJOR.MINOR.PATCH are always integer numbers, and TYPE is an alphanumeric + value. For example, Roo 1.0.3.M1 means major version 1, minor version 0, + patch number 3 and release type M1. + + You can always rely on the natural sort order of the version numbers + to arrive at the latest available version. For example, 1.0.4.RELEASE is + more recent than 1.0.4.RC2. This is because "RELEASE" sorts alphabetically + lower than "RC2". The TYPE segment can generally be broken into two + further undelimited portions, being the release type and a numeric + increment. For example, RC1 means release candidate 1 and RC4 means + release candidate 4. One exception to this is RELEASE means the final + general availability of that release. Other common release types include + "A" for alpha and "M" for milestone. + + We make no guarantees regarding the compatibility of any release + that has a TYPE other than "RELEASE". However, for "RELEASE" releases we + aim to ensure you can use a given "RELEASE" with any other "RELEASE" which + has the same MAJOR.MINOR version number. As such you should be able to + switch from 1.0.4.RELEASE to 1.0.9.RELEASE without any changes. However, + you might have trouble with 1.0.4.RELEASE to 1.0.9.RC1 as RC1 is a + work-in-progress and we may not have identified all regression issues. + Obviously this version portability is only our objective, and sometimes we + need to make exceptions or may inadvertently overlook an issue. We + appreciate you logging a bug + report if you identify a version regression that violates the + conventions expressed in this section, so that at least we can confirm it + and either attempt to remedy it on the next release of that MAJOR.MINOR + version range or bring it to people's attention in the other sections of + this appendix. + + When upgrading you should review the issue tracker for what has + changed since the last version. Because most releases include a large + number of issues in the release notes, we attempt to highlight any major + issues that may require your attention in the sections below. These notes + are not all-encompassing but simply pointers to the main upgrade-related + issues that most people should be aware of. They are also written assuming + you are maintaining currency with the latest public releases of Spring Roo + and therefore the changes you may need to make to your project are + cumulative. +
    + +
    + Upgrading To Any New Release + + Before upgrading any project to the next release of Spring Roo, you + should always: + + + + Run the backup + command using your currently-installed (i.e. existing) version of + Spring Roo. This will help create a ZIP of your project, which may + help if you need to revert. Don't install the new version of Roo until + you've firstly completed this backup. Naturally you can skip this step + if you have an alternate backup technique and have confidence in + it. + + + + Edit your project's pom.xml and verify the Spring + Roo annotations JAR matches the new Roo release you are installing. + Spring Roo 1.1.0.M3 and above will do this automatically on your + behalf when you load it on an existing project. + + + + Edit your project's pom.xml and verify that major + libraries match the new versions that are now used by Roo. The + simplest approach to doing this is to create a new directory and use + "roo script clinic.roo" and then diff your + existing pom.xml against the newly-created Petclinic + pom.xml. + + + + After modifying the pom.xml as described above, you + will need to update your Eclipse .classpath file. The + simplest way to achieve this is to use mvn eclipse:clean + eclipse:eclipse from the command prompt, or use the perform eclipse command + at the roo> shell prompt. You can skip this step if + you use m2eclipse, as would be the case for any SpringSource Tool + Suite user. + + + + Please refer to the specific upgrade section of this appendix for + further instructions concerning upgrading to a particular version of + Roo. + + If you experience any difficulty with upgrading your projects, + please use the community support + forum for assistance. +
    + +
    + Upgrading to 1.2.0.RC1 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.2.0.M1 to Spring Roo 1.2.0.RC1 are as follows: + + + + To align with the new persistence and repository choices + introduced with Roo 1.2.0.M1 the entity command has been adjusted to + take the target persistence type into account. Please change your + log.roo scripts to use the new entity jpa command. More + details about the new entity JPA command as well as related annotation + changes please refer to ROO-2833: + + + Old Annotations & Commands + + + + + + + Active Record + + Repository + + Entity + + Command + + + + + + JPA + + @RooEntity + + + + + + entity + + + + Spring Data JPA + + + + @RooRepositoryJpa + + @RooJpaEntity + + entity --activeRecord false + repository jpa + + + + Spring Data MongoDB + + + + @RooRepositoryMongo + + @RooMongoEntity + + entity mongo + repository mongo + + + +
    + + + + + New Annotations & Commands + + + + + + + Active Record + + Repository + + Entity + + Command + + + + + + JPA + + @RooJpaActiveRecord + + + + + + entity jpa + + + + Spring Data JPA + + + + @RooJpaRepository + + @RooJpaEntity + + entity jpa + --activeRecord false + repository jpa + + + + Spring Data MongoDB + + + + @RooMongoRepository + + @RooMongoEntity + + entity mongo + repository mongo + + + +
    +
    +
    +
    + +
    + Upgrading to 1.2.0.M1 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.5.RELEASE to Spring Roo 1.2.0.M1 are as follows: + + + + The presence of @RooWebScaffold does not automatically trigger + Spring MVC JSON integration any more. The exposeJson attribute in this + annotation has been deprecated and will be removed for subsequent + releases. To create Spring MVC JSON integration please see the JSON chapter or simply use the web mvc json all + command. + + + + The presence of @RooWebScaffold does not automatically trigger + Spring MVC Finder integration any more. The exposeFinders attribute in + this annotation has been deprecated and will be removed for subsequent + releases. To create Spring MVC Finder integration please see MVC chapter or simply use the web mvc finder all + command. + + + + To update a Roo GWT project please run web gwt setup + + +
    + +
    + Upgrading to 1.1.3.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.2.RELEASE to Spring Roo 1.1.3.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + For MVC scaffolded applications it is recommended to manually + replace the list.tagx in your application by creating a dummy project + and copying the list.tagx file into your project. This process will be + automated through a new 'web mvc update tags' command in Roo + 1.1.4+. + + +
    + +
    + Upgrading to 1.1.2.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.1.RELEASE to Spring Roo 1.1.2.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + +
    + +
    + Upgrading to 1.1.1.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.0.RELEASE to Spring Roo 1.1.1.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + Converters for displaying related entities on JSP pages are now + registered from a centralized ConversionService artifact rather than + from individual controllers. The change is transparent if you've never + set @RooWebScaffold(registerConverters=false) or plugged + in a custom ConversionService through <mvc:annotation-driven + conversion-service="myConversionService"/>. If you have then + read on. + + Remove all "registerConverters" attributes from + @RooWebScaffold annotations and make sure the + "conversion-service" attribute from <mvc:annotation-driven + conversion-service="applicationConversionService"/> is set. Then + run the Spring Roo 1.1.1 shell and let it install the new + ConversionService. When Roo is done making changes, manually move any + custom getXxxConverter() methods to the new + ConversionService, delete the GenericConversionService field from all + controllers that have it, and delete any @PostContruct + methods used to register the converters. If you had previously + configured your own ConversionService, move any converters or + formatter registrations to the new ConversionService installed by + Spring Roo. + + +
    + +
    + Upgrading to 1.1.0.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.0.RC1 to Spring Roo 1.1.0.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + +
    + +
    + Upgrading to 1.1.0.RC1 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.0.M3 to Spring Roo 1.1.0.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + + + There have been changes made to the web.xml + configuration to allow deployment of GWT scaffolded applications to + GAE. Please compare a web.xml produced in a new Spring + Roo project with your current project's web.xml to + identify differences. + + + + The GWT maven artifacts in your local maven repository should be + removed so they can be replaced with the latest versions. Make sure to + delete ~/.m2/repository/com/google/gwt and + org/codehaus/mojo/gwt-maven-plugin. + + +
    + +
    + Upgrading to 1.1.0.M3 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.0.M2 to Spring Roo 1.1.0.M3 are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + There have been changes made to the web.xml + configuration following the adoption of Spring Framework 3.0.4 + improvements around root servlet mapping of + DispatcherServlet. Please compare a web.xml + produced in a new Spring Roo project with your current + project's web.xml to identify differences. + + + + If you are trying the early-access Google Web Toolkit (GWT) + support, please be aware that from Spring Roo 1.1.0.M3 until Spring + Roo 1.1.0.RELEASE we will be using GWT 2.1 "snapshot" JARs. This + enables you to have access to the latest improvements in GWT + 2.1. + + +
    + +
    + Upgrading to 1.1.0.M2 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.1.0.M1 to Spring Roo 1.1.0.M2 are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + +
    + +
    + Upgrading to 1.1.0.M1 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.0.2.RELEASE to Spring Roo 1.1.0.M1 are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + If you used Roo 1.0.2's web MVC scaffolding, be aware there are + considerable changes to the web tier to support our new MVC features + (such as JSPX round-tripping and easy tags). The recommended approach + is therefore to start a new project with Roo 1.1.0.M1 to identify the + changes that are needed to src/main/webapp. + + +
    + +
    + Upgrading to 1.0.2.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.0.1.RELEASE to Spring Roo 1.0.2.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + If you are using Spring Security in your Roo application, it is + recommended you review issue ROO-579 + and consider disabling the ShallowEtagHeaderFilter filter + in your web.xml. + + +
    + +
    + Upgrading to 1.0.1.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.0.0.RELEASE to Spring Roo 1.0.1.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + +
    + +
    + Upgrading to 1.0.0.RELEASE + + The main changes you need to be aware of when upgrading from Spring + Roo 1.0.0.RC4 to Spring Roo 1.0.0.RELEASE are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + Due to CSS issues discovered in the Roo RC4 release (ROO-480), + the standard.css, alt.css and the + layout.jspx files required adjustment. To update these + three files, please replace them with the same files generated in a + dummy project using Roo 1.0.0.RELEASE. + + +
    + +
    + Upgrading to 1.0.0.RC4 + + When upgrading from Spring Roo 1.0.0.RC3 to Spring Roo 1.0.0.RC4 you + should be aware that a large number of changes have been applied to the + web scaffolding functionality. This has impacted the Web layer. We + therefore recommend the following: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + Roo 1.0.0.RC4 takes advantage of the new type conversion API + introduced in Spring Framework 3.0.0.RC3 (see chapter + 5 of the Spring reference documentation) which is aimed to + replace property editors. To remove existing property editors from + your current project you can issue the following command: rm -rf + src/main/java/com/foo/domain/*Editor.java (depending on your + package naming convention) + + + + The easiest way to update the web artifacts is to delete the old + ones completely. You can use the following command from a *nix prompt + to achieve this: rm -rf src/main/webapp/* + + + + Another (optional) step is to replace the web controllers. This + step is only required if you have used the dateFormat + @RooWebScaffold(dateFormat="..") attribute in the + @RooWebScaffold annotation: rm -rf + src/main/java/com/foo/web/* (depending on your package naming + convention). Alternatively, you can simply remove this attribute from + the @RooWebScaffold annotation. Note, date formats can now be defined + via the field date command (see ROO-453 + for further information). + + + + Run the controller command again to regenerate all necessary web + artifacts. You might wish to use either the controller all or controller scaffold + command. This will recreate all web artifacts. + + +
    + +
    + Upgrading to 1.0.0.RC3 + + The main changes you need to be aware of when upgrading from Spring + Roo 1.0.0.RC2 to Spring Roo 1.0.0.RC3 are as follows: + + + + Complete the steps recommended in the Upgrading To Any New Release + section. + + + + Edit your project's + src/main/webapp/WEB-INF/urlrewrite.xml and ensure it + protects the resources as discussed in the ROO-271. + + + + If you had previously used the "test mock" or + "persistence exception translation" commands, we have + moved the resulting AspectJ files to the Spring Aspects project (which + has always been a dependency of all Roo projects). This will mean you + automatically receive improvements made to these features in the + future as part of the Spring Framework release cycle. You should + therefore delete the following files if your project contains them: + Jpa_Exception_Translator.aj, + AbstractMethodMockingControl.aj, + JUnitStaticEntityMockingControl.aj and + MockStaticEntityMethods.aj. You must also ensure you use + Spring Framework 3.0.0.RC2 or above (which is the project which + contains the Spring Aspects project). See ROO-315 + and ROO-316 + for further information. + + + + Do not attempt to use the Spring Roo integration built into + SpringSource Tool Suite (STS) 2.2.0 or earlier with Spring Roo + 1.0.0.RC3 or above. You must upgrade to STS 2.2.1 or above if you wish + to use Roo 1.0.0.RC3 with the STS integration. This is due to an + internal API change made to support third-party add-on development. If + you are using STS 2.2.0 (or earlier) and are unable to upgrade, you + can of course use Roo outside of any version of STS without any issue. + The upgrade requirement is simply to access the STS integration, such + as CTRL + R commands and STS' embedded Roo shell. + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-cloud-foundry.xml b/deployment-support/src/site/docbook/reference/base-cloud-foundry.xml new file mode 100644 index 000000000..2c9c84290 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-cloud-foundry.xml @@ -0,0 +1,250 @@ + + + Cloud Foundry Add-On + + VMware Cloud Foundry is + a recently-released platform as a service (PaaS) offering for developers on + many popular programming languages, including Java. + + Spring Roo provides comprehensive integration with Cloud Foundry. With + Roo you can easily login to Cloud Foundry, view your applications, bind to + services, deploy applications and gather statistics. In fact there are more + than 30 unique Cloud Foundry commands added to the Spring Roo shell to help + you explore and benefit from Cloud Foundry. + +
    + Installing the Cloud Foundry Add-On + + The Cloud Foundry add-on provides the mechanism through which Cloud + Foundry features are available in Spring Roo. To install this add-on, you + simply load Spring Roo 1.1.3 and type (most of which can be completed + using the TAB key, or CTRL + SPACE if using STS): + + pgp automatic trust +addon install bundle --bundleSymbolicName org.springframework.roo.addon.cloud.foundry + + + + + + + + The “pgp” command simply ensures the signed bundles needed by the + Cloud Foundry add-on can be installed. The “addon install” command + instructs Roo to download and install the Cloud Foundry support. The + add-on is successfully installed once you see the “Successfully installed + add-on: Spring Roo - Addon - Cloud Foundry [version: a.b.c.d]” message on + your screen. + + As with all Roo add-ons, you could also install the Cloud Foundry + add-on by simply attempting to use it. To follow this alternate + installation path, enter the “pgp automatic trust” command, then “cloud + foundry” and press enter. A list of matching add-ons will be displayed. + You’ll probably want to install the first (and currently only match), so + use the “addon install id --searchResultId 1” command. + + Alternatively you can just executing the following command which + will prompt you to install the Cloud Foundry add-on, it is still required + that you enable automatic trust prior to installation. + + pgp automatic trust +cloud foundry login + + + + + + + + +
    + +
    + Getting Started + + The integration of Cloud Foundry into Roo has added a lot of new + functionality and commands to the Roo shell, in this chapter we will + explore these new commands and deploy a sample application to the cloud. + After installing the Cloud Foundry add-on you will first need to login. To + do this, use the following command: +
    +
    + Logging In + + cloud foundry login + + This command takes in three options: email, password, and + cloudControllerUrl. The cloudControllerUrl is optional, but the when + logging into Cloud Foundry for the first login the email and password are + mandatory. You aren't required to enter the email and password everytime + you login, Roo will store these locally for you. The cloudControllerUrl + defaults the Cloud service provided by VMware, api.cloudfoundry.com, but + can be changed to point to private Cloud Foundry instances. + +
    + +
    + The Commands + After logging in a many new Cloud Foundry comands will be presented + to you. You can see these by typing "cloud foundry" in the shell and then + pressing TAB. + + + + + + + +
    + +
    + Deploying Your Application + + The first command that is likely to be of use is "cloud foundry + deploy". With this command you are are able to deploy an application to + Cloud Foundry. The deploy command has a number of options: appName + (mandatory), path (mandatory), urls, instances, and memory. Roo will + automatically present you with existing deployed applications to enable + you to choose a unique name, and will also present any WARs found in the + project. If a WAR isn't found the "CREATE" option presented. By selecting + create you will trigger a Maven package, which will create a deployable + application. Onece the application has been successfully deployed when you + see "The application 'new-expenses' was successfully pushed". + + + + + + + +
    + +
    + Viewing Your Applications + + After running the above command, and assuming that you had created a + project in the first place your application will be deployed to Cloud + Foundry. To verify this you can run the command "cloud foundry list apps", + which will display all applications currently deployed. + + + + + + + + + There are two other application deployed, both of which are started + and bound to services. You will also notice that a URL has been mapped to + the each application and that the application that was just deployed + "new-expenses" is currently stopped and no services have been bound to it. + The URL has been created and mapped based on the application name, which + is what Roo defaults to if a URL is not provided. + +
    + +
    + Binding Services + + + + + + + The next step is to bind the application "new-expenses" to a + service, before we do this though we need to check that we have a service + to bind to. To this we use the "cloud foundry list services" command which + will display a list of possible services we can create instances of and + currently provisioned services. As can be seen above Cloud Foundry + currently provides 4 services: Redis, MongoDB, RabbitMQ, and MySQL. There + is currently one provisioned service, that is an instance of MySQL called + "misql". As there is already a MySQL service present we are going to bind + this to our "new-expenses" application. + + To bind "new-expenses" to a service we use the "cloud foundry bind + service --serviceName misql --appName new-expenses" command. Roo's + auto-completion makes navigating the options a breeze. + + + + + + + + If you were to run "cloud foundry list apps" at this point you would + see that the application "new-expenses" is now bound to the MySQL service + instance "misql". We should now be ready to start the application, but + before we do lets take a look at how much memory has been assigned to the + application. To do this we run "cloud foundry view app memory". When we + first deployed the application no memory value was specified so, as you + can see below, the default value provisioned is 256 megabytes. + +
    + +
    + Provisioning Memory + + + + + + +
    + Starting Your Application + Now that we have verified that we + should have enough memory to start we simple run "cloud foundry start app + --appName new-expenses". + + + + + + + + + To verify that the application has actually started simply navigate + to the URL you previously mapped to the application, in this case it is + "new-expenses.cloudfoundry.com", and you should see your + application. + + + + + + +
    + +
    + +
    + Conclusion + + Cloud Foundry is a ground breaking service and open source platform + that allows developers to maximise there productivity by not having to + manage the platform to which they deploy. The initial integration with Roo + allows developers to deploy and manage their applications with very little + effort from with in the shell. In this chapter we have installed the Cloud + Foundry Add-On in Roo which enabled applications to be deployed to and + managed on Cloud Foundry. We have shown how easy Cloud Foundry makes it + for the developer to take advantage of the cloud from with Roo, by going + the deployment process step-by-step. There are other commands that haven't + been explicity covered by this guide and may be expanded on in the + future. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-dbre.xml b/deployment-support/src/site/docbook/reference/base-dbre.xml new file mode 100644 index 000000000..9cda6dd3b --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-dbre.xml @@ -0,0 +1,790 @@ + + + Incremental Database Reverse Engineering (DBRE) Add-On + + The incremental database reverse engineering (DBRE) add-on allows you + to create an application tier of JPA 2.0 entities based on the tables in + your database. DBRE will also incrementally maintain your application tier + if you add or remove tables and columns. + +
    + Introduction + +
    + What are the benefits of Roo's incremental reverse + engineering? + + Traditional JPA reverse engineering tools are designed to + introspect a database schema and produce a Java application tier once. + Roo's incremental database reverse engineering feature differs because + it has been designed to enable developers to repeatedly re-introspect a + database schema and update their Java application. For example, consider + if a column or table has been dropped from the database (or renamed). + With Roo the re-introspection process would discover this and helpfully + report errors in the Java tier wherever the now-missing field or entity + was referenced. In simple terms, incremental database reverse + engineering ensures Java type safety and easy application maintenance + even if the database schema is constantly evolving. Just as importantly, + Roo's incremental reverse engineering is implemented using the same + unique design philosophy as the rest of Roo. This means very fast + application delivery, clutter-free .java source files, extensive + usability features in the shell (such as tab completion and hinting) and + so on. +
    + +
    + How does DBRE work? + +
    + Obtaining database metadata + + The DBRE commands (see below) + make live connections to the database configured in your Roo project + and obtain database metadata from the JDBC driver's implementation of + the standard java.sql.DatabaseMetadata + interface. When the database is reverse engineered, the metadata + information is converted to XML and is stored and maintained in the + dbre.xml file in the src/main/resources directory of your project. + DBRE creates JPA entities based on the table names in your database + and fields based on the column names in the tables. Simple and + composite primary keys are supported (see for more details) and relationships + between entities are also created using the imported and exported key + information obtained from the metadata. +
    + +
    + Class and field name creation + + DBRE creates entity classes with names that are derived from the + associated table name using a simple algorithm. If a table's name + contains an underscore, hyphen, forward or back slash character, an + upper case letter is substituted for each of these characters. This is + also similar for column and field names. The following tables contain + some examples. + + + + + + Table name + + DBRE-produced entity class name + + + + + + order + + Order.java + + + + line_item + + LineItem.java + + + + EAM_MEASUREMENT_DATA_1H + + EamMeasurementData1h.java + + + + COM-FOO\BAR + + ComFooBar.java + + + + + + + + + + Column name + + DBRE-produced field name + + + + + + order + + order + + + + EMPLOYEE_NUMBER + + employeeNumber + + + + USR_CNT + + usrCnt + + + + +
    +
    +
    + +
    + Installation + + DBRE supports most of the relational databases that can be + configured for Roo-managed projects such as MySQL, MS SQL, and PostgreSQL. These drivers + are auto-detected by Roo and you will be prompted by the Roo shell to + download your configured database's JDBC driver when you first issue the + database introspect or database reverse engineer commands (see below). For example, if you have configured + your Roo project to use a MySQL database, when the database introspect + command is first issued, you will see the following console output: + + roo> database introspect --schema no-schema-required +Located add-on that may offer this JDBC driver +1 found, sorted by rank; T = trusted developer; R = Roo 1.1 compatible +ID T R DESCRIPTION ------------------------------------------------------------- +01 Y Y 5.1.13.0001 #jdbcdriver driverclass:com.mysql.jdbc.Driver. This... +-------------------------------------------------------------------------------- +[HINT] use 'addon info id --searchResultId ..' to see details about a search result +[HINT] use 'addon install id --searchResultId ..' to install a specific search result, or +[HINT] use 'addon install bundle --bundleSymbolicName TAB' to install a specific add-on version +JDBC driver not available for 'com.mysql.jdbc.Driver' + + You can get further information about the search result with the + following command:roo> addon info id --searchResultId 01 + + This may list several versions of a driver if available. + + You can then install the latest MySQL JDBC driver by entering the + following Roo command: + + roo> addon install id --searchResultId 01 + + Alternatively, to install a different version (if available) of the + driver you can use the following command:roo> addon install bundle --bundleSymbolicName org.springframework.roo.wrapping.mysql-connector-java;<version> + + The JDBC driver for MySQL is immediately available for you to use. + You can now enter the database introspect and database reverse engineer + commands (see below). + + Note: currently there are no + open-source JDBC drivers for Oracle or DB2 and Roo does not provide OSGi + drivers for these databases. If you are an Oracle or DB2 user, you will + need to obtain an OSGi-enabled driver from Oracle or IBM respectively or + wrap your own Oracle or DB2 driver jars using Roo's wrapping facility. Use + the addon create + wrapper to turn an existing Oracle JDBC driver into an OSGi bundle + you can install into Roo. Roo does provide a wrapping pom.xml for the DB2 + Express-C edition that can be used to convert your db2jcc4.jar into an + OSGi-compliant driver. You can then use the osgi start command to install + the jar, for example: + + roo> osgi start --url file:///tmp/org.springframework.roo.wrapping.db2jcc4-9.7.2.0001.jar +
    + +
    + DBRE Add-On commands + + After you have configured your persistence layer with the jpa setup command and installed + all the JDBC drivers, you can introspect and reverse engineer the database + configured for your project. DBRE contains two commands: + + + + roo> database introspect --schema --file --enableViews + + This command displays the database structure, or schema, in XML + format. The --schema is mandatory and for databases which support + schemas, you can press tab to display a list of schemas from your + database. You can use the --file option to save the information to the + specified file. + + The --enableViews option when specified will also retrieve + database views and display them with the table information. + + Notes: + + + + The term "schema" is not used by all databases, such as + MySQL and Firebird, and for these databases the target database + name is contained in the JDBC URL connection string. However the + --schema option is still required but Roo's tab assist feature + will display "no-schema-required". + + + + PostgreSQL upper case schema names are not supported. + + + + + + roo> database reverse engineer --schema --package --activeRecord --repository + --service --testAutomatically --enableViews + --includeTables --excludeTables + --includeNonPortableAttributes + --disableVersionFields --disableGeneratedIdentifiers + + This command creates JPA entities in your project representing + the tables and columns in your database. As for the database + introspect command, the --schema option is required and tab assistance + is available. You can use the --package option to specify a Java + package where your entities will be created. If you do not specify the + --package option on second and subsequent executions of the database + reverse engineer command, new entities will be created in the same + package as they were previously created in. + + Use the --activeRecord option to create 'Active Record' entities (default if not specified). + + Use the --repository option to create Spring Data JPA Repositories for each entity. + If specified as true, the --activeRecord option is ignored. + + Use the --service option to create a service layer for each entity. + + Use the --testAutomatically option to create integration tests + automatically for each new entity created by reverse + engineering. + + The --enableViews option when specified will also retrieve + database views and reverse engineer them into entities. Note that this + option should only be used in specialised use cases only, such as + those with database triggers. + + You can generate non-portable JPA @Column attributes, such as + 'columnDefinition' by specifying the --includeNonPortableAttributes + option. + + Use the --disableVersionFields option to disable the generation of 'version' fields. + + Use the --disableGeneratedIdentifiers option to disable auto generated identifiers. + + Since the DBRE Add-on provides incremental database reverse + engineering, you can execute the command as many times as you want and + your JPA entities will be maintained by Roo, that is, new fields will + be added if new columns are added to a table, or fields will be + removed if columns are deleted. Entities are also deleted in certain + circumstances if their corresponding tables are dropped. + + Examples of the database reverse engineer command: + + + + roo> database reverse engineer --schema order --package ~.domain --excludeTables "foo* bar?" + + This will reverse engineer all tables + except any table whose name starts with 'foo' + and any table called bar with one extra character, such as 'bar1' + or 'bars'. + + You can use the --includeTables and --excludeTables option + to specify tables that you want or do not want reverse engineered + respectively. The options can take one or more table names. If + more than one table is required, the tables must be enclosed in + double quotes and each separated by a space. Wild-card searching + is also permitted using the asterisk (*) character to match one or + more characters or the '?' character to match exactly one + character. For example: + + Note: excluding tables not + only prevent entities from being created but associations are also + not created in other entities. This is done to prevent compile + errors in the source code. + + + + roo> database reverse engineer --schema order --package ~.domain --includeTables "foo* bar?" + + This will reverse engineer all tables who table whose name + starts with 'foo' and any table called bar with one extra + character, such as 'bar1' or 'bars'. + + + + You can also reverse engineer more than one schema by + specifying a doubled-quoted space-separated list of schemas. + Reverse engineering of foreign-key releationships between tables + in different schemas is supported. For example: + + roo> database reverse engineer --schema "schema1 schema2 schema3" --package ~.domain + + This will reverse engineer all tables from schemas + "schema1", "schema2", and "schema3". + + + + +
    + +
    + The @RooDbManaged annotation + + The @RooDbManaged annotation is added to all new entities created by + executing the database reverse engineer command. Other Roo annotations, + @RooJpaActiveRecord, @RooJavaBean, and @RooToString are also added to the + entity class. The attribute "automaticallyDelete" is added to the + @RooDbManaged annotation and is set to "true" so that Roo can delete the + entity if the associated table has been dropped. However, if + "automaticallyDelete" is set to "false", or if any annotations, fields, + constructors, or methods have been added to the entity (i.e in the .java + file), or if any of the Roo annotations are removed, the entity will not + be deleted. + + The presence of the @RooDbmanaged annotation on an entity class + triggers the creation of an AspectJ inter-type declaration (ITD) ".aj" + file where fields and their getters and setters are stored matching the + columns in the table. For example, if an entity called Employee.java is + created by the database reverse engineer command, a file called + Employee_Roo_DbManaged.aj is also created and maintained by Roo. All the + columns of the matching employee table will cause fields to be created in + the entity's DbManaged ITD. An example of a DBRE-created entity is as + follows: + + @RooJavaBean +@RooToString +@RooDbManaged(automaticallyDelete = true) +@RooJpaActiveRecord(table = "employee", schema = "expenses") +public class Employee { +} + + Along with the standard entity, toString, configurable ITDs, a + DbManaged ITD is created if there are more columns in the employee table + apart from a primary key column. For example, if the employee table has + mandatory employee name and employee number columns, and a nullable age + column the ITD could look like this: + + privileged aspect Employee_Roo_DbManaged { + + @Column(name = "employee_number") + @NotNull + private String Employee.employeeNumber; + + public String Employee.getEmployeeNumber() { + return this.employeeNumber; + } + + public void Employee.setEmployeeNumber(String employeeNumber) { + this.employeeNumber = employeeNumber; + } + + @Column(name = "employee_name", length = "100") + @NotNull + private String Employee.employeeName; + + public String Employee.getEmployeeName() { + return this.employeeName; + } + + public void Employee.setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + @Column(name = "age") + private Integer Employee.age; + + public Integer Employee.getAge() { + return this.age; + } + + public void Employee.setAge(Integer age) { + this.age = age; + } + + ... +} + + If you do not want DBRE to manage your entity any more, you can + "push-in" refactor the fields and methods in the DbManaged ITD and remove + the @RooDbManaged annotation from the .java file. +
    + +
    + Supported JPA 2.0 features + + DBRE will produce and maintain primary key fields, including + composite keys, entity relationships such as many-valued and single-valued + associations, and other fields annotated with the JPA @Column + annotation. + + The following sections describe the features currently + supported. + +
    + Simple primary keys + + For a table with a single primary key column, DBRE causes an + identifier field to be created in the entity ITD annotated with @Id and + @Column. This is similar to executing the entity jpa command by + itself. +
    + +
    + Composite primary keys + + For tables with two or more primary key columns, DBRE will create + a primary key class annotated with @RooIdentifier(dbManaged = true) and + add the "identifierType" attribute with the identifier class name to the + @RooJpaActiveRecord annotation in the entity class. For example, a + line_item table has two primary keys, line_item_id and order_id. DBRE + will create the LineItem entity class and LineItemPK identifier class as + follows: + + @RooJavaBean +@RooToString +@RooDbManaged(automaticallyDelete = true) +@RooJpaActiveRecord(identifierType = LineItemPK.class, table = "line_item", schema = "order") +public class LineItem { +} + + @RooIdentifier(dbManaged = true) +public class LineItemPK { +} + + Roo will automatically create the JPA entity ITD containing a + field annotated with @EmbeddedId with type LineItemPK as follows: + + privileged aspect LineItem_Roo_JpaEntity { + + declare @type: LineItem: @Entity; + + declare @type: LineItem: @Table(name = "line_item", schema = "order"); + + @EmbeddedId + private LineItemPK LineItem.id; + + public LineItemPK LineItem.getId() { + return this.id; + } + + public void LineItem.setId(LineItemPK id) { + this.id = id; + } + + ... +} + + and an identifier ITD for the LineItemPK class containing the + primary key fields and the type annotation for @Embeddable, as + follows: + + privileged aspect LineItemPK_Roo_Identifier { + + declare @type: LineItemPK: @Embeddable; + + @Column(name = "line_item_id", nullable = false) + private BigDecimal LineItemPK.lineItemId; + + @Column(name = "order_id", nullable = false) + private BigDecimal LineItemPK.orderId; + + public LineItemPK.new(BigDecimal lineItemId, BigDecimal orderId) { + super(); + this.lineItemId = lineItemId; + this.orderId = orderId; + } + + private LineItemPK.new() { + super(); + } + + ... +} + + If you decide that your table does not require a composite primary + key anymore, the next time you execute the database reverse engineer + command, Roo will automatically change the entity to use a single + primary key and remove the identifier class if it is permitted. +
    + +
    + Entity relationships + + One of the powerful features of DBRE is its ability to create + relationships between entities automatically based on the foreign key + information in the dbre.xml file. The following sections describe the + associations that can be created. + +
    + Many-valued associations with many-to-many multiplicity + + Many-to-many associations are created if a join table is + detected by DBRE. To be identified as a many-to-many join table, the + table must have exactly two primary keys and have exactly two + foreign-keys pointing to other entity tables and have no other + columns. + + For example, the database contains a product table and a + supplier table. The database has been modelled such that a product can + have many suppliers and a supplier can have many products. A join + table called product_supplier also exists and links the two tables + together by having a composite primary key made up of the product id + and supplier id and foreign keys pointing to each of the primary keys + of the product and supplier tables. DBRE will create a bi-directional + many-to-many association. DBRE will designate which entities are the + owning and inverse sides of the association respectively and annotate + the fields accordingly as shown in the following code snippets: + + privileged aspect Product_Roo_DbManaged { + + @ManyToMany + @JoinTable(name = "product_supplier", + joinColumns = { + @JoinColumn(name = "prod_id") }, + inverseJoinColumns = { + @JoinColumn(name = "supp_id") }) + private Set<Supplier> Product.suppliers; + + ... +} privileged aspect Supplier_Roo_DbManaged { + + @ManyToMany(mappedBy = "suppliers") + private Set<Product> Supplier.products; + + ... +} + + DBRE will also create many-to-many associations where the two + tables each have composite primary keys. For example: + + privileged aspect Foo_Roo_DbManaged { + + @ManyToMany + @JoinTable(name = "foo_bar", + joinColumns = { + @JoinColumn(name = "foo_bar_id1", referencedColumnName = "foo_id1"), + @JoinColumn(name = "foo_bar_id2", referencedColumnName = "foo_id2") }, + inverseJoinColumns = { + @JoinColumn(name = "foo_bar_id1", referencedColumnName = "bar_id1"), + @JoinColumn(name = "foo_bar_id2", referencedColumnName = "bar_id2") }) + private Set<Bar> Foo.bars; + + ... +} +
    + +
    + Single-valued associations to other entities that have + one-to-one multiplicity + + If the foreign key column represents the entire primary key (or + the entire index) then the relationship between the tables will be one + to one and a bi-directional one-to-one association is created. + + For example, the database contains a customer table and an + address table and a customer can only have one address. The following + code snippets show the one-to-one mappings: + + privileged aspect Address_Roo_DbManaged { + + @OneToOne + @JoinColumn(name = "address_id") + private Party Address.customer; + + ... +} privileged aspect Customer_Roo_DbManaged { + + @OneToOne(mappedBy = "customer") + private Address Party.address; + + ... +} +
    + +
    + Many-valued associations with one-to-many multiplicity + + If the foreign key column is part of the primary key (or part of + an index) then the relationship between the tables will be one to + many. An example is shown below: + + privileged aspect Order_Roo_DbManaged { + + @OneToMany(mappedBy = "order") + private Set<LineItem> Order.lineItems; + + ... +} +
    + +
    + Single-valued associations to other entities that have + many-to-one multiplicity + + When a one-to-many association is created, for example a set of + LineItem entities in the Order entity in the example above, DBRE will + also create a corresponding many-to-one association in the LineItem + entity, as follows: + + privileged aspect LineItem_Roo_DbManaged { + + @ManyToOne + @JoinColumn(name = "order_id", referencedColumnName = "order_id") + private Order LineItem.order; + + ... +} +
    + +
    + Multiple associations in the same entity + + DBRE will ensure field names are not duplicated. For example, if + an entity has more than one association to another entity, the field + names will be created with unique names. The following code snippet + illustrates this: + + privileged aspect Foo_Roo_DbManaged { + + @ManyToMany + @JoinTable(name = "foo_bar", + joinColumns = { + @JoinColumn(name = "foo_bar_id1", referencedColumnName = "foo_id1"), + @JoinColumn(name = "foo_bar_id2", referencedColumnName = "foo_id2") }, + inverseJoinColumns = { + @JoinColumn(name = "foo_bar_id1", referencedColumnName = "bar_id1"), + @JoinColumn(name = "foo_bar_id2", referencedColumnName = "bar_id2") }) + private Set<Bar> Foo.bars; + + @ManyToMany + @JoinTable(name = "foo_com", + joinColumns = { + @JoinColumn(name = "foo_com_id1", referencedColumnName = "foo_id1"), + @JoinColumn(name = "foo_com_id2", referencedColumnName = "foo_id2") }, + inverseJoinColumns = { + @JoinColumn(name = "foo_com_id1", referencedColumnName = "bar_id1"), + @JoinColumn(name = "foo_com_id2", referencedColumnName = "bar_id2") }) + private Set<Bar> Foo.bars1; + + ... +} +
    +
    + +
    + Other fields + + DBRE will detect column types from the database metadata and + create and maintain fields and field annotations appropriately. Strings, + dates, booleans, numeric fields, CLOBs and BLOBs are all supported by + DBRE, as well as the JSR 303 @NotNull validation constraint. +
    + +
    + Existing fields + + Roo checks the .java file for a field before it creates it in the + ITD. If you code a field in the entity's .java file, Roo will not create + the field in the DbManaged ITD if detected in the database metadata. For + example, if your table has a column called 'name' and you have added a + field called 'name' to the .java file, Roo will not create this field in + the ITD when reverse engineered. + + Roo also ensures the entity's identity field is unique. For + example if the @Id field is called 'id' but you also add a field with + the same name to the .java file, DBRE will automatically rename the @Id + field by prefixing it with an underscore character. +
    +
    + +
    + Troubleshooting + + This section explains scenarios that may be encountered when using + the DBRE feature. + + + + Executing the database introspect or + database reverse engineer commands causes the message 'JDBC driver not + available for oracle.jdbc.OracleDriver' to be + displayed + + This is due to the Oracle JDBC driver not having been installed. + The driver must be installed if you have installed Roo for the first + time. See . This also applies to + other databases, for example, HSQL and H2. + + + + Executing the database introspect or + database reverse engineer commands with the Firebird database + configured causes the message 'Exception in thread "JLine Shell" + java.lang.NoClassDefFoundError: javax/resource/ResourceException' to + be displayed + + This is due to the javax.resource connector jar not installed. + Remove the cache directory under your Roo installation directory, + start the Roo shell, and run the command: + + osgi start --url + http://spring-roo-repository.springsource.org/release/org/springframework/roo/wrapping/org.springframework.roo.wrapping.connector/1.0.0010/org.springframework.roo.wrapping.connector-1.0.0010.jar + + + Re-install the Firebird driver. See . + + + + The error message 'Caused by: + org.hibernate.HibernateException: Missing sequence or table: + hibernate_sequence' appears when starting Tomcat + + When the database reverse engineer command is first run, the + property determining whether tables are created and dropped which is + defined in the persistence.xml file is modified to a value that + prevents new database artifacts from being created. This is done to + avoid deleting the data in your tables when unit tests are run or a + web application is started. For example, if you use Hibernate as your + JPA 2.0 provider the property is called 'hibernate.hbm2ddl.auto' and + is initially set to 'create' when the project is first created. This + value causes Hibernate to create tables and sequences and allows you + to run unit tests and start a web application. However, the property's + value is changed to 'validate' when the database reverse engineer + command is executed. Other JPA providers such as EclipseLink and + OpenJPA have a similar property which are also changed when the + command is run. If you see this issue when running unit tests or when + starting your web application after reverse engineering, you may need + to change the property back to 'create' or 'update'. Check your + persistence.xml for the property values for other JPA + providers. + + + + The message 'Unable to maintain + database-managed entity <entity + name> because its associated table + name could not be found' appears in the Roo console during reverse + engineering + + When DBRE first creates an entity it puts in the table name in + the 'table' attribute of the @RooJpaActiveRecord annotation. This is + the only mechanism DBRE has for associating an entity with a table. If + you remove the 'table' attribute, DBRE has no way of determining what + the entity's corresponding table is and as a result cannot maintain + the entity's fields and associations. + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-gwt.xml b/deployment-support/src/site/docbook/reference/base-gwt.xml new file mode 100644 index 000000000..7fb85a811 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-gwt.xml @@ -0,0 +1,602 @@ + + + Google Web Toolkit Add-On + + Google Web Toolkit (GWT) is a technolgy developed by Google to allow + the use of existing Java knowledge and tools to build high performance, + desktop-esk web applications. Whilst GWT abstracts away many complexities of + web application development by not requiring you to learn Javascript and + HTML nor worry about browser quirks and memory leaks there is still a start-up cost associated with GWT + and the combination of Roo and GWT doesn’t absolve you completely from + getting your hands a little dirty. This chapter aims to explain how Roo can + reduce the time cost involved with getting started with GWT and does not + attempt to provide a complete guide on GWT or its use. The GWT team has + written excellent + documentation to help you in understanding and using GWT in your + project, the GWT documentation is especially useful when it comes to + customising your application. + + The GWT add-on enables you to create a complete web application for + your domain model with a single command. Once enabled, the GWT add-on will + maintain your application to ensure it reflects changes to the domain model. + Currently the add-on only has a single command, which can be used to setup + GWT in any Roo project. As such, Beginning With + Roo: The Tutorial can be leveraged when starting out with Roo and + GWT. + + The first iteration of the add-on allowed you to generate a fully + fledged GWT web application in under a minute via the expenses script (to + run the expenses script just execute the command script expenses.roo from the Roo shell). The + resulting application incorporated several hot new features found in GWT + 2.1, these include: + + + + the new lightweight RequestFactory + infrastructure for client-server communication; + + + + the built-in best practice MVP + (Model View Presenter) framework; + + + + ultra efficient new data + presentation widgets; and, + + + + data-binding + support. + + + + In Roo 1.1.1 we have built upon this by: + + + + making the add-on more Roo-like, via a faux-ITD model; + + + + incorporating all the improvements and fixes found in GWT 2.1.1, + such as support for inheritance in proxied entities; and, + + + + ensuring that user customisation remains intact upon each launch + of Roo. + + + + This chapter will outline each of these improvements in more + detail. + + + +
    + GWT Add-On Commands + + The main GWT add-on commands are as follows: + + + + web gwt setup - turn an + existing Roo project it a GWT web application. + + + + web gwt all - create GWT + request and proxy classes for all domain types in your project + + If your project has a domain model, which is currently + represented by Roo’s JPA support via the entity and related field + commands, additional views will be created to mirror entities in the + domain. (A full run down of how to implement your domain model via the + Roo shell can be found in section + 2.5 of Beginning With Roo: The + Tutorial) + + + + web gwt gae update - to be run + when the database is changed to Google App Engine from an SQL database + or back again + + + + To demostrate the basic structure of the conjured GWT application a + new Roo project, with a very basic domain model, will be created using the + following commands: + + project --topLevelPackage com.springsource.roo.zoo + +jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY + +enum type --class ~.shared.domain.Species +enum constant --name Fish +enum constant --name Bird +enum constant --name Mammal +enum constant --name Reptile +enum constant --name Amphibian + +entity jpa --class ~.server.domain.Animal +field string --fieldName name --notNull +field enum --fieldName species --type ~.shared.domain.Species + + + + This will create project with a layout as presented in Figure + GWT.1. + + + +
    + Figure GWT.1: Basic Roo project + + + + + + +
    + + + + Upon running the gwt setup command, regardless of the presence of a + domain model, a number of static scaffold files will be copied into your + project. Figure GWT.2 displays the new files and directories + (highlighted). + + + +
    + Figure GWT.2: New packages and files created from running “gwt + setup” + + + + + + +
    + + + + Most of the interesting stuff happens in the client package so we + will concentrate on its sub-packages and files. The two sub-packages of + interest are: + + + + managed + + + + this package contains all the files that are maintained by + Roo. These are files that are created and updated to reflect + changes in the domain model. The GWT add-on enforces a number of + rules that mean that the add-on will not touch source. As GWT + doesn’t currently support AspectJ the standard definition of what + constitutes source is different than in other add-ons, such as the + entity add-on. This will be expanded upon in the section ITDs: GWT + Style below. + + + + + + scaffold + + + + this package contains static files that provide a framework + for the other parts of the application. The files in this package + are never updated or changed, they are copied to the Roo project + upon running the gwt setup command. + + + + + + + + After initial setup all the action occurs in the managed package. + The managed package is comprised of: + + + + activity + + + + contains all classes that leverage the Activity + infrastructure which is part of the new MVP framework in GWT. + These files are changed as new entities are added or removed from + the domain model. + + + + + + request + + + + contains all classes that revolve around the use of + RequestFactory. For each entity in the domain model a *Proxy and + *Request class is created as highlighted in Figure GWT.3. More + information can be found on RequestFactory via the GWT + documentation, a basic synopsis is: a *Proxy class represents a + server-side entity and a *Request class represents a server-side + service. + + + +
    + Figure GWT.3: *Proxy and *Request classes + + + + + + +
    +
    +
    +
    + + + ui + + + + contains all the managed view and ui related classes and + files. When an entity is added to the domain model 8 view sets are + created (a set generally includes a concrete-abstract type pair + and a ui.xml file, an example of two file sets appear in Figure + GWT.4) and a ProxyRenderer class. The file sets are as + follows: + + + + *DetailsView + + + + *EditView + + + + *ListEditor + + + + *ListView + + + + *MobileDetailsView + + + + *MobileEditView + + + + *MobileListView + + + + *SetEditor + + + +
    + Figure GWT.4: *View classes and *.ui.xml + files + + + + + + +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    + Running and Compiling + + A GWT application can be run in two ways it can be run via + Development Mode or once compiled to JavaScript from a standard + application server such as Jetty. + + + + Development Mode + + Development mode allows you to make changes to your application + without having to recompile to JavaScript, a time consuming operation, it + also lets you to debug your application as if it were a standard Java + application. More can be found on Development Mode via the GWT team’s + documentation here. + To run the application in Development Mode from the command line execute + the Maven goal mvn gwt:run, this will + open the Development Mode console where you can launch the application by + clicking “Launch Default Browser”. + + + + + GWT Development Mode console + + + + + + + + + Development Mode requires that you are using a browser that supports + the Development Mode plug-in, you should be prompted to install the + plug-in upon first launch of the application if the browser that doesn’t + currently have the plug-in installed. Alternatively you can check to see + whether your browser is supported and download the plug-in from here. + + + + Jetty + + To compile the application to JavaScript and run it in Jetty execute + the Maven goal mvn jetty:run-exploded + from the command line. For larger applications compilation can take some + time, so running the application outside of Development Mode is often not + practical but can be beneficial when wanting to test the speed and size of + the compiled application or to run the application in browsers that are + not currently supported by the Development Mode plug-in. + + +
    + +
    + Desktop and Mobile Views + + The application created via GWT add-on comes in two flavours: + Desktop and Mobile. The default view depends on the device accessing the + application. If you are viewing the application from a desktop browser + then the following Desktop view would be displayed: + + + Desktop List and Details Views + + + + + + + + + + + If you are viewing the application from a smartphone such as an + Android device or an iPhone the following Mobile views would be + displayed: + + + Mobile List and Details Views + + + + + + + + + To force the desktop browser to display the Mobile view instead of + the Desktop the “m=true” query string needs to be added to the URL used to + access the application. For example to access the Mobile view from a + desktop browser whilst using Development Mode the URL would be: + + + http://127.0.0.1:8888/ApplicationScaffold.html?gwt.codesvr=127.0.0.1:9997&m=true + + +
    + +
    + ITDs: GWT Style + + One of the critical technologies that underpin Roo is AspectJ with + Roo relying heavily on its inter-type declaration (ITD) features. GWT + doesn’t currently support ITDs, but will in the future (please vote here + to register your support), due to this a different approach had to be + created which mimics how ITDs works albeit with an impact on class + hierarchy. To achieve the same end as ITDs an abstract-concrete model has + been introduced in Roo 1.1.1, this replicates how ITDs are used within Roo + and provides clear separation between Roo and end-user + modifications. + + To demonstrate the changes a view class that is created, as part of + running the expenses script, will be examined, + EmployeedMobileEditView.java. Prior to 1.1.1, only Roo managed source + files were created, so upon running expenses.roo a singular + EmployeedMobileEditView.java was created. Any changes that Roo needed to + make to this file as result of modifying the server-side Employee entity + would cause user made changes to be overwritten. + + As of Roo 1.1.1 two class files are created for each class that Roo + may need to manage as a result of changes to entities. In addition to the + singular EmployeedMobileEditView.java a + EmployeedMobileEditView_Roo_Gwt.java file is also created from which + EmployeedMobileEditView extends. All changes that Roo needs to make to + will occur ONLY in + EmployeedMobileEditView_Roo_Gwt and the end-user has the ability to + leverage the Roo managed code or override it. + + Following Roo convention a managed abstract class from which a + concrete class extends is suffixed with “_Roo_Gwt”, a warning is also + placed at the top of the source file. If a class is not referenced by + another type only a warning is placed at the top of the source file. These + naming conventions and warnings serve to highlight that this file is + “owned” by Roo and a user shouldn’t make changes to the file. + + +
    + +
    + UiBinder ui.xml Files + + In addition to Roo respecting user modifications to GWT client-side + types changes made to UiBinder xml files are also preserved. The current + implementation is fairly basic and round-tripping support will be added in + a future release. + + The management of ui.xml file works in the following way: + + + + Roo looks for an element that has an “id=boundElementHolder” + attribute, if a “boundElementHolder” element is not found Roo leaves + the file. + + + + If a “boundElementHolder” element is found each element + contained within the “boundElementHolder” element is examined to see + if there is an element which has an id attribute which corresponds to + each bound field declared in the bound type. If an element is not + found it is added based upon what has been specified as part of the + original scaffolded application. + + + + To stop the add-on recreating a field just create an + invisible element with an id attribute equal to the field not be + displayed. For example if the field “supervisor” wasn’t to be + displayed the declared element in “boundElementHolder” would need + to be replaced by <div id=”supervisor” + style=”display:none”/>. Alternatively a adding “display:none” + to the standard declared element’s style attribute can just be + added. + + + + + + Roo will re-order elements based on the order found in the + underlying entity. + + +
    + +
    + Expected GWT Add-On Behaviour + + Prior to Roo 1.1.1 the behaviour of the GWT add-on was largely + undefined, the following clarifies what can be expected of the add-on in + Roo 1.1.1. + + + + The add-on will only make changes to the abstract class, never + the concrete type. NEVER. + + + + Roo managed files are suffixed with _Roo_Gwt and have a warning + comment in the first line notifying the user should not edit the + file. + + + + When a user adds/deletes/edits a field in a monitored Entity the + addon will make appropriate changes in the mirrored types abstract + classes. + + + + When an entity is deleted, or the @RooJpaActiveRecord annotation + is removed, the mirrored types will remain in play as to remain + consistent with not touch user source. + + + + Roo non-destructively manages a UiBinder xml file, thought + formatting is lost in the process. + + + + +
    + +
    + Migrating a Roo GWT project (1.1 -> 1.1.1+) + + Unfortunately a number of breaking API changes in GWT happened with + the release of GWT 2.1.1. Like any application built against an external + library, you will need to refactor your application to deal with these + changes. + + The transition to the new abstract-concrete model and its associated + benefits is not automatic. To take advantage of the new abstract-concrete + model used by the GWT add-on, you will need to inherit from the + respective *_Roo_Gwt files and optionally remove the methods in the + concrete type that have been declared in the *_Roo_Gwt file. + + +
    + +
    + Troubleshooting + + Known GWT Issues + + Whilst a number of issues have been resolved in GWT 2.1.1, there + are still a few problems you will most likely come across: + + + + RequestFactory doesn't support is*()/had*() methods for + primitive booleans and EditorModel doesn't realise that primitive + types are now supported in Proxies, which means that primitives are + still not supported in the GWT add-on. + + + + “mvn clean gwt:compile” doesn’t work and a “mvn clean compile + gwt:compile” needs to be used. + + + + The “Deprecated use of id="boundElementHolder"” warning will be + removed when round-tripping support is added. + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-jsf.xml b/deployment-support/src/site/docbook/reference/base-jsf.xml new file mode 100644 index 000000000..dd2b58300 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-jsf.xml @@ -0,0 +1,221 @@ + + + JavaServer Faces (JSF) Add-On + + The JSF add-on allows you to conveniently scaffold JSF managed beans + and XHTML views for an existing domain model. Currently this domain model is + derived from the Roo supported JPA integration through the entity jpa and + related field commands. The following features are included: + + + + Automatic update of JSF managed beans and converters reflecting + changes in the domain model + + + + Choice of either Oracle Mojarra or Apache MyFaces JSF 2 + implementations + + + + Server-side validation based on JSR 303 constraints defined in the + domain layer + + + + Integration of PrimeFaces JSF Component + Suite, including automatic scaffolding of PrimeFaces controls such + as: + + + + AutoComplete + + + + Calendar + + + + FileUpload + + + + InputText + + + + InputTextarea + + + + Media + + + + SelectManyMenu + + + + Spinner + + + + + + User-selectable PrimeFaces themes + + + +
    + JSF commands + + The JSF add-on contains four commands: + + + + roo> web jsf setup --implementation --library --theme + + When this command is run for the first time in a single-module + project or an empty module, the necessary JSF artifacts are copied to + the project or module such as the pom dependencies and repositories + and the web.xml file. A default PrimeFaces theme called "south-street" + is configured as well in the web.xml. + + The web jsf setup command can be run as many times as you like + to change the JSF implementation and the theme. + + The --implementation option when specifed allows you to chouse + either the Oracle Mojarra or Apache MyFaces JSF + implementations. + + The --library option has only one selectable value, being + PRIMEFACES. + + The --theme option lets you select one of 30 PrimeFaces themes + for your UI. + + + + roo> web jsf all --package + + The web jsf all command creates JSF managed beans and converters + for all entities in the specified package. A JSF XHTML page is also + created in the src/main/webapp/pages directory for each entity. + + + + roo> web jsf scaffold --class --entity --beanName --includeOnMenu + + The web jsf scaffold command lets you create a managed bean for + a particular entity in your project. + + The --class option is where you specify the name of the managed + bean class. + + The --entity option lets you specify the entity for the managed + bean and is only required if the focus is not on the entity you want + to create the managed bean for. + + If you do not wish the 'create' and 'list' menu selections to + appear for the entity in the menu on the generated UI, specify false + in the --includeOnMenu option. + + + + roo> web jsf media --url --player + + The web jsf media command is used for embedding multimedia + content such as videos and music on your JSF home page. + + The --url option is where you specify the url of the the media + content, such as a YouTube video. + + The media player used is automaticallly selected based on the + url or file extension of the media file in the url if applicable, + however, where this cannot be determined you can use the --player + option to select a suitable player. + + +
    + +
    + The @RooJsfManagedBean annotation + + The @RooJsfManagedBean annotation is added to all new classes + created by the web jsf all and web jsf scaffold commands. The annotation + causes the introduction of the javax.faces.bean.ManagedBean and + javax.faces.bean.SessionScoped annotations in the *_Roo_ManagedBean.aj + ITD. Note that if you specify a scope other than @SessionScoped in the + managed bean .java file, the scope annotation is removed from the ITD. For + example, if you want your bean to be @RequestScoped, simply annotate your + managed bean with the @RequestScoped annotation. + + Use the beanName attribute to force the naming of the managed bean + referred to by other beans and in XHTML pages. + + As mentioned before, the includeOnMenu attribute when set to false + prevents the 'Create' and 'List all' menu selections for the entity from + showing in the UI's menu. +
    + +
    + The @RooJsfConverter annotation + + When a new managed bean is created, a converter class is also + created containing the @RooJsfConverter annotation. The JSF converter + class implements the javax.faces.convert.Converter interface and has + implementations of the getAsObject and getAsString methods (introduced in + an ITD) to perform Object-to-String and String-to-Object conversions + between model data objects and a String representation of those objects + that is suitable for rendering. +
    + +
    + The @RooJsfApplicationBean annotation + + Whenever a managed bean is created for the first time, Roo will + install a class containing the @RooJsfApplicationBean annotation. The ITD + generated from this annotation contains the PrimeFaces menu with the + 'Create' and 'List all' operations for each entity. Whenever a managed + bean is created, provding the @RooJsfManagedBean includeOnMenu attribute + is either not specifed or set to 'true', new menu selections are + automatically added to the *__Roo_ApplicationBean.aj ITD. Similarly, when + a managaed bean is deleted or the includeOnMenu attribute is set to false, + the menu selections are removed. +
    + +
    + The bikeshop example + + The Roo distribution contains a script called bikeshop.roo that + demonstrates the JSF add-on capability. Please note that the --equals + attribute should be specified as true on the entity jpa command for all + entities intended to be scaffolded with JSF. Alternatively, add the + @RooEquals annotation to existing entities. + + In the Roo shell, type: + + roo> script bikeshop.roo + + When complete, exit the shell and run Jetty as follows: + + mvn jetty:run-exploded + + View the application at http://localhost:8080/bikeshop: + + + + + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-json.xml b/deployment-support/src/site/docbook/reference/base-json.xml new file mode 100644 index 000000000..5736a48da --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-json.xml @@ -0,0 +1,331 @@ + + + JSON Add-On + + + There are a number of ways to work with JSON document + serialization and desrialization in Roo projects: + + Option 1: Built-in JSON handling managed in domain layer + (discussed in this section) + + + + This offers customizable FlexJson + integration + + + + Option 2: Spring MVC detects the Jackson library in the + application classpath + + + + simply use Spring's @RequestBody + and @ResponseBody + annotations in the controllers, or + + + + take advantage of Spring's ContentNegotiatingViewResolver + + + The JSON add-on offers JSON support in the domain layer as well + as the Spring MVC scaffolding. A number of methods are provided to + facilitate serialization and deserialization of JSON documents into domain + objects. The JSON add-on makes use of the Flexjson + library. + +
    + Adding JSON Functionality to Domain Types + + The add-on offers an annotation as well as two commands that support + the integration of JSON support into the project's domain layer: + + + + Annotating a target type with the default @RooJson annotation will prompt Roo to create + an ITD with the following four methods: + + public String toJson() { + return new JSONSerializer().exclude("*.class").serialize(this); +} +This method returns a JSON representation of the current + object. + + public static Owner fromJsonToOwner(String json) { + return new JSONDeserializer<Owner>().use(null, Owner.class).deserialize(json); +} +This method has a String parameter representing the JSON + document and returns a domain type instance if the document can be + serialized by the underlying deserializer. + + public static String toJsonArray(Collection<Owner> collection) { + return new JSONSerializer().exclude("*.class").serialize(collection); +} +This method will convert a collection of the target type, + provided as method parameter, into a valid JSON document containing an + array. + + public static Collection<Owner> fromJsonArrayToOwners(String json) { + return new JSONDeserializer<List<Owner>>().use(null, + ArrayList.class).use("values", Owner.class).deserialize(json); +} +This method will convert a JSON array document, passed in as + a method parameter, into a collection of the target type. + + The @RooJson annotation can be used to customize the names of + the methods being introduced to the target type. Furthermore, you can + disable the creation of any of the above listed methods by providing + an empty String argument for the unwanted method in the @RooJson + annotation. Example: + + @RooJson(toJsonMethod="", fromJsonMethod="myOwnMethodName") + + + + The json add Roo + shell command will introduce the @RooJson annotation into the + specified target type. + + + + The json all + command will detect all domain entities in the project and annotate + all of them with the @RooJson annotation. + + +
    + +
    + JSON REST Interface in Spring MVC controllers + + Once your domain types are annotated with the @RooJson annotation, + you can create Spring MVC scaffolding for your JSON enabled types. + + + + + The web mvc + json setup Roo shell command configures the current project to + support JSON integration using Spring MVC. + + + + The web mvc json + add Roo shell command introduces the @RooWebJson annotation into the specified + target type. + + + + The web mvc json + all Roo shell command finds all JSON-enabled types (@RooJson) in the project and creates Spring MVC + controllers for each (if a controller does not already exist), or adds + @RooWebJson to existing controllers + (should they already exist). + + + + Annotating an existing Spring MVC controller with the + @RooWebJson annotation will prompt Roo to create an ITD with a number + of methods: + + + + listJson + @RequestMapping(headers = "Accept=application/json") +@ResponseBody +public ResponseEntity<String> ToppingController.listJson() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json; charset=utf-8"); + List<Topping> result = toppingService.findAllToppings(); + return new ResponseEntity<String>(Topping.toJsonArray(result), headers, HttpStatus.OK); +}As you can see this method takes advantage of Spring's + request mappings and will respond to HTTP GET requests that + contain an 'Accept=application/json' header. The @ResponseBody + annotation is used to serialize the JSON document. + + To test the functionality with curl, you can try out the + Roo "pizza shop" sample script (run roo> script pizzashop.roo; + then quit the Roo shell and start Tomcat 'mvn tomcat:run'): + + curl -i -H "Accept: application/json" http://localhost:8080/pizzashop/toppings + + + + showJson + @RequestMapping(value = "/{id}", headers = "Accept=application/json") +@ResponseBody +public ResponseEntity<String> ToppingController.showJson(@PathVariable("id") Long id) { + Topping topping = toppingService.findTopping(id); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json; charset=utf-8"); + if (topping == null) { + return new ResponseEntity<String>(headers, HttpStatus.NOT_FOUND); + } + return new ResponseEntity<String>(topping.toJson(), headers, HttpStatus.OK); +}This method accepts an HTTP GET request with a @PathVariable + for the requested Topping ID. The entity is serialized and returned + as a JSON document if found, otherwise an HTTP 404 (NOT FOUND) + status code is returned. The accompanying curl command is as + follows: + + curl -i -H "Accept: application/json" http://localhost:8080/pizzashop/toppings/1 + + + + createFromJson + @RequestMapping(method = RequestMethod.POST, headers = "Accept=application/json") +public ResponseEntity<String> ToppingController.createFromJson(@RequestBody String json) { + Topping topping = Topping.fromJsonToTopping(json); + toppingService.saveTopping(topping); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + return new ResponseEntity<String>(headers, HttpStatus.CREATED); +}This method accepts a JSON document sent via HTTP POST, + converts it into a Topping instance, persists that new instance, + and returns an HTTP 201 (CREATED) status code. The accompanying + curl command is as follows: + + curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" + -d '{"name": "Thin Crust"}' http://localhost:8080/pizzashop/bases + + + + createFromJsonArray + @RequestMapping(value = "/jsonArray", method = RequestMethod.POST, headers = "Accept=application/json") +public ResponseEntity<String> ToppingController.createFromJsonArray(@RequestBody String json) { + for (Topping topping: Topping.fromJsonArrayToToppings(json)) { + toppingService.saveTopping(topping); + } + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + return new ResponseEntity<String>(headers, HttpStatus.CREATED); +}This method accepts a document containing a JSON array sent + via HTTP POST and converts the array into instances that + are then persisted. The method returns an HTTP 201 (CREATED) + status code. The accompanying curl command is as follows: + + curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" + -d '[{"name":"Cheesy Crust"},{"name":"Thick Crust"}]' + http://localhost:8080/pizzashop/bases/jsonArray + + + + updateFromJson + @RequestMapping(method = RequestMethod.PUT, headers = "Accept=application/json") +public ResponseEntity<String> ToppingController.updateFromJson(@RequestBody String json) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + Topping topping = Topping.fromJsonToTopping(json); + if (toppingService.updateTopping(topping) == null) { + return new ResponseEntity<String>(headers, HttpStatus.NOT_FOUND); + } + return new ResponseEntity<String>(headers, HttpStatus.OK); +}This method accepts a JSON document sent via HTTP PUT and + converts it into a Topping instance before attempting to merge it + with an existing record. If no existing record is found, an + HTTP 404 (NOT FOUND) status code is sent to the client, + otherwise an HTTP 200 (OK) status code is sent. The accompanying + curl command is as follows: + + curl -i -X PUT -H "Content-Type: application/json" -H "Accept: application/json" + -d '{id:6,name:"Mozzarella",version:1}' + http://localhost:8080/pizzashop/toppings + + + + updateFromJsonArray + @RequestMapping(value = "/jsonArray", method = RequestMethod.PUT, + headers = "Accept=application/json") +public ResponseEntity<String> BaseController.updateFromJsonArray(@RequestBody String json) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + for (Base base: Base.fromJsonArrayToBases(json)) { + if (baseService.updateBase(base) == null) { + return new ResponseEntity<String>(headers, HttpStatus.NOT_FOUND); + } + } + return new ResponseEntity<String>(headers, HttpStatus.OK); +}This method accepts a document containing a JSON array sent + via HTTP PUT and converts the array into transient entities which + are then merged. The method returns an HTTP 404 (NOT FOUND) status + code if any of the instances to be updated are not found, otherwise + it returns an HTTP 200 (OK) status code. The accompanying curl + command is as follows: + + curl -i -X PUT -H "Content-Type: application/json" -H "Accept: application/json" + -d '[{id:1,"name":"Cheesy Crust",version:0},{id:2,"name":"Thick Crust",version:0}]' + http://localhost:8080/pizzashop/bases/jsonArray + + + + deleteFromJson + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, headers = "Accept=application/json") +public ResponseEntity<String> ToppingController.deleteFromJson(@PathVariable("id") Long id) { + Topping topping = toppingService.findTopping(id); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + if (topping == null) { + return new ResponseEntity<String>(headers, HttpStatus.NOT_FOUND); + } + toppingService.deleteTopping(topping); + return new ResponseEntity<String>(headers, HttpStatus.OK); +}This method accepts an HTTP DELETE request with an + @PathVariable identifying the Topping instance to be deleted. + HTTP status code 200 (OK) is returned if a Topping with + that ID was found, otherwise HTTP status code 404 (NOT + FOUND) is returned. The accompanying curl command is as + follows: + + curl -i -X DELETE -H "Accept: application/json" http://localhost:8080/pizzashop/toppings/1 + + + + jsonFind... + [Optional] Roo will also generate a method to retrieve a + document containing a JSON array if the form backing object + defines dynamic finders. Here is an example taken from + VisitController in the pet clinic sample application, after + adding JSON support to it: + + @RequestMapping(params = "find=ByDescriptionAndVisitDate", method = RequestMethod.GET, + headers = "Accept=application/json") +public String jsonFindVisitsByDescriptionAndVisitDate(@RequestParam("description") String desc, + @RequestParam("visitDate") @DateTimeFormat(style = "M-") Date visitDate, Model model) { + return Visit.toJsonArray(Visit.findVisitsByDescriptionAndVisitDate(desc, visitDate).getResultList()); +}This method accepts an HTTP GET request with a number of + request parameters which define the finder method as well as the + finder method arguments. The accompanying curl command is as + follows: + + curl -i -H Accept:application/json + http://localhost:8080/petclinic/visits?find=ByDescriptionAndVisitDate%26description=test%26visitDate=12/1/10 + + + + + + If you need help configuring how FlexJson serializes or deserializes + JSON documents, please refer to their reference + documentation. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-layers.xml b/deployment-support/src/site/docbook/reference/base-layers.xml new file mode 100644 index 000000000..7066df762 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-layers.xml @@ -0,0 +1,429 @@ + + + Application Layering + + Java enterprise applications can take many shapes and forms depending + on their requirements. Depending on these requirements, you need to decide + which layers your application needs. Many applications won't benefit from additional complexity + and maintenance cost of service or repository layers unless there is a need. + With version 1.2.0 Spring Roo offers support for specific application + layering tailored to the needs of the application. This section provides an + overview of Roo's support for service and repository layers. + + + Note: This section provides an overview of the application + layering options Spring Roo offers since the 1.2.0.M1 release. It does not discuss the merits + of different approaches to architecting enterprise applications. + + +
    + The Big Picture + + With the Roo 1.2.0 release internals have been changed to allow the + integration of multiple application layers. This is particularly useful + for the support of different persistence mechanisms. In previous releases + the only persistence supported in Roo core was the JPA Entity Active + Record pattern. With the internal changes in place Roo can now support + alternative persistence providers which support application + layering. + + + + + + + + While there are a number of new layering and persistence choices + available, by default Roo will continue to support the JPA Active Record + Entity by default (marked orange). However, you can easily change existing + applications by adding further service or repository layers (details + below). If you add new layers Roo will automatically change its ITDs in + the consumer layer or service layer respectively. For example it will + automatically inject and call a new service layer within an existing MVC + controller, Integration test or data on demand for a given domain + type. +
    + +
    + Persistence Layers + + There are now three options available in Roo core to support data + persistence, JPA Entities (Active Record style), JPA Repositories and + MongoDB Repositories. + +
    + JPA Entities (Active Record style) + + Active record-style JPA Entities have been the default since the + first release of Spring Roo and will remain so. In order to configure + your project for JPA persistence, you can run the jpa setup + command: + + roo> jpa setup --provider HIBERNATE --database HYPERSONIC_PERSISTENTThis + configures your project to use the Hibername object relational mapper + along with a in-memory database (HSQLDB). Further details about this + persistence option can be found here. + + Active record-style JPA entities supported by Roo need to have a + @RooJpaActiveRecord annotation which + takes care of providing an ID field along with its accessor and mutator, + In addition this annotation creates the typical CRUD methods to support + data access. + + roo> entity jpa --class ~.domain.PizzaThis + command will create a Pizza domain type along with active record-style + methods to persist, update, read and delete your entity. The following + example also contains a number of fields which can be added through the + field command via + the Roo shell. + + @RooJavaBean +@RooToString +@RooJpaActiveRecord +public class Pizza { + + @NotNull + @Size(min = 2) + private String name; + + private BigDecimal price; + + @ManyToMany(cascade = CascadeType.ALL) + private Set<Topping> toppings = new HashSet<Topping>(); + + @ManyToOne + private Base base; +}Further details about command options and functionalities + provided by active record-style JPA Entities please refer to the Persistence Add-on chapter. +
    + +
    + JPA Repository + + Developers who require a repository / DAO layer instead of the + default Roo entity-based persistence approach can do so by creating a + Spring + Data JPA backed repository for a given JPA domain type. The + domain type backing the repository needs have a JPA @Entity annotation + and also a ID field defined along with accessors and mutators. After + configuring your project for JPA persistence via the jpa setup command, this + functionality is automatically provided by annotating the domain type + with Roo's @RooJpaEntity + annotation.roo> entity jpa --class ~.domain.Pizza --activeRecord falseBy + defining --activeRecord false you can opt out of the otherwise default + Active Record style. The following example also contains a number of + fields which can be added through the field command via the Roo + shell. + + @RooJavaBean +@RooToString +@RooJpaEntity +public class Pizza { + + @NotNull + @Size(min = 2) + private String name; + + private BigDecimal price; + + @ManyToMany(cascade = CascadeType.ALL) + private Set<Topping> toppings = new HashSet<Topping>(); + + @ManyToOne + private Base base; +}With a domain type in place you can now create a new + repository for this type by using the repository jpa + command: + + roo> repository jpa --interface ~.repository.PizzaRepository --entity ~.domain.PizzaThis + will create a simple interface definition which leverages Spring Data + JPA: + + @RooJpaRepository(domainType = Pizza.class) +public interface PizzaRepository { +}Of course, you can simply add the @RooJpaRepository annotation on any interface by + hand in lieu of issuing the repository jpa command in + the Roo shell. + + The adition of the @RooJpaRepository annotation will trigger the + creation of a fairly trivial AspectJ ITD which adds an extends statement + to the PizzaRepository interface resulting in the equivalent of this + interface definition: + + public interface PizzaRepository extends JpaRepository<Pizza, Long> {}Note, + the JpaRepository + interface is part of the Spring Data + JPA API and provides all CRUD functionality out of the + box. +
    + +
    + MongoDB Persistence + + As an alternative to JPA persistence, Spring Roo offers MongoDB support by + leveraging the Spring Data + MongoDB project. + +
    + Setup + + To configure a project for MongoDB persistence you can use the + mongo setup + command: + + roo> mongo setupThis + will configure your Spring Application context to use a MongoDB + installation running on localhost and the default port. Optional + command attributes allow you to define host, port, database name, + username and password. Furthermore, to configure your application for + deployment on VMware + CloudFoundry you can use the --cloudFoundry attribute. +
    + +
    + Entities + + Once the application is configured for MongoDB support, the + entity mongo and + repository mongo + commands become available: + + roo> entity mongo --class ~.domain.PizzaThis + command will create a Pizza domain type annotated with @RooMongoEntity. This annotation is responsibe + for triggering the creation of an ITD which provides a Spring Data ID + annotated field as well as its accessor and mutator. The following + example also contains a number of fields which can be added through + the field command + via the Roo shell. + + @RooJavaBean +@RooToString +@RooMongoEntity +public class Pizza { + + @NotNull + @Size(min = 2) + private String name; + + private BigDecimal price; + + @ManyToMany(cascade = CascadeType.ALL) + private Set<Topping> toppings = new HashSet<Topping>(); + + @ManyToOne + private Base base; +} +
    + +
    + Repository + + With a domain type in place you can now create a new repository + for this type by using the repository mongo + command (or by applying the @RooMongoRepository annotation to an + arbitrary interface: + + roo> repository mongo --interface ~.repository.PizzaRepository --entity ~.domain.PizzaThis + will create a simple interface definition which leverages Spring + Data MongoDB: + + @RooMongoRepository(domainType = Pizza.class) +public interface PizzaRepository { + + List<Pizza> findAll(); +}Similar the Spring Data JPA driven repository seen above, + this interface is augmented through an ITD which introduces the PagingAndSortingRepository + provided by the Spring Data API and is responsible for providing all + necessary CRUD functionality. In addition this interface defines a + 'custom' finder which is not offered by the PagingAndSortingRepository + implementation: List<Pizza> findAll();. + This method iis required by Spring Roo's UI scaffolding and is + automatically implemented by the query + builder mechanism offered by Spring Data MongoDB. +
    + +
    + Example & Cloud Foundry Deployment + + Similar to applications which use JPA persistence (see this + blog for details on using JPA with Postgres) MongoDB + applications can be easily deployed to VMware CloudFoundry. The + following script provides an example of the Pizza Shop sample application which has been + adjusted for use with a MongoDB-backed repository + layer:// Create a new project. +project com.springsource.pizzashop + +// Create configuration for MongoDB peristence +mongo setup --cloudFoundry true + +// Define domain model. +entity mongo --class ~.domain.Base --testAutomatically +field string --fieldName name --sizeMin 2 --notNull --class ~.domain.Base +entity mongo --class ~.domain.Topping --testAutomatically +field string --fieldName name --sizeMin 2 --notNull --class ~.domain.Topping +entity mongo --class ~.domain.Pizza --testAutomatically +field string --fieldName name --notNull --sizeMin 2 --class ~.domain.Pizza +field number --fieldName price --type java.lang.Float +field set --fieldName toppings --type ~.domain.Topping +field reference --fieldName base --type ~.domain.Base +entity mongo --class ~.domain.PizzaOrder --testAutomatically +field string --fieldName name --notNull --sizeMin 2 --class ~.domain.PizzaOrder +field string --fieldName address --sizeMax 30 +field number --fieldName total --type java.lang.Float +field date --fieldName deliveryDate --type java.util.Date +field set --fieldName pizzas --type ~.domain.Pizza + +// Add layer support. +repository mongo --interface ~.repository.ToppingRepository --entity ~.domain.Topping +repository mongo --interface ~.repository.BaseRepository --entity ~.domain.Base +repository mongo --interface ~.repository.PizzaRepository --entity ~.domain.Pizza +repository mongo --interface ~.repository.PizzaOrderRepository --entity ~.domain.PizzaOrder +service type --interface ~.service.ToppingService --entity ~.domain.Topping +service type --interface ~.service.BaseService --entity ~.domain.Base +service type --interface ~.service.PizzaService --entity ~.domain.Pizza +service type --interface ~.service.PizzaOrderService --entity ~.domain.PizzaOrder + +// Create a Web UI. +web mvc setup +web mvc all --package ~.web + +// Package the application into a war file. +perform package + +// Deploy and start the application in CloudFoundry +cloud foundry login +cloud foundry deploy --appName roo-pizzashop --path /target/pizzashop-0.1.0.BUILD-SNAPSHOT.war --memory 512 +cloud foundry create service --serviceName pizzashop-mongo --serviceType mongodb +cloud foundry bind service --serviceName pizzashop-mongo --appName roo-pizzashop +cloud foundry start app --appName roo-pizzashop + +
    +
    +
    + +
    + Service Layer + + Developers can also choose to create a service layer in their + application. By default, Roo will create a service interface (and + implementation) for one or more domain entities. If a + persistence-providing layer such as Roo's default entity layer or a repository layer is present for a given + domain entity, the service layer will expose the CRUD functionality + provided by this persistence layer through its interface and + implementation. + + As per Roo's conventions all functionality will be introduced via + AspectJ ITDs therefore providing the developer a clean canvas for + implementing custom business logic which does not naturally fit into + domain entities. Other common use cases for service layers are security or + integration of remoting such as Web Services. For a more detailed + discussion please refer to the architecture chapter. + + The integration of a services layer into a Roo project is similar to + the repository layer by using the @RooService annotation directly or the + service command in the Roo + shell: + + roo> service --interface ~.service.PizzaService --entity ~.domain.PizzaThis + command will create the PizzaService interface in the defined package and + additionally a PizzaServiceImpl in the same package (the name and package + of the PizzaServiceImpl can be customized via the optional --class + attribute). + + @RooService(domainTypes = { Pizza.class }) +public interface PizzaService { +}Following Roo conventions the default CRUD method + definitions can be found in the ITD: + + void savePizza(Pizza pizza); +Pizza findPizza(Long id); +List<Pizza> findAllPizzas(); +List<Pizza> findPizzaEntries(int firstResult, int maxResults); +long countAllPizzas(); +Pizza updatePizza(pizza pizza); +void deletePizza(Pizza pizza); + + Similarly, the PizzaServiceImpl is rather empty: + + public class PizzaServiceImpl implements PizzaService { +} + + Through the AspectJ ITD the PizzaServiceImpl type is annotated with + the @Service and @Transactional annotations by default. Furthermore + the ITD will introduce the following methods and fields into the target + type: + + @Autowired PizzaRepository pizzaRepository; + +public void savePizza(Pizza pizza) { + pizzaRepository.save(pizza); +} + +public Pizza findPizza(Long id) { + return pizzaRepository.findOne(id); +} + +public List<Pizza> findAllPizzas() { + return pizzaRepository.findAll(); +} + +public List<Pizza> findPizzaEntries(int firstResult, int maxResults) { + return pizzaRepository.findAll(new PageRequest(firstResult / maxResults, maxResults)).getContent(); +} + +public long countAllPizzas() { + return pizzaRepository.count(); +} + +public Pizza updatePizza(Pizza pizza) { + return pizzaRepository.save(pizza); +} + +public void deletePizza(Pizza pizza) { + pizzaRepository.delete(pizza); +} +As you can see, Roo will detect if a persistence provider + layer exists for a given domain type and automatically inject this + component in order to delegate all service layer calls to this layer. In + case no persistence (or other 'lower level' layer exists, the service + layer ITD will simply provide method stubs. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-overview.xml b/deployment-support/src/site/docbook/reference/base-overview.xml new file mode 100644 index 000000000..ae1455aeb --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-overview.xml @@ -0,0 +1,130 @@ + + + Base Add-On Overview + + When you download the Spring Roo distribution ZIP, there are actually + two major logical components in use. The first of these is the + "Roo core", which provides an environment in which to + host add-ons and provide services to them. The other component is what we + call "base add-ons". A base add-on is different from a + third party add-on only in that it is included in the Roo distribution by + default and does not require you to separately install it. In addition, you + cannot remove a base add-on using normal Roo commands. + + Base add-ons always adopt the package name prefix + org.springframework.roo.addon. We also have a part of Roo known + as "Roo core". This relates to the core modules, and these always have + package names that start with org.springframework.roo (but + excluding those with "addon" as the next package name segment, + as in that case they'd be a "base add-on"). Roo core provides very few + commands, and whatever commands it provides are generally internal + infrastructure-related features (like "poll status" or "metadata for id") or + sometimes aggregate the features provided by several individual base add-ons + (e.g. "entity jpa + --testAutomatically"). + + Add-ons that do not ship with Spring Roo but are nevertheless about to + be used with it are known as "installable add-ons" (these were previously + called "third-party add-ons", but we decided to change the name in Roo 1.1 + in view that SpringSource itself was publishing add-ons that were not + shipping as part of Roo and the use of the term "third-party" was + confusing). Such add-ons do not appear under the + org.springframework.roo package name space. A large number of + individuals and organizations publish installable add-ons, and indeed even + within the SpringSource division of VMware we have teams publishing + installable add-ons. The decision as to whether an add-on becomes a base + add-on or an installable add-on depends on a large number of factors, but in + general we prefer installable add-ons over base add-ons. This offers + flexibility around release cycles, licenses, deployment footprint, code + maintenance and so on. + + Of course as a user of Roo you do not need to be aware of whether a + particular component is part of Roo core, a base add-on or an installable + add-on. It's just useful for us to formally define these commonly-used terms + and explain the impact on whether you need to install or uninstall a + component or not. + + The individual base add-ons provided by Roo provide capabilities in + the following key functional areas: + + + + Project management (like project creation, dependency management, + "perform" commands) + + + + General type management (like creation of types, toString method, + JavaBean methods) + + + + Persistence (like JPA setup, entities) + + + + Field management (like JSR 303 and field creation with JPA + compliance) + + + + Database introspection and reverse engineering + + + + Dynamic finders (creation of finders without needing to write the + JPA-QL for them) + + + + JUnit testing (with integration and mock testing) + + + + Spring MVC (including URL rewriting, JSP services, controller + management) + + + + Spring Web Flow + + + + Spring Security + + + + Selenium testing + + + + Java Message Service (JMS) + + + + Simple Mail Transfer Service (SMTP) + + + + Log4J configuration + + + + We have added dedicated chapters for many of these functional areas in + this, Part II of our documentation. You can also + find more introductory material concerning these areas in Part I, along with our samples, the command reference and project resources. + diff --git a/deployment-support/src/site/docbook/reference/base-persistence.xml b/deployment-support/src/site/docbook/reference/base-persistence.xml new file mode 100644 index 000000000..c7d4e9d90 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-persistence.xml @@ -0,0 +1,632 @@ + + + Persistence Add-On + + The persistence add-on provides a convenient way to create Java + Persistence API (JPA v2) compliant entities. There are different + commands available to configure JPA, create new JPA-compliant entities, and + add fields to these entities. In the following a summary of the features + offered by the Spring Roo persistence add-on: + +
    + JPA setup command + + The jpa setup command + provides the following options and attributes: + + Database Options: + + + + HSQL (in + memory) + + + + HSQL + (persistent) + + + + H2 (in + memory) + + + + MySQL + + + + Postgres + + + + MS SQL + Server + + + + Sybase + + + + Oracle * + + + + DB2 * + + + + DB2/400 + + + + Google App + Engine (GAE) + + + + Apache + Derby (Java DB) + + + + Firebird + + + + * The JDBC driver dependencies for these databases are not available + in public Maven repositories. As such, Roo configures a default dependency + in your project pom.xml. You need to adjust it according to + your specific version of your database driver available in your private + Maven repository. + + Some useful hints to get started with Oracle Express (Oracle XE): + After installing Oracle XE you need to find the JDBC driver under + ${oracle-xe}/app/oracle/product/10.2.0/server/jdbc/lib and + run the command: + + mvn install:install-file -Dfile=ojdbc14_g.jar -DgroupId=com.oracle -DartifactId=ojdbc14 -Dversion=10.2.0.2 -Dpackaging=jar -DgeneratePom=true + + Also, if you dont want Jetty (or Tomcat) to be conflicting with + oracle-xe web-server, you should use the following command: mvn + jetty:run -Djetty.port=8090. + + ORM Provider Options: + + + + EclipseLink + + + + Hibernate + + + + OpenJPA + + + + DataNucleus + 3.0 + + + + In addition, the jpa + setup command accepts optional databaseName, + userName and password attributes + for your convenience. However, it's not necessary to use this command. You + can easily edit these details in the database.properties file + at any time. Finally, you can also specify a pre-configured JNDI + datasource via the jndiDataSource attribute. + + The jpa setup command can be re-run at any time. This means you can + change your ORM provider or database when you plan to move your + application between your development setup (e.g. Hibernate with HSQLDB) to + your production setup (e.g. EclipseLink with DB2). Of course this is a + convenience only. You'll naturally experience fewer deployment issues if + you use the same platform for both development and production. + + Running the jpa setup command in the Roo shell takes care of + configuring several aspects in your project: + + + + JPA dependencies are registered in the project + pom.xml Maven configuration. It includes the JPA API, ORM + provider (and its dependencies), DB driver, Spring ORM, Spring JDBC, + Commons DBCP, and Commons Pool + + + + Persistence XML configuration with a persistence-unit + preconfigured based on your choice of ORM provider and Database. Here + is an example for the EclipseLink ORM provider and HSQL + database: + + <persistence xmlns="http://java.sun.com/xml/ns/persistence" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0" + xsi:schemaLocation="http://java.sun.com/xml/ns/persistence + http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> + + <persistence-unit name="persistenceUnit" transaction-type="RESOURCE_LOCAL"> + <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> + <properties> + <property name="eclipselink.target-database" + value="org.eclipse.persistence.platform.database.HSQLPlatform"/> + + <!--value='drop-and-create-tables' to build a new database on each run; + value='create-tables' creates new tables if needed; + value='none' makes no changes to the database--> + <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/> + + <property name="eclipselink.ddl-generation.output-mode" value="database"/> + + <property name="eclipselink.weaving" value="static"/> + </properties> + </persistence-unit> +</persistence> +By default the persistence unit is configured to build a new + database on each application restart. This helps to avoid data + inconsistencies during application development when the domain model + is not yet finalized (new fields added to an entity will yield new + table columns). If you feel that your domain model is stable you can + manually switch to a mode which allows data persistence across + application restarts in the persistence.xml file. This is documented + in the comment above the relevant property. Each ORM provider uses + different property names and values to achieve this. + + + + A database properties file + (src/main/resources/META-INF/spring/database.properties) + which contains user name, password, JDBC driver name and connection + URL details:database.url=jdbc\:hsqldb\:mem\:foo +database.username=sa +database.password= +database.driverClassName=org.hsqldb.jdbcDriver +This file can be edited manually, or you can use the properties set command, + or by using the databaseName, + userName and password + attributes of the jpa + setup command. You can edit the properties file or use any of + these commands at any time. + + + + A DataSource definition and a transaction manager are added to + the Spring application context: + + [...] +<bean class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" id="dataSource"> + <property name="driverClassName" value="${database.driverClassName}"/> + <property name="url" value="${database.url}"/> + <property name="username" value="${database.username}"/> + <property name="password" value="${database.password}"/> +</bean> + +<bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager"> + <property name="entityManagerFactory" ref="entityManagerFactory"/> +</bean> + +<tx:annotation-driven mode="aspectj" transaction-manager="transactionManager"/> + +<bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" + id="entityManagerFactory"> + <property name="dataSource" ref="dataSource"/> +</bean> + + + +
    + +
    + Entity JPA command + + Using the entity jpa command you can + create simple Java beans which are annotated with JPA annotations. There + are several optional attributes which can be used as part of this command + but in its simplest form it will generate the following artifacts: + + roo> entity jpa --class ~.Person +Created SRC_MAIN_JAVA/com/foo +Created SRC_MAIN_JAVA/com/foo/Person.java +Created SRC_MAIN_JAVA/com/foo/Person_Roo_JavaBean.aj +Created SRC_MAIN_JAVA/com/foo/Person_Roo_Jpa_Entity.aj +Created SRC_MAIN_JAVA/com/foo/Person_Roo_Jpa_ActiveRecord.aj +Created SRC_MAIN_JAVA/com/foo/Person_Roo_ToString.aj +Created SRC_MAIN_JAVA/com/foo/Person_Roo_Configurable.aj +~.Person roo> +As you can see from the Roo shell messages there are 6 files + generated (also, note that the context has changed to the Person type in + the Roo shell): + + + + Person.java: + + @RooJavaBean +@RooToString +@RooJpaActiveRecord +public class Person { +} +You will notice that by default, the Person type does not + contain any fields (these will be added with the field commands or + manually in the type) or methods. + + + + Person_Roo_JavaBean.aj (this will only be generated when fields + are added to the Person type) + + The first annotation added by the entity jpa command is the + @RooJavaBean annotation. This annotation will automatically add public + accessors and mutators via an ITD for each field added to the Person + type. This annotation (like all Roo annotations) has source retention + (so it will not be present in the generated byte code). + + + + Person_Roo_ToString.aj + + The second annotation added to the Person type is the + @RooToString annotation. This annotation will generate a toString + method for the Person type via an ITD. The + toString() method will contain a concatenated + representation of all field names and their values using the + commons-lang RefectionToStringBuilder by default. If you want to + provide your own toString() method alongside the Roo generated + toString() method you can declare the + toStringMethod attribute in the @RooToString + annotation. This attribute allows you to change the default method + name of the Roo-managed toString() (default name) + method, thereby allowing your custom toString() method alongside the + Roo-managed method. + + + + Person_Roo_Configurable.aj + + This ITD is automatically created and does not require the + @RooConfigurable annotation to be introduced into the Person.java + type. It takes care of marking the Person type with Spring's + @Configurable annotation. This annotation allows you to inject any + types from the Spring bean factory into the Person type. The injection + of the JPA entity manager (which is defined as a bean in the + application context) is possible due to the presence of the + @Configurable annotation. + + + + Person_Roo_Jpa_Entity.aj + + The forth annotation is the @RooJpaActiveRecord annotation. This + annotation triggers the creation of two ITDs: the + Person_Roo_Jpa_Entity.aj ITD and the Person_Roo_Jpa_ActiveRecord.aj + ITD. Note that If you do not want ActiveRecord-style methods in your + domain object you can just use the @RooJpaEntity annotation. + + The JPA @Entity annotation is added to the + Person_Roo_Jpa_Entity.aj ITD. This annotation marks the Person as + persistable. By default, the JPA implementation of your choice will + create a table definition in your database for this type. Once fields + are added to the Person type, they will be added as columns to the + Person table. + + privileged aspect Person_Roo_Jpa_Entity { + + declare @type: Person: @Entity; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id") + private Long Person.id; + + @Version + @Column(name = "version") + private Integer Person.version; + + public Long Person.getId() { + return this.id; + } + + public void Person.setId(Long id) { + this.id = id; + } + + public Integer Person.getVersion() { + return this.version; + } + + public void Person.setVersion(Integer version) { + this.version = version; + } +} + + + As can be seen, the Person_Roo_Jpa_Entity.aj ITD introduces two + fields by default. An id field (which is + auto-incremented) and a version field (used for + JPA-managed optimistic locking). + + + + Person_Roo_Jpa_ActiveRecord.aj + + As mentioned previously, the @RooJpaActiveRecord annotation also + triggers the creation of the Person_Roo_Jpa_ActiveRecord.aj ITD. This + contains a number of persistence related CRUD methods into your Person + type via the ITD: + + privileged aspect Person_Roo_Jpa_ActiveRecord { + + @PersistenceContext + transient EntityManager Person.entityManager; + + @Transactional + public void Person.persist() { + if (this.entityManager == null) this.entityManager = entityManager(); + this.entityManager.persist(this); + } + + @Transactional + public void Person.remove() { + if (this.entityManager == null) this.entityManager = entityManager(); + if (this.entityManager.contains(this)) { + this.entityManager.remove(this); + } else { + Person attached = this.entityManager.find(this.getClass(), this.id); + this.entityManager.remove(attached); + } + } + + @Transactional + public void Person.flush() { + if (this.entityManager == null) this.entityManager = entityManager(); + this.entityManager.flush(); + } + + @Transactional + public Person Person.merge() { + if (this.entityManager == null) this.entityManager = entityManager(); + Person merged = this.entityManager.merge(this); + this.entityManager.flush(); + return merged; + } + + + public static final EntityManager Person.entityManager() { + EntityManager em = new Person().entityManager; + if (em == null) throw new IllegalStateException("Entity manager has not been \ + injected (is the Spring Aspects JAR configured as an AJC/AJDT \ + aspects library?)"); + return em; + } + + public static long Person.countPeople() { + return entityManager().createQuery("select count(o) from Person o", Long.class) + .getSingleResult(); + } + + @SuppressWarnings("unchecked") + public static List<Person> Person.findAllPeople() { + return entityManager().createQuery("select o from Person o", Person.class).getResultList(); + } + + public static Person Person.findPerson(Long id) { + if (id == null) return null; + return entityManager().find(Person.class, id); + } + + @SuppressWarnings("unchecked") + public static List<Person> Person.findPersonEntries(int firstResult, int maxResults) { + return entityManager().createQuery("select o from Person o", Person.class) + .setFirstResult(firstResult).setMaxResults(maxResults).getResultList(); + } +} + + + The Person_Roo_Jpa_ActiveRecord.aj ITD introduces a number of + methods such as persist(), + remove(), merge(), + flush() which allow the execution of ActiveRecord-style + persistence operations on each Roo-managed JPA entity. Furthermore, a + number of persistence-related convenience methods are provided. These + methods are countPeople(), + findAllPeople(), + findPerson(..), and + findPersonEntries(..). + + All persistence methods are configured with Spring's + Transaction support (Propagation.REQUIRED, + Isolation.DEFAULT). + + Similar to the @RooToString annotation you can change the + default method name for all persistence-related methods generated + through the @RooJpaActiveRecord annotation. For example: + + @RooJpaActiveRecord(persistMethod = "save") + + + + The entity jpa command offers a + number of optional (but very useful) attributes worth mentioning. For + example the --testAutomatically attribute + can be used to have Roo to generate and maintain integration tests for the + Person type (and the persistence methods generated as part of it). + Furthermore, the --abstract and --extends attributes allow you to mark classes as + abstract or inheritance patterns. Of course this can also be done directly + in the Java sources of the Person type but sometimes it is useful to do + this through a Roo command which can be scripted and replayed if desired. + Other attributes allow you to define the identifier field name as well as + the identifier field type which, in turn, allows the use of complex + identifier types. +
    + +
    + Field commands + + As mentioned earlier in this chapter the field commands allow you to + add pre-configured field definitions to your target entity type + (Person.java in our example). In addition to simply adding the field names + and types as defined via the command the appropriate JPA annotations are + added to the field definitions. For example adding a birth day field to + the Person.java type with the following command ...~.Person roo> field date --fieldName birthDay --type java.util.Date +Managed SRC_MAIN_JAVA/com/foo/Person.java +Created SRC_MAIN_JAVA/com/foo/Person_Roo_JavaBean.aj +Managed SRC_MAIN_JAVA/com/foo/Person_Roo_ToString.aj +~.Person roo>... yields the following field definition in + Person.java: + + @Temporal(TemporalType.TIMESTAMP) +@DateTimeFormat(style = "M-") +private Date birthDay; +You'll notice that the @Temporal annotation is part of the + JPA specification and defines how date values are persisted to and + retrieved from the database in a transparent fashion. The @DateTimeFormat + annotation is part of the Spring framework and takes care of printing and + parsing Dates to and from String values when necessary (especially Web + frontends frequently take advantage of this formatting capability). + + Also note that Roo created a Person_Roo_JavaBean.aj ITD to generate + accessors and mutators for the birthDay field and it also updated the + toString() method to take the birthDay field into account. + + Aside from the Date + (and Calendar) type, the field command offers String, Boolean, Enum, Number, Reference and Set types. The Reference and Set types are of special interest + here since they allow you to define relationships between your + entities: + + + + The field + reference command will create a JPA many-to-one (default) or + one-to-one relationship: + + ~.Person roo> field reference --fieldName car --type com.foo.Car +The field definition added to the Person type contains the + appropriate JPA annotations: + + @ManyToOne +@JoinColumn +private Car car; +The optional --cardinality command attribute allows you to + define a one-to-one relationship (via JPAs @OneToOne annotation) + between Person and Car if you wish: + + @OneToOne +@JoinColumn +private Car car;You can add the mappedBy attribute to the + @OneToOne annotation to define the FK name handled by the inverse side + (Car) of this relationship. + + Consider the following constraint: when you delete a Person, any + Car they have should also be deleted, but not vice versa (i.e. you + should be able to delete a Car without deleting its owner). In the + database, the foreign key should be in the "car" table. + + @Entity +@RooJavaBean +@RooJpaActiveRecord +public class Person { + + // Inverse side ("car" table has the FK column) + @OneToOne(cascade = CascadeType.ALL, mappedBy = "owner") + private Car car; + +} + + @Entity +@RooJavaBean +@RooJpaActiveRecord +public class Car { + + // Owning side (this table has the FK column) + @OneToOne + @JoinColumn + private Person owner; +} + + If you delete a Person from the Person list, both the Person and + the Car are deleted. So the cascading works. But if you delete a Car, + the transaction will roll back and you will see an exception due it + being referenced by a person. To overcome this situation you can add + the following method to your Car.java: + + @PreRemove +private void preRemove() { + this.getOwner().setCar(null); +} This hooks into the JPA lifecycle callback function and + will set the reference between Person and Car to null before + attempting to remove the record. + + + + The field set + command will allow you to create a many-to-many (default) or a + one-to-many relationship:field set --fieldName cars --type com.foo.Car + + The field definition added to the Person type contains the + appropriate JPA annotation: + + @ManyToMany(cascade = CascadeType.ALL) +private Set<Car> cars = new HashSet<Car>();To + change the mapping type to one-to-many simply use the --cardinality attribute. To achieve a true m:n + relationship you will need to issue the field set commands for both + sides of the relationship. + + + + Like the entity jpa command, the field command offeres a + number of optional (but very useful) attributes worth mentioning. For + example, you can change the field / column name translations with the + --column attribute. Furthermore there are + a number of attributes which translate directly to their equivalents + defined in JSR 303 + (Bean Validation). These attributes include --notNull, --sizeMin, --sizeMax and other related attributes. Please + refer to the field + command in the appendix to review the different attributes offered. + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-solr.xml b/deployment-support/src/site/docbook/reference/base-solr.xml new file mode 100644 index 000000000..9470b0d92 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-solr.xml @@ -0,0 +1,244 @@ + + + Apache Solr Add-On + + The Apache + Solr add-on provides integration between the Roo generated domain + model and the Apache Solr search platform. If you haven't heard of the open + source Solr system, here's a quick description from the project web + site: + + Solr is the popular, blazing fast open source enterprise search + platform from the Apache Lucene project. Its major features include powerful + full-text search, hit highlighting, faceted search, dynamic clustering, + database integration, and rich document (e.g., Word, PDF) handling. Solr is + highly scalable, providing distributed search and index replication, and it + powers the search and navigation features of many of the world's largest + internet sites. + + Solr is written in Java and runs as a standalone full-text + search server within a servlet container such as Tomcat. Solr uses the + Lucene Java search library at its core for full-text indexing and search, + and has REST-like HTTP/XML and JSON APIs that make it easy to use from + virtually any programming language. Solr's powerful external configuration + allows it to be tailored to almost any type of application without Java + coding, and it has an extensive plugin architecture when more advanced + customization is required. + +
    + Solr Server Installation + + The addon requires a running instance of the Apache Solr server. To + install a Solr server just follow these four easy steps: + + + + Download the server: http://www.apache.org/dyn/closer.cgi/lucene/solr/ + + + + Unzip (untar) the download: tar xf apache-solr-1.4.0.tgz + + + + Change into the solr example directory: cd + apache-solr-1.4.0/example + + + + Start the Solr server: java -jar start.jar + + + + Verify Solr is running correctly: http://localhost:8983/solr/admin/ + + + + +
    + +
    + Solr Add-On Commands + + Once the server is running you can setup the Solr integration for + your project using the following Roo commands: + + + + roo> solr setupThis command + installs the SolrJ driver dependency into the project pom.xml and + registers your Solr server in application context so it can be + injected whereever you need it in your project. + + + + ~.Person roo> solr add This + command allows you to mark an individual entity for automatic Solr + indexing. The @RooSolrSearchable annotation will be added to the + target entity (Person). Furthermore, the following ITD is + generated: + + privileged aspect Person_Roo_SolrSearch { + + @Autowired + transient SolrServer Person.solrServer; + + public static QueryResponse Person.search(String queryString) { + return search(new SolrQuery("person.solrsummary_t:" + queryString.toLowerCase())); + } + + public static QueryResponse Person.search(SolrQuery query) { + try { + QueryResponse rsp = solrServer().query(query); + return rsp; + } catch (Exception e) { + e.printStackTrace(); + } + return new QueryResponse(); + } + + public static void Person.indexPerson(Person person) { + List<Person> people = new ArrayList<Person>(); + people.add(person); + indexPeople(people); + } + + public static void Person.indexPeople(Collection<Person> people) { + List<SolrInputDocument> documents = new ArrayList<SolrInputDocument>(); + for (Person person : people) { + SolrInputDocument sid = new SolrInputDocument(); + sid.addField("id", "person." + person.getId()); + sid.addField("person.birthday_dt", person.getBirthDay()); + sid.addField("person.id_l", person.getId()); + sid.addField("person.name_s", person.getName()); + //add summary field to allow searching documents for objects of this type + sid.addField("person.solrsummary_t", new StringBuilder().append( + person.getBirthDay()).append(" ").append( + person.getId()).append(" ").append(person.getName())); + documents.add(sid); + } + try { + SolrServer solrServer = solrServer(); + solrServer.add(documents); + solrServer.commit(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void Person.deleteIndex(Person person) { + SolrServer solrServer = solrServer(); + try { + solrServer.deleteById("person." + person.getId()); + solrServer.commit(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @PostUpdate + @PostPersist + private void Person.postPersistOrUpdate() { + indexPerson(this); + } + + @PreRemove + private void Person.preRemove() { + deleteIndex(this); + } + + public static final SolrServer Person.solrServer() { + SolrServer _solrServer = new Person().solrServer; + if (_solrServer == null) throw new IllegalStateException("Entity manager \ + has not been injected (is the Spring Aspects JAR \ + configured as an AJC/AJDT aspects library?)"); + return _solrServer; + } + +} +The ITD introduces two search methods; one for conducting + simple searches against Solr documents for Person, and another one + which works with a preconfigured SolrQuery object. The SolrQuery + object allows you to leverage all functionalities of the Solr search + server (like faceting, sorting, term highliting, pagination, + etc). + + The indexPerson(..) and + indexPeople(..) methods allow you to add new + person instances or even collections of persons to the Solr index. The + deleteIndex(..) method allows you to remove a + person from the Solr index. + + All indexing, and delete operations are executed in s separate + thread and will therefore not impact the performance of your Web + application (this is currently achieved through the + SolrSearchAsyncTaskExecutor.aj aspect). + + Furthermore, to trigger automatic indexing of new person + instances (or updated person instances) this itd registers the + postPersistOrUpdate() method which hooks into the + JPA lifecycle through the JPA @PostUpdate and @PostPersist + annotations. Similarly, the preRemove() method + hooks in the JPA lifecylce through the @PreRemove annotation. + + + + roo> solr allThis command + will mark all entities in the project for automatic Solr indexing. The + generated functionality is the same as shown above. + + + + +
    + +
    + The @RooSolrSearchable Annotation + + The @RooSolrSearchable annotation allows you to change all method + names through their respective attributes in the annotation. Marking a + method name with an empty String will instruct the Roo Solr add-on to not + generate that method (i.e. @RooSolrSearchable(preRemoveMethod="")). + + By default all fields in a domain entity are indexed as dynamic + fields (defined in the default schema.xml which Solr ships with). The + default format of a field name is as follows: + + <simple-entity-name>.<field-name>_<field-type> +person.birthday_dt + + This ensures each field is uniquely mapped across your domain model + by prepending the entity name followed by the field name and field type + (which is used to trigger the dynamic field mapping). You can change field + names by adding a @Field annotation to a field in the domain object (i.e. + Person) which contains your own field (you need to provide a field + definition in the Solr schema for it as well): + + @Field("my:field:name:birthday") +@Temporal(TemporalType.TIMESTAMP) +@DateTimeFormat(style = "M-") +private Date birthDay; +To index existing DB entity tables each entity exposes a + convenience method (example for Person entity): + + Person.indexPeople(Person.findAllPeople()); The + URL of the solr server location can be changed in the project + src/main/resources/META-INF/spring/solr.properties config file. + + Front-end (controller and MVC/JSP views) are currently a + work-in-progress. However, the following Ajax Library offers a neat + front-end for those who want to take this a step further: http://github.com/evolvingweb/ajax-solr + It is planned to provide a out of the box integration with the Ajax-Solr + front-end through this addon in the medium term. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/base-web.xml b/deployment-support/src/site/docbook/reference/base-web.xml new file mode 100644 index 000000000..076ce7907 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/base-web.xml @@ -0,0 +1,741 @@ + + + Web MVC Add-On + + + CSS considerations: The Web UI has been + tested successfully with FireFox, Opera, Safari, Chrome, and IE. Given + that IE6 is not supported any more by most players in the market, it has + a number of severe technical limitations and it has a fast + declining user base Spring Roo does not support IE6. Your mileage + may vary - there will likely be issues with CSS support. + The Web MVC add-ons allow you to conveniently scaffold Spring + MVC controllers and JSP(X) views for an existing domain model. Currently + this domain model is derived from the Roo supported JPA integration through + the entity jpa and related field commands. As shown in the Introduction and the Beginning With Roo: The Tutorial the Web MVC + scaffolding can deliver a fully functional Web frontend to your domain + model. The following features are included: + + + + Automatic update of JSPX view artifacts reflecting changes in the + domain model + + + + A fully customizable set JSP of tags is provided, all tags are XML + only (no tag-backing Java source code is required) + + + + Tags offer integration with the Dojo Ajax toolkit for + client-side validation, date pickers, tool tips, filtering selects + etc + + + + Automatic URL rewriting to provide best-practice RESTful + URIs + + + + Integration of Apache + Tiles templating framework to allow for structural customization + of the Web user interface + + + + Use of cascading stylesheets to allow for visual customization of + the Web user interface + + + + Use of Spring MVC themeing support to dynamically adjust Web user + interface by changing CSS + + + + Internationalization of complete Web UI is supported by simply + adding new message bundles (6+ languages are already suppprted) + + + + Pagination integration for large datasets + + + + Client- and server-side validation based on JSR 303 constraints + defined in the domain layer + + + + Generated controllers offer best-practice use of Spring framework + MVC support + + + + The following sections will offer further details about available + commands to generate Web MVC artifacts and also the new JSP(X) + round-tripping model introduced in Roo 1.1. + +
    + Controller commands + + The Web MVC addon offers a number of commands to generate and + maintain various Web artifacts: + + + + ~.Person roo> web mvc setupThe + first time the web mvc + setup command is executed Roo will install all artifacts + required for the Web UI. + + + + + + ~.Person roo> web mvc scaffold --class com.foo.web.PersonControllerThe + controller scaffold command will create a Spring MVC controller for + the Person entity with the following method signatures: + + + + + + Method Signature  + + Comment  + + + + + + public String create(@Valid Person person, + BindingResult result, ModelMap modelMap) {..} + + The create method is triggered by HTTP POST requests + to /<app-name>/people. The submitted form data will be + converted to a Person object and validated against JSR 303 + constraints (if present). Response is redirected to the show + method. + + + + public String createForm(ModelMap modelMap) + {..} + + The create form method is triggered by a HTTP GET + request to /<app-name>/people?form. The resulting form + will be prepopulated with a new instance of Person, + referenced Cars and datepatterns (if needed). Returns the + Tiles view name. + + + + public String show(@PathVariable("id") Long id, + ModelMap modelMap) {..} + + The show method is triggered by a HTTP GET request to + /<app-name>/people/<id>. The resulting form is + populated with a Person instance identifier by the id + parameter. Returns the Tiles view name. + + + + public String list(@RequestParam(value = "page", + required = false) Integer page, @RequestParam(value = + "size", required = false) Integer size, ModelMap modelMap) + {..} + + The list method is triggered by a HTTP GET request to + /<app-name>/people. This method has optional + parameters for pagination (page, size). Returns the Tiles + view name. + + + + public String update(@Valid Person person, + BindingResult result, ModelMap modelMap) {..} + + The update method is triggered by a HTTP PUT request + to /<app-name/people. The submitted form data will be + converted to a Person object and validated against JSR 303 + constraints (if present). Response is redirected to the show + method. + + + + public String updateForm(@PathVariable("id") Long + id, ModelMap modelMap) { + + The update form method is triggered by a HTTP GET + request to /<app-name>/people/<id>?form. The + resulting form will be prepopulated with a Person instance + identified by the id parameter, referenced Cars and + datepatterns (if needed). Returns the Tiles view + name. + + + + public String delete(@PathVariable("id") Long id, + @RequestParam(value = "page", required = false) Integer + page, @RequestParam(value = "size", required = false) + Integer size) {..} + + The delete method is triggered by a HTTP DELETE + request to /<app-name>/people/<id>. This method + has optional parameters for pagination (page, size). + Response is redirected to the list method. + + + + public Collection<Car> populateCars() {..} + + This method prepopulates the 'car' attribute. This + method can be adjusted to handle larger collections in + different ways (pagination, caching, etc). + + + + void addDateTimeFormatPatterns(ModelMap + modelMap) {..} + + Method to register date and time patterns used for + date and time binding for form submissions. + + + + As you can see Roo implements a number of methods to + offer a RESTful MVC frontend to your domain layer. All of these + methods can be found in the PersonController_Roo_Controller.aj ITD. + Feel free to push-in any (or all) of these methods to change default + behaviour implemented by Roo. + + The web mvc + scaffold command offers a number of optional attributes which + let you refine the way paths are managed and which methods should be + generated in the controller. The --disallowedOperations attribute helps you + refine which methods should not be generated in the scaffolded Roo + controller. If you want to prevent several methods from being + generated provide a comma-separated list (i.e.: --disallowedOperations + delete,update,create). You can also specify which methods should be + generated and which not in the PersonController.java source: + + @RooWebScaffold(path = "people", formBackingObject = Person.class, create = false, + update = false, delete = false) +@RequestMapping("/people") +@Controller +public class PersonController {}If you don't define a custom + path Roo will use the plural representation of the simple name of the + form backing entity (in our case 'people'). If you wish you can define + more complex custom paths like /public/people or + /my/special/person/uri (try to to stick to REST patterns if you can + though). A good use case for creating controllers which map to custom + paths is security. You can, for example create two controllers for the + Person entity. One with the default path (/people) for public access + (possibly with delete, and update functionality disabled) and one for + admin access (/admin/people). This way you can easily secure the + /admin/* path with the Spring Security addon. + + + + + + roo> web mvc all --package ~.webThe + web mvc all command + provides a convenient way to quickly generate Web MVC controllers for + all JPA entities Roo can find in your project. You need to specify the + --package attribute to define a + package where these controllers should be generated. While the web mvc + all command is convenient, it does not give you the same level of + control compared to the web mvc scaffold + command. + + + + + + roo> web mvc controller --class com.foo.web.CarController --preferredMapping /public/car +Created SRC_MAIN_JAVA/com/foo/web/CarController.java +Created SRC_MAIN_WEBAPP/WEB-INF/views/public/car +Created SRC_MAIN_WEBAPP/WEB-INF/views/public/car/index.jspx +Managed SRC_MAIN_WEBAPP/WEB-INF/i18n/application.properties +Managed SRC_MAIN_WEBAPP/WEB-INF/views/menu.jspx +Created SRC_MAIN_WEBAPP/WEB-INF/views/public/car/views.xml +The web mvc controller command is different from the other + two controller commands shown above. It does not + generate an ITD with update, create, delete and other methods to + integrate with a specific form backing entity. Instead, this command + will create a simple controller to help you get started for developing + a custom functionality by stubbing a simple + get(), post() and + index() method inside the controller: + + @RequestMapping("/public/car/**") +@Controller +public class CarController { + + @RequestMapping + public void get(ModelMap modelMap, HttpServletRequest request, + HttpServletResponse response) { + } + + @RequestMapping(method = RequestMethod.POST, value = "{id}") + public void post(@PathVariable Long id, ModelMap modelMap, HttpServletRequest request, + HttpServletResponse response) { + } + + @RequestMapping + public String index() { + return "public/car/index"; + } +} +In addition, this controller is registered in the Web MVC + menu and the application Tiles definition. Furthermore, a simple view + (under WEB-INF/views/public/car/index.jspx). + + + + + + roo> web mvc finder add --class ~.web.PersonController --formBackingType ~.domain.PersonThe + web mvc finder + add command used from the Roo shell will introdroduce the + @RooWebFinder annotation into the + specified target type. + + + + + + roo> web mvc finder allThe + web mvc finder + all command used from the Roo shell will introdroduce the + @RooWebFinder annotations to all + existing controllers which have a form backing type that offers + dynamic finders. + + + + +
    + +
    + Application Conversion Service + + Whenever a controller is created for the first time in an + application, Roo will also install an application-wide ConversionService + and configure it for use in webmvc-config.xml as follows: + + <mvc:annotation-driven conversion-service="applicationConversionService"/> +... +<bean id="applicationConversionService" class="com.springsource.vote.web.ApplicationConversionServiceFactoryBean"/> + + Spring MVC uses the ConversionService when it needs to convert + between two objects types -- e.g. Date and String. To become more familiar + with its features we recommend that you review the (brief) sections on + "Type Conversion" and "Field Formatting" in the Spring Framework + documentation. + + The ApplicationConversionServiceFactoryBean is a Roo-managed Java + class and it looks like this: + + @RooConversionService +public class ApplicationConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean { + + @Override + protected void installFormatters(FormatterRegistry registry) { + super.installFormatters(registry); + // Register application converters and formatters + } + +} + + As the comment indicates you can use the installFormatters() method + to register any Converters and Formatters you wish to add. In addition to + that Roo will automatically maintain an ITD with Converter registrations + for every associated entity that needs to be displayed somewhere in a + view. A typical use case is where entities from a many-to-one association + need to be displayed in one of the JSP views. Rather than using the + toString() method for that, a Converter defines the formatting logic for + how to present the associated entity as a String. + + + Note, a custom written or pushed in converter method needs to be + registered manually via the installFormatters + method which is already present in your + ApplicationConversionServiceFactoryBean.java + source code. + + + In some cases you may wish to customize how a specific entity is + formatted as a String in JSP views. For example suppose we have an entity + called Vote. To customize how it is displayed in the JSP views add a + method like this: + + @RooConversionService +public class ApplicationConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean { + + // ... + + public Converter<Vote, String> getVoteConverter() { + return new Converter<Vote, String>() { + public String convert(Vote source) { + return new StringBuilder().append( + source.getIp()).append(" ").append(source.getRegistered()).toString(); + } + }; + } +} + + At this point Roo will notice that the addition of the method and + will remove it from the ITD much like overriding the toString() method in + a Roo entity works. + + Note, in some cases you may create a form backing entity which does + not contain any suitable fields for conversion. For example, the entity + may only contain a field indicating a relationship to another entity (i.e. + type one-to-one or one-to-many). Since Roo does not use these fields for + its generated converters it will simply omit the creation of a converter + for such form backing entities. In these cases you may have to provide + your own custom converter to convert from your entity to a suitable String + representation in order to prevent potential converter exceptions. +
    + +
    + JSP Views + + As mentioned in the previous section, Roo copies a number of static + artifacts into the target project after issuing the controller command for + the first time. These artifacts include Cascading Style Sheets, images, + Tiles layout + definitions, JSP files, message property files, a complete tag library and + a web.xml file. These artifacts are arranged in different folders which is + best illustrated in the following picture: + + + + + + + + The i18n folder contains translations of the Web UI. The + messages_XX.properties files are static resources (which will never be + adjusted after the initial installation) which contain commonly used + literals which are part of the Web UI. The application.properties file + will be managed by Roo to contain application-specific literals. New types + or fields added to the domain layer will result in new key/value + combinations being added to this file. If you wish to translate the values + generated by Roo in the application.properties file, just create a copy of + this file and rename it to application_XX.properties (where XX represents + your language abbreviation). + + Roo uses XML compliant JSP files (JSPX) instead of the more common + JSP format to allow round-tripping of views based on changes in the domain + layer of your project. Not all jspx files in the target project are + managed by Roo after the initial installation (although future addons may + choose to do so). Typically jspx files in sub folders under WEB-INF/views + are maintained in addition to the menu.jspx. + + Here is an example of a typical roo managed jspx file (i.e.: + WEB-INF/views/people/update.jspx): + + <?xml version="1.0" encoding="UTF-8" standalone="no"?> +<div xmlns:field="urn:jsptagdir:/WEB-INF/tags/form/fields" + xmlns:form="urn:jsptagdir:/WEB-INF/tags/form" + xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0"> + <jsp:output omit-xml-declaration="yes"/> + + <form:update id="fu_com_foo_Person" modelAttribute="person" path="/people" + z="3lX+WZW4CQVBb7OlvB0AvdgbGRQ="> + <field:datetime dateTimePattern="${person_birthday_date_format}" field="birthDay" + id="c_com_foo_Person_birthDay" z="dXnEoWaz4rI4CKD9mlz+clbSUP4="/> + <field:select field="car" id="c_com_foo_Person_car" itemValue="id" items="${cars}" + path="/cars" z="z2LA3LvNKRO9OISmZurGjEczHkc="/> + <field:select field="cars" id="c_com_foo_Person_cars" itemValue="id" items="${cars}" + multiple="true" path="/cars" z="c0rdAISxzHsNvJPFfAmEEGz2LU4="/> + </form:update> +</div>You will notice that this file is fairly concise + compared to a normal jsp file. This is due to the extensive use of the tag + library which Roo has installed in your project in the WEB-INF/tags + folder. Each tag offeres a number of attributes which can be used to + customize the appearance / behaviour of the tag - please use code + completion in your favourite editor to review the options or take a peek + into the actual tags. + + All tags are completely self-reliant to provide their functionality + (there are no Java sources needed to implement specific behaviour of any + tag). This should make it very easy to customize the behaviour of the + default tags without any required knowledge of traditional Java JSP tag + development. You are free to customize the contents of the Roo provided + tag library to suit your own requirements. You could even offer your + customized tag library as a new addon which other Roo users could install + to replace the default Roo provided tag library. + + Most tags have a few common attributes which adhere with Roo + conventions to support round-tripping of the jspx artifacts. The following + rules should be considered if you wish to customize tags or jspx files in + a Roo managed project: + + + + The id attribute is used by Roo to find existing elements and + also to determine message labels used as part of the tag + implementation. Changing a tag identifier will result in another + element being generated by Roo when the Roo shell is active. + + + + Roo provided tags are registered in the root element of the jspx + document and are assigned a namespace. You should be able to see + element and attribute code completion when using a modern IDE (i.e. + SpringSource Tool Suite) + + + + The z attribute represents a hash key for a given element (see a + detailed discussion of the hash key attribute in the paragraph + below). + + + + The hash key attribute is important for Roo because it helps + determining if a user has altered a Roo managed element. This is the + secret to round-trip support for JSPX files, as you can edit anything at + any time yet Roo will be able to merge in changes to the JSPX + successfully. The hash key shown in the "z" attribute is calculated as + shown in the following table: + + + + + + Included in hash key calculation  + + Not included in hash + key calculation  + + + + + + Element name (name only, not namespace) + + Namespace of element name + + + + Attribute names present in element + + White spaces used in the element + + + + Attribute values present in the element + + Potential child elements + + + + + + The z key and its value + + + + + + Any attribute (and value) whose name starts with + '_' + + + + + + The order of the attributes does not contribute to the + value of a hash key + + + + + + The hash code thus allows Roo to determine if the element is in its + "original" Roo form, or if the user has modified it in some way. If a user + changes an element, the hash code will not match and this indicates to Roo + that the user has customized that specific element. Once Roo has detected + such an event, Roo will change the "z" attribute value to "user-managed". + This helps clarify to the user that Roo has adopted a "hands off" approach + to that element and it's entirely the user's responsibility to maintain. + If the user wishes for Roo to take responsibility for the management of a + "user-managed" element once again, he or she can simply change the value + of "z" to "?". When Roo sees this, it will replace the questionmark + character with a calculated hash code. This simple mechanism allows Roo to + easily round trip JSPX files without interfering with manual changes + performed by the user. It represents a significant enhancement from Roo + 1.0 where a file was entirely user managed or entirely Roo managed. + + Roo will order fields used in forms in the same sequence they appear + in the domain object. The user can freely change the sequence of form + elements without interfering with Roo's round tripping approach (Roo will + honour user chosen element sequences as long as it can detect individual + elements by their id). + + The user can nest Roo managed elements in in any structure he wishes + without interfering with Roo jspx round tripping. For example elements can + be enclosed by HTML div or span tags to change visual or structural + appearance of a page. + + Most default tags installed by Roo have a render attribute which is + of boolean type. This allows users to completely disable the rendering of + a given tag (and potential sub tags). This is useful in cases where you + don't wish individual fields in a form to be presented to the user but + rather have them autopopulated through other means (i.e. input + type="hidden"). The value of the render attribute can also be calculated + dynamically through the Spring Expression Language (SpEL) or normal JSP + expression language. The generated create.jspx in Roo application + demonstrates this. + + + + Scaffolding of JPA reference + relationships + + The Roo JSP addon will read JSR 303 (bean validation API) + annotations found in a form-backing object. The following convention is + applied for the generation of create and update (and finder) + forms: + + + + Data type / JPA annotation + + Scaffolded HTML Element + + + + + + String (sizeMax < 30; @Size) + + Input + + + + String (sizeMax >=30, @Size) + + Textarea + + + + Number (@Min, @Max, @DecimalMin & @DecimalMax are + recognized) + + Input + + + + Boolean + + Checkbox + + + + Date / Calendar (@Future & @Past are recognized) + (Spring's @DateTimeFormat in combination with the + style or pattern + attributes is recognized) + + Input (with JS Date chooser) + + + + Enum / @Enumerated + + Select + + + + @OneToOne + + Select + + + + @ManyToMany + + Select (multi-select) + + + + @ManyToOne + + Select + + + + @OneToMany * + + Nothing: A message is displayed explaining that this + relationship is managed from the many-side + + + + * As mentioned above, Roo does not scaffold a HTML form + element for the 'one' side of a @OneToMany relationship. To make this + relationship work, you need to provide a @ManyToOne annotated field on the + opposite side: + + +field set --fieldName students --type com.foo.domain.Person --class com.foo.domain.School --cardinality ONE_TO_MANY + +field reference --fieldName school --type com.foo.domain.School --class com.foo.domain.Person --cardinality MANY_TO_ONE + +In case a field is annotated with @Pattern, the regular + expression is passed on to the tag library where it may be applied through + the use of the JS framework of choice. + + + + Automatic Scaffolding of dynamic + finders + + Roo will attempt to scaffold Spring MVC JSP views for all dynamic + finders registered in the form backing object. This is done by using the + web mvc finder all + or web mvc finder + add command. + + Due to file name length restrictions by many file systems (see http://en.wikipedia.org/wiki/Comparison_of_file_systems) + Roo can only generate JSP views for finders which have 244 characters or + less (including folders). If the finder name is longer than 244 characters + Roo will silently skip the generation of jsp view artifacts for the + dynamic finder in question). More detail can be found in ticket ROO-1027. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/images/altui.png b/deployment-support/src/site/docbook/reference/images/altui.png new file mode 100644 index 000000000..a4a531624 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/altui.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/AutoInstallingTheCloudFoundryAddOn.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/AutoInstallingTheCloudFoundryAddOn.png new file mode 100644 index 000000000..c0d2dbc82 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/AutoInstallingTheCloudFoundryAddOn.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryApps.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryApps.png new file mode 100644 index 000000000..995225809 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryApps.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryDeploy.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryDeploy.png new file mode 100644 index 000000000..07f0e5221 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryDeploy.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryFromTheRooShell.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryFromTheRooShell.png new file mode 100644 index 000000000..45310a2b6 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryFromTheRooShell.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryListServices.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryListServices.png new file mode 100644 index 000000000..987dce252 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryListServices.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryNewExpenses.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryNewExpenses.png new file mode 100644 index 000000000..1027856e9 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryNewExpenses.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryServiceBinding.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryServiceBinding.png new file mode 100644 index 000000000..a8449cbac Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryServiceBinding.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryStartApp.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryStartApp.png new file mode 100644 index 000000000..9ac188cb3 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryStartApp.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryViewMemory.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryViewMemory.png new file mode 100644 index 000000000..59318109e Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/CloudFoundryViewMemory.png differ diff --git a/deployment-support/src/site/docbook/reference/images/cloud-foundry/ManuallyInstallingTheCloudFoundryAddOn.png b/deployment-support/src/site/docbook/reference/images/cloud-foundry/ManuallyInstallingTheCloudFoundryAddOn.png new file mode 100644 index 000000000..b41dc9ba2 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/cloud-foundry/ManuallyInstallingTheCloudFoundryAddOn.png differ diff --git a/deployment-support/src/site/docbook/reference/images/codecompletion.png b/deployment-support/src/site/docbook/reference/images/codecompletion.png new file mode 100644 index 000000000..70cda5d62 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/codecompletion.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtAfterGwtSetupZooProjectLayout.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtAfterGwtSetupZooProjectLayout.png new file mode 100644 index 000000000..5d7115ee7 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtAfterGwtSetupZooProjectLayout.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtDesktopView.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtDesktopView.png new file mode 100644 index 000000000..296d6937a Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtDesktopView.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtDevMode.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtDevMode.png new file mode 100644 index 000000000..964b4daee Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtDevMode.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtInitialZooProjectLayout.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtInitialZooProjectLayout.png new file mode 100644 index 000000000..36638710b Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtInitialZooProjectLayout.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtMobileViews.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtMobileViews.png new file mode 100644 index 000000000..a45573839 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtMobileViews.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtProxyRequestClasses.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtProxyRequestClasses.png new file mode 100644 index 000000000..464235e9d Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtProxyRequestClasses.png differ diff --git a/deployment-support/src/site/docbook/reference/images/gwt/GwtViewFiles.png b/deployment-support/src/site/docbook/reference/images/gwt/GwtViewFiles.png new file mode 100644 index 000000000..773d6478f Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/gwt/GwtViewFiles.png differ diff --git a/deployment-support/src/site/docbook/reference/images/jsf-bikeshop.png b/deployment-support/src/site/docbook/reference/images/jsf-bikeshop.png new file mode 100644 index 000000000..f11a77bf9 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/jsf-bikeshop.png differ diff --git a/deployment-support/src/site/docbook/reference/images/layering.png b/deployment-support/src/site/docbook/reference/images/layering.png new file mode 100644 index 000000000..49254c848 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/layering.png differ diff --git a/deployment-support/src/site/docbook/reference/images/pizza.png b/deployment-support/src/site/docbook/reference/images/pizza.png new file mode 100644 index 000000000..4fd66d8d2 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/pizza.png differ diff --git a/deployment-support/src/site/docbook/reference/images/projectfolders.png b/deployment-support/src/site/docbook/reference/images/projectfolders.png new file mode 100644 index 000000000..3757bd444 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/projectfolders.png differ diff --git a/deployment-support/src/site/docbook/reference/images/restmappings.png b/deployment-support/src/site/docbook/reference/images/restmappings.png new file mode 100644 index 000000000..b46b7abb6 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/restmappings.png differ diff --git a/deployment-support/src/site/docbook/reference/images/standardui.png b/deployment-support/src/site/docbook/reference/images/standardui.png new file mode 100644 index 000000000..c21d61ef2 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/standardui.png differ diff --git a/deployment-support/src/site/docbook/reference/images/tenminutes.png b/deployment-support/src/site/docbook/reference/images/tenminutes.png new file mode 100644 index 000000000..41be3eff3 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/tenminutes.png differ diff --git a/deployment-support/src/site/docbook/reference/images/web-project-structure.png b/deployment-support/src/site/docbook/reference/images/web-project-structure.png new file mode 100644 index 000000000..b93d9b921 Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/web-project-structure.png differ diff --git a/deployment-support/src/site/docbook/reference/images/webfolders.png b/deployment-support/src/site/docbook/reference/images/webfolders.png new file mode 100644 index 000000000..f5fde220f Binary files /dev/null and b/deployment-support/src/site/docbook/reference/images/webfolders.png differ diff --git a/deployment-support/src/site/docbook/reference/index.xml b/deployment-support/src/site/docbook/reference/index.xml new file mode 100644 index 000000000..a914d1e30 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/index.xml @@ -0,0 +1,151 @@ + + + + Spring Roo - Reference Documentation + + Spring Roo + + 2.0.0.BUILD-SNAPSHOT + + + + DISID Corporation, S.L + + + + Pivotal Software, Inc + + + + + + Copyright 2009-2014 VMware, Inc. All Rights + Reserved. + + Copies of this document may be made for your own use and for + distribution to others, provided that you do not charge any fee for such + copies and further provided that each copy contains this Copyright + Notice, whether distributed in print or electronically. + + + + + + + + + Welcome to Spring Roo + + + Welcome to Spring Roo! In this part of the reference guide we will + explore everything you need to know in order to use Roo effectively. + We've designed this part so that you can read each chapter consecutively + and stop at any time. However, the more you read, the more you'll learn + and the easier you'll find it to work with Roo. + + Parts II, III and IV + of this manual are more designed for reference usage and people who wish + to extend Roo itself. + + + + + + + + + + + + + + + + + Base Add-Ons + + + This part of the reference guide provides a detailed reference to + the major Roo base add-ons and how they work. This part goes into more + detail than the tutorial chapter and + offers a "bigger picture" discussion than the command reference appendix. + + + + + + + + + + + + + + + + + + + + + + + Internals and Add-On Development + + + In this part of the guide we reveal how Roo works internally. With + this knowledge you'll be well-positioned to be able to check out the Roo + codebase, build a development release, and write add-ons to extend + Roo. + + You should be familiar with Part I + of this reference guide and ideally have used Roo for a period of time + to gain the most value from this part. + + + + + + + + + + + External Add-Ons + + + In this part of the guide we detail external Roo add-ons. + + + + + + + Appendices + + + The fourth and final part of the reference guide provides + appendices and background information that does not neatly belong within + the other parts. The information is intended to be treated as a + reference and not read consecutively. + + + + + + + + + + + diff --git a/deployment-support/src/site/docbook/reference/internals-advanced-add-ons.xml b/deployment-support/src/site/docbook/reference/internals-advanced-add-ons.xml new file mode 100644 index 000000000..1a4a9dd03 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/internals-advanced-add-ons.xml @@ -0,0 +1,20 @@ + + Advanced Add-Ons + TBC. +
    + Metadata + TBC +
    +
    + Annotations + TBC +
    +
    + Inter-Type Declarations + TBC +
    +
    + Recommendations + TBC +
    +
    diff --git a/deployment-support/src/site/docbook/reference/internals-development.xml b/deployment-support/src/site/docbook/reference/internals-development.xml new file mode 100644 index 000000000..be2ef909d --- /dev/null +++ b/deployment-support/src/site/docbook/reference/internals-development.xml @@ -0,0 +1,353 @@ + + + Development Processes + + In this chapter we'll cover how we develop Roo, and how you can check + it out and get involved. + +
    + Guidelines We Follow + + Whether you are part of the Roo core development team, you want to + contribute patches, or you want to develop add-ons there are a few + guidelines we would like to bring to your attention. + + + + Design Goals + + + + High productivity for Java developers + + + + Encourage reuse of existing knowledge, skills and + experience + + + + + + Eliminate barriers to adoption, no runtime component, + minimal size, best possible development experience + + + + Avoid lock-in + + + + No runtime component + + + + Minimal download size + + + + Best possible development experience + + + + + + Embrace the strengths of Java + + + + Development-time: tooling, popularity, API quality, + static typing + + + + + + Deploy-time: performance, memory use, footprint + + + + + + + + Embrace the advantages of AspectJ + + + + Use AspectJ inter-type declarations (ITDs) for “active” + generation + + + + Active generation automatically maintains output + + + + + + Delivers compilation unit separation of concerns + + + + Easier for users, and easier for us as developers + + + + + + Instant IDE support + + + + Reduce time to market and adoption barriers + + + + + + Other good reasons + + + + Mature, “push in” refactor, compile-time is + welcome + + + + + + + + ITD Model + + + + Roo owns *_Roo_*.aj files + + + + Will delete them if necessary + + + + + + + + Every ITD-providing add-on registers a 'suffix' + (namespace) + + + + E.g. the 'Entity' add-on provides *_ROO_JPA_ACTIVE_RECORD.aj + + + + A missing ITD provider causes AJ file removal + + + + + + + + ITDs have proper import management + + + + So they look and feel normal to developers + + + + + + So they 'push-in refactor' in a natural form + + + + + + + + Usability = Highest Priority + + + + Interactivity of Roo Shell + + + + Tab completion, context awareness, command hiding, hint + support, etc + + + + Background monitoring of externally made changes (allows + integration with any development style) + + + + Background monitoring to avoid crude 'generation' + steps + + + + + + Immutability of Metadata Types + + + + Immutability as a first step to manage concurrency + + + + String-based keys (start with 'MID:') + + + + Metadata and keys built on demand only (never + persisted) + + + + Metadata can depend on other metadata + + + + if 'upstream' metadata changes, 'downstream' metadata is + notified + + + + Some metadata will want to monitor the file + system + + + + + + Central metadata service available and cache is provided to + enhance performance + + + + + + Conventions we follow + + + + Ensure usability is first-class + + + + Minimize the JAR footprint that Roo requires + + + + Relocate runtime needs to sister Spring projects + + + + Embrace immutability as much as possible + + + + Maximize performance in generated code + + + + Minimize memory consumption in generated code + + + + Use long artifact IDs to facilitate identification + + + + Don't put into @Roo* what you could calculate + + + + Don't violate generator predictability conventions + + + + +
    + +
    + Source Repository + + We develop against a public Git repository from which you can + anonymously checkout the code: + + git clone git://git.springsource.org/roo/roo.git spring-roo + + Review source code without Git http://git.springsource.org/roo/roo/trees/master + or https://fisheye.springsource.org/changelog/spring-roo. + + Roo itself uses Maven, so it's very easy to build the standard package, + install, assembly and site goals. PGP should be installed, see the 'Setting Up + for Development' section below for details. +
    + +
    + Setting Up for Development + + We maintain up-to-date documentation in the readme.txt + in the root of the checkout location. Please follow these instructions + carefully. +
    + +
    + Submitting Patches + + Submitting a patch for a bug, improvement or even a new feature + which you always wanted addressed can be of great help to the Spring Roo + project. + + To get started, you could build Roo from sources (as described + above), and locally start changing source code as you see fit. Then test + your changes and if all works well, you can create a git patch and attach + it to a ticket in our bug tracker. To create a patch with Git you can + simply use the following command in Roo's source code root + directory: + + <spring-roo>$ git status +<spring-roo>$ git add (files) +<spring-roo>$ git commit -m 'Explain what I changed' +<spring-roo>$ git format-patch origin/master --stdout > ROO-XXXX.patchThe + resulting .patch file can then be attached to the ROO-XXXX ticket in our + bug tracker. +
    + +
    + Path to Committer Status + + Essentially if you submit a patch and we think it is useful to + commit to the code base, we will ask you to complete our contributor + agreement. This is just a simple web form that deals with issues like + patents and copyrights. Once this is done, we can apply your patch to the + source code repository. + + If you're working on a large module that is part of the Roo Git + repository, and you have a history of providing quality patches and + "looking after" the code you've previously written, we will likely invite + you to join us as a committer. We have certain commit policies which are + more fully detailed in the readme.txt that is in the root of + the checkout location. We have numerous committers external to VMware, so + Roo is very much a welcoming project in terms of committers. We look + forward to you joining us. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/internals-simple-add-ons.xml b/deployment-support/src/site/docbook/reference/internals-simple-add-ons.xml new file mode 100644 index 000000000..e578a951b --- /dev/null +++ b/deployment-support/src/site/docbook/reference/internals-simple-add-ons.xml @@ -0,0 +1,914 @@ + + + Simple Add-Ons + + + Pretty Good Privacy in Spring Roo + + The introduction of PGP + with Spring Roo 1.1 allows the Roo user to indicate exactly which + developers he trusts to sign software that Roo will download and + activate in the Roo Shell. Roo itself is now also PGP + signed in every release. To support these capabilities, a new protocol + handler called httppgp:// has been introduced into Roo. This + tells Roo that a given HTTP URL also has a PGP armour detached signature + available. By requiring PGP signatures for all add-ons, we're able to + conveniently and safely host all Roo add-ons for the community. It's up + to the user to decide if he trusts a given PGP key, and without trusting + that key, Roo will refuse to even spend time downloading the + httppgp:// resource. Roo's approach also means you can use + standalone PGP tools like GnuPG to perform signature-related operations to + independently verify Roo's correct operation. + This chapter will provide an introduction to Spring Roo add-on + development. The intention is to provide a step-by-step guide that walks the + developer from zero code to a fully deployed and published add-on that + is immediately available to all Spring Roo users. With the release of Spring + Roo 1.1, a new set of commands is available that are designed to provide a + fast introduction to add-on development, as well as easy access to registered + add-ons by Spring Roo 1.1 users. + + + OSGi in Spring Roo + + Spring Roo runs in an OSGi container + since version 1.1. This internal change is ideal for Roo’s add-on model + because it allows Roo users to install, uninstall, start, and stop + different add-ons dynamically without restarting the Roo shell. + Furthermore, OSGi allows + automatic provisioning of external add-on repositories and provides very + good infrastructure for developing modular, as well as embedded, and + service-oriented applications. Under the hood, Spring Roo uses the Apache Felix + OSGi implementation. + A new add-on named 'Add-On Creator' has been developed that + facilitates the creation of a new Spring Roo add-on. Furthermore, it offers + out of the box support for the + Subversion integration + provided by Google Code as + well as zero setup for hosting the add-on in a public Maven repository hosted + as part of a Google Code + project. In order to register the add-on with RooBot - a Spring Roo add-on + registration service - the add-on is also required to be OSGi compliant, needs + to be signed with PgP keys and the addon bundle needs to be registered + through the httppgp protocol. Add-on developers get all these features + automatically configured if they use the new 'Add-On Creator' feature that + ships with Spring Roo 1.1. + + The following sections will present a complete step-by-step guide + demonstrating how to bootstrap a new Spring Roo add-on, publish and release it + as your own Google Code project, and register it with the RooBot service. + + +
    + Project Setup + + In addition to the general installation steps discussed in the + development process chapter (section 4), you should also follow the + following project specific steps: + + + + Create a new project in Google + Code: Sign in with your Google Account and navigate to http://code.google.com/hosting/createProject + where you can create your project: + + + + Project Name - a meaningful name such as + spring-roo-addon-mvc-i18n-french + + + + Project Summary - a summary of your project such as 'Spring + Roo Add-On to provide French translation for Spring MVC + scaffolding' + + + + Project Description - description that could include a + version compatibility matrix for your add-on + + + + Version control system - Subversion + + + + Source code license - GNU General Public License v3 + + + + Project Labels - Spring Roo, Java, Add-On + + + + + + By default, SVN hosting in Google Code will give you a trunk, + tags, branches and a wiki folder. In order to host a Maven repository + in your Google code project, you should also create a repo folder as + root for the new repository:$ svn mkdir -m "create maven repository" https://<project-name>.googlecode.com/svn/repo --username <username> --password <password> + + + + Check out your newly created project from SVN:$ svn checkout https://<project-name>.googlecode.com/svn/trunk/ <project-name> --username <username> + + + + (optional) Enter your Google Code SVN credentials into your + local maven repository settings.xml:<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> + <servers> + <server> + <id>Google Code</id> + <username>myusername</username> + <password>mypassword</password> + </server> + </servers> +</settings> + + +
    + +
    + Fast Creation + + + Roo's Add-On Creator Commands + + With release 1.1, Spring Roo offers the following commands to help + developers quickly create new add-ons: + + + + addon create simple + + + + What: Command & Operations + support + + + + When: Simple add-ons that want to + add dependencies and/or configuration artifacts to a project + + + + + + + addon create + advanced + + + + What: Command, Operations & ITD + support + + + + When: Full-fledged add-ons that + offer new functionality to project enhancements to existing + Java types in project introduction of new Java types (+ + ITDs) + + + + + + addon create i18n + + + + What: Extension to the existing + ‘web mvc install language’ command + + + + When: A new translation is added to + the Spring MVC admin UI scaffolding + + + + + + addon create + wrapper + + What: Wrapping of a Maven + artifact with an OSGi compliant manifest + + + + When: A dependency is needed to + complete other functionality offered by a Roo add-on (for + example a JDBC driver for the DBRE add-on) + + + + + Once you have installed Java, Maven, PGP, and SVN tools, and have + created and checked out your Google Code project, you can change into the + <project-name> directory, which at this stage should contain only the + .svn directory. In the <project-name> directory, you can + start the Spring Roo shell and use one of the new commands for add-on + creation:roo> addon create simple --topLevelPackage com.foo --projectName <project-name> + + The addon create + simple command will scaffold a number of artefacts: + + [1] pom.xml +[2] readme.txt +[3] legal/LICENSE.TXT +[4] src/main/java/com/foo/batch/BatchCommands.java +[5] src/main/java/com/foo/batch/BatchOperations.java +[5] src/main/java/com/foo/batch/BatchOperationsImpl.java +[6] src/main/java/com/foo/batch/BatchPropertyName.java +[7] src/main/assembly/assembly.xmlThis newly created add-on + project can be imported into the SpringSource Tool Suite via File > + Import > Maven > Existing Maven projects. Let's discuss some of these + artefacts in more detail: + + + + pom.xml - This is the Maven + project configuration. This configuration ships with a number of + preinstalled Maven plugins that facilitate the PGP artefact + signing process as well as the project release process (including + tagging etc). It also adds the OSGi and Felix dependencies needed + for the addon to run in the Roo Shell. Furthermore, + several commonly used Spring Roo modules are preinstalled. These + modules provide functionalities such as file system monitoring, Roo + shell command registration, etc. More information about these + functionalities is provided in the following sections. + + The add-on developer should open up the pom.xml file and modify + some project specific references and documentation (marked in bold + font): + + <?xml version="1.0" encoding="UTF-8" standalone="no"?> +<project [...]> + [...] + <name>com-foo-batch</name> + <organization> + <name>Your project/company name goes here (used in copyright and vendor information in the manifest)</name> + </organization> + [...] + <description>An add-on created by Spring Roo's addon creator feature.</description> + <url>http://www.some.company</url> + <properties>Some of these properties can also be + provided when issuing the addon create + command. + + + + readme.txt - You can provide + any setup or installation information about your add-on in this file. + This file is used by other developers who checkout your add-on source + code from the SVN repository. + + + + legal/LICENSE.TXT - Copy the + appropriate license text for your add-on into this file. + + + + src/main/java/com/foo/batch/BatchCommands.java + - This is a fully working code example demonstrating how to register + commands offered by your addon into the Spring Roo Shell (more detailed + information in the next section). + + + + src/main/java/com/foo/batch/BatchOperations.java & + BatchOperationsImpl.java - These artefacts are used to + perform operations triggered by a command (more information in the + next sections). + + + + src/main/java/com/foo/batch/BatchPropertyName.java + - This type provides a simple example demonstrating the use of static + command completion options for the Spring Roo Shell. An example of + static command completion options are for example the database selection + options as part of the jpa setup + command. + + + + src/main/assembly/assembly.xml + - This artefact defines configurations used for the packaging of the + add-on. + + +
    + +
    + Shell Interaction + + Spring Roo provides an easy way for external add-ons to contribute + new commands to the Roo Shell. Looking at the code extract below, there + are really only two artefacts needed in your command type to register a + new command in the Roo Shell; your type needs to implement the CommandMarker interface, and you need to create a + method annotated with @CliCommand. Let us + review some details: + + [1] @Component +[1] @Service +[2] public class BatchCommands implements CommandMarker { + +[3] @Reference private BatchOperations operations; + @Reference private StaticFieldConverter staticFieldConverter; + +[4] protected void activate(ComponentContext context) { + staticFieldConverter.add(BatchPropertyName.class); + } + +[4] protected void deactivate(ComponentContext context) { + staticFieldConverter.remove(BatchPropertyName.class); + } + +[5] @CliAvailabilityIndicator("welcome property") + public boolean isPropertyAvailable() { + return operations.isProjectAvailable(); + } + +[6] @CliCommand(value="welcome property", help="Obtains a pre-defined system property") +[7] public String property(@CliOption(key="name", mandatory=false, specifiedDefaultValue="USERNAME", unspecifiedDefaultValue="USERNAME", help="The property name you'd like to display") BatchPropertyName propertyName) { + return operations.getProperty(propertyName); + }There are a few artefacts of interest when developing + Spring Roo add-ons: + + + + To register components and services in the Roo shell, the type + needs to be annotated with the @Component & @Service annotations provided by Felix. These + components can be injected into other add-ons (more interesting for + functionalities exposed by operations types). + + + + The command type needs to implement the CommandMarker interface, which Spring Roo scans + for in order to detect classes that contribute commands to the Roo + Shell. + + + + The Felix @Reference + annotations are used to inject services and components offered by + other Spring Roo core components or even other add-ons. In this + example, we are injecting a reference to the add-on's own + BatchOperations interface and the StaticFieldConverter component offered + by the Roo Shell OSGi bundle. The Felix @Reference annotation is similar in purpose to + Spring's @Autowired and @Inject annotations. + + + + The activate and deactivate methods can optionally be + implemented to get access to the lifecycle of the addon's bundle as + managed by the underlying OSGi container. Roo add-on developers can use + these lifecycle hooks for registration and deregistration of converters + (typically in command types) or for the registration of metadata + dependencies (typically in ITD-providing add-ons) or any other component + initialization activities. + + + + The optional @CliAvailabilityIndicator + annotation allows you to limit when a command is available in the + Spring Roo Shell. Methods thus annotated should return a boolean to + indicate whether a command should be visible to the Roo Shell. For + example, many commands are hidden before a project has been created. + + + + The @CliCommand annotation + plays a central role for Roo add-on developers. It allows the + registration of new commands for the Roo Shell. Methods annotated with + @CliCommand can optionally return a + String value to contribute a log statement to the Spring Roo Shell. + Another, more flexible, option to provide log statements in the Roo + Shell is to register a standard JDK logger, which allows the developer + to present color-coded messages to the user in the Roo shell, with the + color coding being dependent on the log level (warning, info, error, + etc). + + + + The optional @CliOption + annotation can be used to annotate method parameters. These parameters + define command attributes that are presented as part of a command. + Roo will attempt to automatically convert user-entered values into the + Java type of the annotated method parameter. In the example above, + Roo will convert the user-entered String to a BatchPropertyName. By + default, Roo offers converters for common number types, String, Date, + Enum, Locale, boolean and Character. See the + org.springframework.roo.shell.converters package + for examples if you need to implement a custom converter. + + +
    + +
    + Operations + + Almost all Spring Roo add-ons provide operations types. These types + do most of the work behind Roo's passive generation principle (active + generation is taken care of by AspectJ Intertype declarations (ITDs) - + more about that later). Methods offered by the operations types provided + by the add-on are typically invoked by the accompanying "command" type. + Alternatively, operations types can also be invoked by other add-ons (this + is a rather unusual case). + + Implementations of the Operations interface need to be annotated + with the Felix @Component and @Service annotations to make their functionality + available within Roo's OSGi container. Dependencies can be injected into + operations types via the Felix @Reference + annotation. If the dependency exists in a package that is not yet + registered in the add-on's pom.xml, you need to add the dependency there + to add the relevant bundle to the add-on's classpath. + + The Add-On Creator generated project includes example code which + uses Roo's source path abstractions, file manager and various Util classes + that take care of project file management. + + Typical functionality offered by operations types include: + + + + Adding new dependencies, plugins, & repositories to the + Maven project pom.xml. + + + + Copying static artefacts from the add-on jar into the user + project (i.e. CSS, images, tagx, configuration files, etc). + + + + Configuring application contexts, web.xml, and other config + artefacts. + + + + Managing properties files in the user project. + + + + Creating new Java source types in the user project. + + + + Adding trigger (or other) annotations to target types (most + common), fields or methods. + + + + Spring Roo offers a wide range of abstractions and metadata types + that support these use cases. For example, the following services are + offered: + + + + org.springframework.roo.process.manager.FileManager + + + + use file manager for all file system operations in project + (offers automatic undo on exception) + + + + + + org.springframework.roo.project.PathResolver + + + + offers abstraction over common project paths + + + + + + org.springframework.roo.metadata.MetadataService + + + + offers access to Roo metadata bean info metadata for + mutators/accessors of target type + + + + + + org.springframework.roo.project.ProjectMetadata + + + + project name, top level package read access to project + dependencies, repositories, etc + + + + + + org.springframework.roo.project.ProjectOperations + + + + add, remove project Maven dependencies, plugins, + repositories, filters, properties, etc + + + + + + + + In addition the org.springframework.roo.support bundle provides a + number of useful utils classes: + + + + org.springframework.roo.support.util.Assert + + + + similar to Spring’s Assert, exceptions thrown by Assert will + cause Roo's File manager abstraction to roll back. + + + + + + org.springframework.roo.support.util.FileCopyUtils + + + + useful for copying resources from add-on into project + + + + + + org.springframework.roo.support.util.TemplateUtils + + + + useful for obtaining InputStream of resources in + bundle + + + + + + org.springframework.roo.support.util.XmlUtils + + + + hides XML ugliness + + + + writeXml methods + + + + Xpath abstraction & cache + + + + XML Transformer setup + + + + + + +
    + +
    + Packaging & Distribution + + Once your add-on is complete, you can test its functionality locally + by generating an OSGi-compliant jar bundle and installing it in the Spring + Roo Shell: + + <project-name>$ mvn clean installThis + will generate your add-on OSGi bundle in the project's + target directory. In a separate directory, you can + start the Spring Roo Shell and use the following command to test your new + add-on: + + roo> osgi start --url file:///<path-to-addon-project/target/<addon-bundle-name>.<version>.jarThis + should install and activate your new Spring Roo Add-On. For troubleshooting, + Roo offers the following OSGi commands: + + + + osgi + ps - Displays OSGi bundle information & status. + This should list your add-on as active. + + + + osgi log - Access + OSGi container logs. This could identify possible issues occurring + during add-on activation. + + + + osgi scr list + - Lists all currently registered services and components. + This should list your add-on's command, metadata provider, and + operations types. + + + + osgi scr info - Info about a specific + component. This can be used to identify possible unresolved + dependencies. + + + + osgi start - + install a new add-on directly from a local or remote location. + + + + help osgi - Help on Roo's ~20 + osgi commands. + + + + Once you have tested the add-on successfully in your development + environment, you can release the add-on source code to your Google Code + project, create a tag, and install all relevant artifacts in the project's + Maven repository: + + <project-name>$ svn add pom.xml src/ legal/ readme.txt + +<project-name>$ svn commit -m "initial commit" + +<project-name>$ mvn release:prepare release:perform The + Maven release plugin will ask for tag and release artefact names. Roo + follows the OSGi convention of using the major, minor and micro version + numbers followed by a textual identifier, e.g. 0.1.1.RELEASE, + 0.1.2.BUILD-SNAPSHOT, etc. + + Deployment for bundles created with Roo's "wrapping" command can be + deployed rather than released. For example, to create a wrapped bundle of + the PostgreSQL JDBC driver, use this command:roo> addon create wrapper --topLevelPackage com.foo.wrapper --projectName spring-roo-postgres-wrapper --artifactId postgresql \ +--groupId postgresql --version 9.0-801.jdbc3 --description "Postgres #jdbcdriver driverclass:org.postgresql.Driver." \ +--licenseUrl http://jdbc.postgresql.org/license.html --docUrl http://jdbc.postgresql.org/ --vendorName "The PostgreSQL Global Development Group" + + This can then be deployed to a Google code project (set up in the + same way as described above) with a simple deploy command: + + <project-name>$ mvn deploy +
    + +
    + Publishing to RooBot + + Once the release is complete, check your Google Code project to see + that your add-on's pom.xml has been updated to the new version (e.g. + 0.1.2.BUILD-SNAPSHOT), that a new tag has been committed to the + tags directory, and that the repo + directory has been populated with all the artifacts seen in a typical + Maven repository. All artefacts have been signed with your private PGP key, + and your public key is available in the relevant .asc + files. In the repo directory, you should also find the + repository.xml file which contains all relevant + information for an OSGi OBR repository. + + + Raw URLs in Google Code Source Browser + + When reviewing file contents via the HTTP interface provided by + Google Code, the reader is presented with HTML documents (which + provide syntax highlighting, etc). To get access to the real (raw) URL + of a document (e.g. repo/repository.xml) you need to click the 'View raw + file' link found in the 'File info' section in the right-hand menu. + Example of a raw URL: + http://<project-name>.googlecode.com/svn/repo/repository.xml. + Make sure the version appendix is removed from the URL before clicking the + 'View raw file' link (i.e. + http://<project-name>.googlecode.com/svn/repo/repository.xml?r=25) + The URL to the raw (see sidebar) repository.xml artefact can + then be registered with RooBot: + + Register your new add-on repository by sending an email to s2-roobot@vmware.com where + the subject line MUST be the raw URL to OSGi repository.xml. The email + body is not currently used (but you can send greetings to the Roo team + ;-). Other registration methods are being considered (web front-end, Roo + shell command, etc). + + RooBot verifies a few aspects before publishing your new add-on to + the community: + + + + The provided repository.xml must be a valid OSGi + repository + + + + The resource URI must use the httppgp prefix i.e.: <resource + uri="httppgp://fr-test.googlecode.com/svn/…/> + + + + The bundle referenced in the repository has a corresponding .asc + file containing the PgP public key + + + + The public PGP key of the add-on signer needs to be available at + http://keyserver.ubuntu.com/ + A guide to PGP key management can be found here. + Make sure to publish your key with this command: + + gpg --send-keys --keyserver keyserver.ubuntu.com <your-key-id> + + + + RooBot will retrieve publicly accessible key information (key + owner name, email) from public key server + + + + The referenced bundle contains an OSGi-compliant manifest.mf + file. For example, it will verify that the add-on version defined in + your repository.xml matches the version defined in the manifest of + your add-on. + + + + [Important] To ensure your repository is valid, RooBot will + download all defined resources in the repository. To do that, it will + read the uri attribute and perform an HTTP GET request against the + defined URL (after replacing the httppgp:// protocol handler with + http://). Should the download or verification of any of the defined + resources in the respository fail, RooBot will abort the processing of + the entire repository and try again later. + + + + If all tests pass, RooBot will publish your add-on in a publicly + accessible XML registry http://spring-roo-repository.springsource.org/roobot/roobot.xml. + This registry is available to the RooBot client integrated into the Spring + Roo Shell. + + Once you have sent your email to s2-roobot@vmware.com, you + should receive a response from RooBot indicating that the processing of + your repository has started. If successful, you will see your add-on + listed at http://spring-roo-repository.springsource.org/roobot/roobot.xml + within a few hours. If this does not happen, you can visit the RooBot + error log at http://spring-roo-repository.springsource.org/roobot/roobot-log.txt, + which is refreshed every 5 minutes. + + Once RooBot has published your add-on sucessfully, it will + periodically process your repository to verify its ongoing validity. As + part of this periodic processing, it will also automatically pick up new + versions (add-on releases) in your repository.xml. Therefore it should not + be necessary to explicitly notify RooBot of any changes in your + repository. +
    + +
    + Upgrading Spring Roo Add-Ons from 1.0.x to 1.1.0 + + As OSGi is the runtime platform for Roo 1.1.0 onwards, porting addons + from a previous version will require some small tweaks to your code. Here's + a step-by-step guide on what you need to do: + + + + Change packaging of your project to bundle + + As your plugin will result in an OSGi bundle, you need to change + the packaging from jar to bundle. This will + cause the Maven bundle plugin to create the necessary metadata for you + out of the box. + + + + Change the type of the dependencies to bundle + + Similar to the point above, you need to reference dependencies as + bundles. Again, let the Maven bundle plugin do its job. + + + + Sync the build section of your pom with the one provided in the + addon template + + Compare your add-on's original pom.xml with a + pom.xml generated by the addon create + command (see below). This is mostly related to the Maven bundle plugin + as well as the Maven SCR plugin (see next point for details). + + + Creating a Roo addon project + + addon create simple --topLevelPackage com.mycompany.myproject.roo.addon + + The easiest way to do so is simply creating a dummy addon + project using the template and copying the plugin configuration into + your pom. + + + + + Replace @ScopeDevelopment annotations with @Component and + @Service + + Roo uses Apache Felix as OSGi runtime and thus uses + @Component and + @Service annotations in combination + with the Maven SCR plugin + for details see http://felix.apache.org/site/apache-felix-maven-scr-plugin.html + to create descriptors for the OSGi declarative services + infrastructure. + + + Component declaration with Apache Felix annotations + + @Service +@Component +public class MyCommands implements CommandMarker { + + @Reference MyOperations operations; + + // Your code goes here +} + + So every @ScopeDevelopment + annotation you used in your command and operations classes has to be + replaced by @Service and + @Component. If you had injected other + services into your command or operations class, you can use + @Reference to wire them into your + component instance. Note that your class will have to implement at + least one interface under which Felix can publish the component + instance. Check the output of the Maven SCR plugin for errors to see + whether any further tweaks are necessary. + + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/preface.xml b/deployment-support/src/site/docbook/reference/preface.xml new file mode 100644 index 000000000..611b0dbac --- /dev/null +++ b/deployment-support/src/site/docbook/reference/preface.xml @@ -0,0 +1,75 @@ + + + Preface + + I still recall the moment when I realised that I would like to + program. The motivation for me was recognition that creativity with software + is mostly constrained by your imagination and skills, whereas creativity + outside the software world is typically constrained by whatever physical + items you happen to possess. Of course at that early stage I hadn't yet come + across the subtle constraints in my optimistic assessment of software (such + as CPU capabilities, memory, CAP theory etc!), but the key principle that + software was almost boundlessly flexible sparked an interest that continues + to this day. + + Of course, the creativity potential of software implies an abundance + of time, as it is time that is the principal ingredient in building and + maintaining software. Ever since the "castle clock" in 1206 we have been + exploring better ways of programming ever-increasingly sophisticated + computers, and the last decade in particular has seen a surge in new + languages and techniques for doing so. + + Despite this 800 year history of programming, software projects are no + different from other projects in that they are still bound by the project + management triangle: "cost, scope or schedule: pick any two". Professional + software developers grapple with this reality every day, constantly striving + for new tools and techniques that might help them deliver quality software + more quickly. + + While initial delivery remains the key priority for most software + projects, the long-term operational dimensions of that software are even + more critical. The criticality of these operational dimensions is easily + understood given that most software needs to be executed, managed, + maintained and enhanced for many years into the future. Architectural + standards are therefore established to help ensure that software is of high + quality and preferably based on well-understood, vendor-agnostic and + standards-based mainstream engineering approaches. + + There is of course a natural tension between the visibility of initial + delivery and the conservatism typically embodied in architectural standards. + Innovative new approaches often result in greater productivity and in turn + faster project delivery, whereas architectural standards tend to restrict + these new approaches. Furthermore, there is a social dimension in that most + developers focus their time on acquiring knowledge, skills and experience + with those technologies that will realistically be used, and this in turn + further cements the dominance of those technologies in architectural + standards. + + It was within this historical and present-day context that we set out + to build something that would offer both genuine innovation and + architectural desirability. We sought to build something that would deliver + compelling developer productivity without compromising on engineering + integrity or discarding mainstream existing technologies that benefit from + architectural standards approval, excellent tooling and a massive pool of + existing developer knowledge, skills and experience. + + Spring Roo is the modern-day answer to enterprise Java productivity. + It's the normal Java platform you know, but with productivity levels you're + unlikely to have experienced before (at least on Java!). It's simple to + understand and easy to learn. Best of all, you can use Roo without needing + to seek architectural approval, as the resulting applications use nothing + but the mainstream Java technologies you already use. Plus all your existing + Java knowledge, skills and experience are directly applicable when using + Roo, and applications built with Roo enjoy zero CPU or memory overhead at + runtime. + + Thank you for taking the time to explore Spring Roo. We hope that you + enjoy using Roo as much as we've enjoyed creating it. + + Ben Alex, Founder - Spring Roo + diff --git a/deployment-support/src/site/docbook/reference/welcome-architecture.xml b/deployment-support/src/site/docbook/reference/welcome-architecture.xml new file mode 100644 index 000000000..b1c635021 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/welcome-architecture.xml @@ -0,0 +1,1020 @@ + + + Application Architecture + + In this chapter we'll introduce the architecture of Roo-created + projects. In later chapters we'll cover the architecture of Roo + itself. + + This chapter focuses on web applications created by Roo, as opposed to + add-on projects. + +
    + Architectural Overview + + Spring Roo focuses on the development of enterprise applications + written in Java. In the current version of Roo these applications + typically will have a relational database backend, Java Persistence API + (JPA) persistence approach, Spring Framework dependency injection and + transactional management, JUnit tests, a Maven build configuration and + usually a Spring MVC-based front-end that uses JSP for its views. As such + a Roo-based application is like most modern Java-based enterprise + applications. + + While most people will be focusing on developing these Spring + MVC-based web applications, it's important to recognise that Roo does not + impose any restrictions on the sort of Java applications that can be built + with it. Even with Roo 1.0.0 it was easy to build any type of + self-contained application. Some examples of the types of requirements you + can easily address with the current version of Roo include (but are not + limited to): + + + + Listening for messages on a JMS queue and sending replies over + JMS or SMTP (Roo can easily set up JMS message producers, + consumers and SMTP) + + + + Writing a services layer (perhaps annotated with Spring's + @Service stereotype + annotation) and exposing it using a remoting protocol to a rich + client (Spring's remoting + services will help here) + + + + Executing a series of predefined actions against the database, + perhaps in conjunction with Spring's new @Scheduled or @Async timer + annotations + + + + Experimentation with the latest Spring and + AspectJ + features with minimal time investment + + + + One of the major differences between Roo and traditional, + hand-written applications is we don't add layers of abstraction + unnecessarily. Most traditional Java enterprise applications will have a + DAO layer, services layer, domain layer and controller layer. In a typical + Roo application you'll only use an entity layer (which is similar to a + domain layer) and a web layer. As + indicated by the list above, a services layer might be added if + your application requires it, although a DAO layer is extremely rarely added. + We'll look at some of these layering conventions (and the rationale for + them) as we go through the rest of this chapter. +
    + +
    + Critical Technologies + + Two technologies are very important in all Roo projects, those being + AspectJ and Spring. We'll have a look at how Roo-based applications use + these technologies in this section. + +
    + AspectJ + + AspectJ is a powerful and mature aspect oriented programming (AOP) + framework that underpins many large-scale systems. Spring Framework has + offered extensive support for AspectJ since 2004, with Spring 2.0 + adopting AspectJ's pointcut definition language even for expressing + Spring AOP pointcuts. Many of the official Spring projects offer support + for AspectJ or are themselves heavily dependent on it, with several + examples including Spring Security (formerly Acegi Security System for + Spring), Spring Insight, SpringSource tc Server, SpringSource dm Server, + Spring Enterprise and Spring Roo. + + While AspectJ is most commonly known for its aspect oriented + programming (AOP) features such as applying advice at defined pointcuts, + Roo projects use AspectJ's powerful inter-type declaration (ITD) + features. This is where the real magic of Roo comes from, as it allows + us to code generate members (artifacts like methods, fields etc) in a + different compilation unit (i.e. source file) from the normal .java code + you'd write as a developer. Because the generated code is in a separate + file, we can maintain that file's lifecycle and contents completely + independently of whatever you are doing to the .java files. Your .java + files do not need to do anything unnatural like reference the generated + ITD file and the whole process is completely transparent. + + Let's have a look at how ITDs work. In a new directory, type the + following commands and note the console output: + + roo> project --topLevelPackage com.aspectj.rocks +roo> jpa setup --database HYPERSONIC_IN_MEMORY --provider HIBERNATE +roo> entity jpa --class ~.Hello +Created SRC_MAIN_JAVA/com/aspectj/rocks +Created SRC_MAIN_JAVA/com/aspectj/rocks/Hello.java +Created SRC_MAIN_JAVA/com/aspectj/rocks/Hello_Roo_JpaActiveRecord.aj +Created SRC_MAIN_JAVA/com/aspectj/rocks/Hello_Roo_JpaEntity.aj +Created SRC_MAIN_JAVA/com/aspectj/rocks/Hello_Roo_ToString.aj +Created SRC_MAIN_JAVA/com/aspectj/rocks/Hello_Roo_Configurable.aj +roo> field string --fieldName comment +Managed SRC_MAIN_JAVA/com/aspectj/rocks/Hello.java +Managed SRC_MAIN_JAVA/com/aspectj/rocks/Hello_Roo_JavaBean.aj +Managed SRC_MAIN_JAVA/com/aspectj/rocks/Hello_Roo_ToString.aj + + Notice how there is a standard Hello.java file, as + well as a series of Hello_Roo_*.aj files. Any file ending + in *_Roo_*.aj is an AspectJ ITD and will be managed by Roo. + You should not edit these files directly, as Roo will automatically + maintain them (this includes even deleting files that aren't required, + as we'll see shortly). + + The Hello.java is just a normal Java file. It looks + like this: + + package com.aspectj.rocks; + +import org.springframework.roo.addon.javabean.RooJavaBean; +import org.springframework.roo.addon.tostring.RooToString; +import org.springframework.roo.addon.entity.RooJpaActiveRecord; + +@RooJavaBean +@RooToString +@RooJpaActiveRecord +public class Hello { + + private String comment; +} + + As shown, there's very little in the .java file. + There are some annotations, plus of course the field we added. Note that + Roo annotations are always source-level retention, meaning they're not + compiled into your .class file. Also, as per our usability + goals you'll note that Roo annotations also always start with + @Roo* to help you find them with code assist. + + By this stage you're probably wondering what the ITD files look + like. Let's have a look at one of them, + Hello_Roo_ToString.aj: + + package com.aspectj.rocks; + +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +privileged aspect Hello_Roo_ToString { + + public String Hello.toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + +} + + Notice how the ITD is very similar to Java code. The main + differences are that it is declared with "privileged + aspect", plus each member identifies the target type (in this + case it is "Hello.toString", which means add the + "toString" method to the "Hello" type). The + compiler will automatically recognize these ITD files and cause the + correct members to be compiled into Hello.class. We can see + that quite easily by using Java's javap command. All we + need to do is run the compiler and view the resulting class. From the + same directory as you created the project in, enter the following + commands and observe the final output: + + $ mvn compile +$ javap -classpath target/classes/.:target/test-classes/. com.aspectj.rocks.Hello +Compiled from "Hello.java" +public class com.aspectj.rocks.Hello extends java.lang.Object implements org.springframework.beans.factory.aspectj.ConfigurableObject{ + transient javax.persistence.EntityManager entityManager; + public com.aspectj.rocks.Hello(); + public static java.lang.String ajc$get$comment(com.aspectj.rocks.Hello); + public static void ajc$set$comment(com.aspectj.rocks.Hello, java.lang.String); + public static java.lang.Long ajc$get$id(com.aspectj.rocks.Hello); + public static void ajc$set$id(com.aspectj.rocks.Hello, java.lang.Long); + public static java.lang.Integer ajc$get$version(com.aspectj.rocks.Hello); + public static void ajc$set$version(com.aspectj.rocks.Hello, java.lang.Integer); + static {}; + public static long countHelloes(); + public static final javax.persistence.EntityManager entityManager(); + public static java.util.List findAllHelloes(); + public static com.aspectj.rocks.Hello findHello(java.lang.Long); + public static java.util.List findHelloEntries(int, int); + public void flush(); + public java.lang.String getComment(); + public java.lang.Long getId(); + public java.lang.Integer getVersion(); + public com.aspectj.rocks.Hello merge(); + public void persist(); + public void remove(); + public void setComment(java.lang.String); + public void setId(java.lang.Long); + public void setVersion(java.lang.Integer); + public java.lang.String toString(); +} + + While the javap output might look a little daunting + at first, it represents all the members that Roo has added (via AspectJ + ITDs) to the original Hello.java file. Notice there isn't + just the toString method we saw in the earlier ITD, but + we've also made the Hello class implement Spring's + ConfigurableObject interface, provided access to a JPA + EntityManager, included a range of convenient persistence + methods plus even getters and setters. All of these useful features are + automatically maintained in a round-trip compatible manner via the + ITDs. + + A careful reader might be wondering about the long field names + seen for introduced fields. You can see that these field names start + with "ajc$" in the output above. The reason for this is to + avoid name collisions with fields you might have in the + .java file. The good news is that you won't ever need to + deal with this unless you're trying to do something clever with + reflection. It's just something to be aware of for introduced fields in + particular. Note that the names of methods and constructors are never + modified. + + Naturally as a normal Roo user you won't need to worry about the + internals of ITD source code and the resulting .class + files. Roo automatically manages all ITDs for you and you never need + deal with them directly. It's just nice to know how it all works under + the hood (Roo doesn't believe in magic!). The benefit of this ITD + approach is how easily and gracefully Roo can handle code generation for + you. + + To see this in action, go and edit the Hello.java in + your favourite text editor with Roo running. Do something simple like + add a new field. You'll notice the Hello_Roo_ToString.aj + and Hello_Roo_JavaBean.aj files are instantly and + automatically updated by Roo to include your new field. Now go and write + your own toString method in the .java file. + Notice Roo deletes the Hello_Roo_ToString.aj file, as it + detects your toString method should take priority over a + generated toString method. But let's say you want a + generated toString as well, so change the + Hello.java's @RooToString annotation to read + @RooToString(toStringMethod="generatedToString"). Now + you'll notice the Hello_Roo_ToString.aj file is immediately + re-created, but this time it introduces a generatedToString + method instead of the original toString. If you comment out + both fields in Hello.java you'll also see that Roo deletes + both ITDs. You can also see the same effect by quitting the Roo shell, + making any changes you like, then restarting the Roo shell. Upon restart + Roo will automatically perform a scan and discover if it needs to make + any changes. + + Despite the admittedly impressive nature of ITDs, AspectJ is also + pretty good at aspect oriented programming features like pointcuts and + advice! To this end Roo applications also use AspectJ for all other AOP + requirements. It is AspectJ that provides the AOP so that classes are + dependency injected with singletons when instantiated and transactional + services are called as part of method invocations. All Roo applications + are preconfigured to use the Spring Aspects project, which ships as part + of Spring Framework and represents a comprehensive "aspect library" for + AspectJ. +
    + +
    + Spring + + Spring Roo applications all use Spring. By "Spring" we not only + mean Spring Framework, but also the other Spring projects like Spring + Security and Spring Web Flow. Of course, only Spring Framework is + installed into a user project by default and there are fine-grained + commands provided to install each additional Spring project beyond + Spring Framework. + + All Roo applications use Spring Aspects, which was mentioned in + the AspectJ + section and ensures Spring Framework's @Configurable + dependency injection and transactional advice is applied. Furthermore, + Roo applications use Spring's annotation-driven component scanning by + default and also rely on Spring Framework for instantiation and + dependency injection of features such as JPA providers and access to + database connection pools. Many of the optional features that can be + used in Roo applications (like JMS and SMTP messaging) are also built + upon the corresponding Spring Framework dependency injection support and + portable service abstractions. + + Those Roo applications that include a web controller will also + receive Spring Framework 3's MVC features such as its conversion API, + web content negotiation view resolution and REST support. It is possible + (and indeed encouraged) to write your own web Spring MVC controllers in + Roo applications, and you are also free to use alternate page rendering + technologies if you wish (i.e. not just JSP). + + Generally speaking Roo will not modify any Spring-related + configuration or setting file (e.g. properties) unless specifically + requested via a shell command. Roo also ensures that whenever it + creates, modifies or deletes a file it explicitly tells you about this + via a shell message. What this means is you can safely edit your Spring + application context files at any time and without telling Roo. This is + very useful if the default configuration offered by Roo is unsuitable + for your particular application's needs. + + Because Spring projects are so extensively documented, and Roo + just uses Spring features in the normal manner, we'll refrain from + duplicating Spring's documentation in this section. Instead please refer + to the excellent Spring documentation for guidance, which can be found + in the downloadable distribution files and also on the Spring web + site. +
    +
    + +
    + Entity Layer + + When people use Roo, they will typically start a new project using + the steps detailed in the Beginning With Roo: + The Tutorial chapter. That is, they'll start by creating the + project, installing some sort of persistence system, and then beginning to + create entities and add fields to them. As such, entities and fields + represent the first point in a Roo project that you will be expressing + your problem domain. + + The role of an entity in your Roo-based application is to model the + persistent "domain layer" of your system. As such, a domain object is + specific to your problem domain but an entity is a special form of a + domain object that is stored in the database. By default a single entity + will map to a single table in your database, and a single field within + your entity class will map to a single column within the corresponding + table. However, like most things in Roo this is easily customised using + the relevant standard (in this case, JPA annotations). Indeed most of the + common customisation options (like specifying a custom column or table + name etc) can be expressed directly in the relevant Roo command, freeing + you from even needing to know which annotation(s) should be used. + + Let's consider a simple entity that has been created using the entity jpa command and + following it with a single field command: + + package com.springsource.vote.domain; + +import org.springframework.roo.addon.javabean.RooJavaBean; +import org.springframework.roo.addon.tostring.RooToString; +import org.springframework.roo.addon.entity.RooJpaActiveRecord; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@RooJavaBean +@RooToString +@RooJpaActiveRecord +public class Choice { + + @NotNull + @Size(min = 1, max = 30) + private String namingChoice; + + @Size(max = 80) + private String description; +} + + The above entity is simply a JPA entity that contains two fields. + The two fields are annotated with JavaBean Validation API (JSR 303) + annotations, which are useful if your JPA provider supports this standard + (as is the case if you nominate Hibernate as your JPA provider) or you are + using a Roo-scaffolded web application front end (in which case Roo will + use Spring Framework 3's JSR 303 support). Of course you do not need to + use the JavaBean Validation API annotations at all, but if you would like + to use them the relevant Roo field commands provide tab-completion + compatible options for each. The first time you use one of these Roo field + commands, Roo will add required JavaBean Validation API libraries to your + project (i.e. these libraries will not be in your project until you decide + to first use JavaBean Validation). + + What's interesting about the above entity is what you can actually + do with it. There are a series of methods automatically added into the + Choice.class courtesy of Roo code-generated and maintained + AspectJ ITDs. These include static methods for retrieving instances of + Choice, JPA facade methods for persisting, removing, merging and flushing + the entity, plus accessors and mutators for both the identifier and + version properties. You can fine-tune these settings by modifying + attributes on the @RooJpaActiveRecord annotation. You can + also have Roo remove these services by simply removing the + @RooJpaActiveRecord annotation from the class, in which case + you'll be left with a normal JPA @Entity that you'll need to manage by + hand (e.g. provide your own persistence methods, identifier, version + etc). + + The @RooJavaBean annotation causes an accessor and + mutator (getter and setter) to automatically be generated for each field + in the class. These accessors and mutators are automatically maintained in + an AspectJ ITD by Roo. If you write your own accessor or mutator in the + normal .java file, Roo will automatically remove the corresponding + generated method from the ITD. You can also remove the + @RooJavaBean annotation if you don't want any generated + accessors or mutators (although those related to the version and + identifier fields will remain, as they are associated with + @RooJpaActiveRecord instead of + @RooJavaBean). + + Finally, the @RooToString annotation causes Roo to + create and maintain a public String toString() method in a + separate ITD. This method currently is used by any scaffolded web + controllers if they need to display a related entity. The generated method + takes care to avoid circular references that are commonly seen in + bidirectional relationships involving collections. The method also formats + Java Calendar objects in an attractive manner. As always, you + can write your own toString() method by hand and Roo will + automatically remove its generated toString() method, even if + you still have the @RooToString annotation present. You can + of course also remove the @RooToString annotation if you no + longer wish to have a generated toString() method. + + Before leaving this discussion on entities, it's worth mentioning + that you are free to create your own entity .java classes by + hand. You do not need to use the Roo shell commands to create entities or + maintain their fields - just use any IDE. Also, you are free to use the + @RooToString or @RooJavaBean (or both) + annotations on any class you like. This is especially useful if you have a + number of domain objects that are not persisted and are therefore not + entities. Roo can still help you with those objects. +
    + +
    + Web Layer + + Roo 1.0 can optionally provide a scaffolded Spring MVC web layer. + The scaffolded MVC web layer features are explored in some depth in the + Beginning With Roo: The Tutorial chapter, + including how to customise the appearance. From an architectural + perspective, the scaffolded layer includes a number of URL rewriting rules + to ensure requests can be made in accordance with REST conventions. Roo's + scaffolding model also includes Apache Tiles, Spring JavaScript, plus + ensures easy setup of Spring Security with a single command. + + In Spring Roo 1.1 we also added comprehensive support for Google Web + Toolkit (GWT). This allows you to build Generation IV web HTML5-based web + front-ends. These front-ends access the Spring backend using highly + optimized remoting protocols, and the GWT application represents the GWT + team's recommended best practice architecture. In fact, the GWT team at + Google wrote most of the Roo GWT add-on, so you can be sure it uses the + best GWT 2.1 features. + + Scaffolded web controllers always delegate directly to methods + provided on an @RooJpaActiveRecord class. For maximum + compatibility with scaffolded controllers, it is recommended to observe + the default identifier and version conventions provided by + @RooJpaActiveRecord implementations. If you write a web + controller by hand (perhaps with the assistance of the web mvc controller + command), it is recommended you also use the methods directly exposed on + entities. Most Roo applications will place their business logic between + the entities and web controllers, with only occasional use of services + layers. Please refer to the services + layer section for a more complete treatment of when you'd use a + services layer. +
    + +
    + Optional Services Layer + + As discussed at the start of this chapter, web applications are the + most common type of application created with Roo 1.0.0. A web application + will rarely require a services layer, as most logic + can be placed in the web controller handle methods and the remainder in + entity methods. Still, a services layer makes sense in specific scenarios + such as: + + + + There is business logic that spans multiple entities and that + logic does not naturally belong in a specific entity + + + + You need to invoke business logic outside the scope of a natural + web request (e.g. a timer task) + + + + Remote client access is required and it is therefore more + convenient to simply expose the methods via a remoting protocol + + + + An architectural policy requires the use of a services + layer + + + + A higher level of cohesion is sought in the web layer, with the + web layer solely responsible for HTTP-related management and the + services layer solely responsible for business logic + + + + A greater level of testing is desired, which is generally easier + to mock than simulating web requests + + + + it is preferred to place transactional boundaries and security + authorization metadata on the services layer (as opposed to a web + controller) + + + + As shown, there are a large number of reasons why services layers + remain valuable. However, Roo does not code generate services layers + because they are not strictly essential to building a normal web + application and Roo achieves separation of concern via its AspectJ + ITD-based architecture. + + If you would like to use a services layer, since release 1.2.0 Roo + offers automatic service layer integration for your application. Please + refer to the service layer section in + the application layering chapter for + further details. +
    + +
    + Goodbye DAOs + + One change many existing JEE developers will notice when using + Roo-based applications is that there is no DAO layer (or "Repository" + layer). As with the services + layer, we have removed the DAO layer because it is not strictly + essential to creating the typical web applications that most people are + trying to build. + + If we reflect for a moment on the main motivations for DAOs, it is + easy to see why these are not applicable in Roo applications: + + + + Testing: In a normal application a DAO + provides an interface that could be easily stubbed as part of unit + testing. The interesting point about testing is that most people use + mocking instead of stubbing in modern applications, making it + attractive to simply mock the persistence method or two that you + actually require for a test (rather than the crudeness of stubbing + an entire DAO interface). In Roo-based applications you simply mock + the persistence-related methods that have been introduced to the + entity. You can use normal mocking approaches for the instance + methods on the Roo entity, and use Spring Aspect's + @MockStaticEntityMethods support for the static finder + methods. + + + + Separation of concern: One reason for + having a DAO layer is that it allows a higher cohesion + object-oriented design to be pursued. The high cohesion equates to a + separation of concern that reduces the conceptual weight of + implementing the system. In a Roo-based application separation of + concern is achieved via the separate ITDs. The conceptual weight is + also reduced because Roo handles the persistence methods rather than + force the programmer to deal with them. Therefore separation of + concern still exists in a Roo application without the requirement + for a DAO layer. + + + + Pluggable implementations: A further + benefit of DAOs is they simplify the switching from one persistence + library to another. In modern applications this level of API + abstraction is provided via JPA. As Roo uses JPA in its generated + methods, the ability to plug in an alternate implementation is + already fully supported despite there being no formal DAO layer. You + can see this yourself by issuing the jpa setup command and + specifying alternate implementations. + + + + Non-JPA persistence: It is possible that + certain entities are stored using a technology that does not have a + JPA provider. In this case Roo does not support those entities out + of the box. However, if only a small number of entities are affected + by this consideration there is no reason one or more hand-written + ITDs could not be provided by the user in order to maintain + conceptual parity with the remainder of the Roo application (which + probably does have some JPA). If a large number of entities are + affected, the project would probably benefit from the user writing a + Roo add-on which will automatically manage the ITDs just as Roo does + for JPA. + + + + Security authorisation: Sometimes DAOs + are used to apply security authorisation rules. It is possible to + protect persistence methods on the DAOs and therefore go relatively + low in the control flow to protecting the accessibility of entities. + In practice this rarely works well, though, as most authorisation + workflows will target a use case as opposed to the entities required + to implement a use case. Further, the approach is unsafe as it is + possible to transitively acquire one entity from another without + observing the authorisation rules (e.g. + person.getPartner().getChildren().get(1).setFirstName("Ben")). + It is also quite crude in that it does not support transparent + persistence correctly, in that the example modification of the first + name would flush to the database without any authorisation check + (assuming this mutative operation occurred within the context of a + standard transactional unit of work). While it's possible to work + around many of these issues, authorisation is far better tackled + using other techniques than the DAO layer. + + + + Security auditing: In a similar argument + to authorisation, sometimes DAOs are advocated for auditing + purposes. For the same types of reasons expressed for authorisation, + this is a suboptimal approach. A better way is to use AOP (e.g. + AspectJ field set pointcuts), a JPA flush event handle, or a + trigger-like model within the database. + + + + Finders: If you review existing DAOs, + you'll find the main difference from one to another is the finder + methods they expose. Dynamic finders are automatically supported by + Roo and introduced directly to the entity, relieving the user from + needing DAOs for this reason. Furthermore, it is quite easy to + hand-write a finder within the entity (or an ITD that adds the + finder to the entity if a separate compilation unit is + desired). + + + + Architectural reasons: Often people + express a preference for a DAO because they've always done it that + way. While maintaining a proven existing approach is generally + desirable, adopting Roo for an application diminishes the value of a + DAO layer to such an extent that it leaves little (if any) + engineering-related reasons to preserve it. + + + + It's also worth observing that most modern RAD frameworks avoid DAO + layers and add persistence methods directly to entities. If you compare + similar technologies to Roo, you will see this avoidance of a DAO layer is + commonplace, mainstream and does not cause problems. + + Naturally you can still write DAOs by hand if you want to, but the + majority of Roo add-ons will not be compatible with such DAOs. As such you + will not receive automated testing or MVC controllers that understand your + hand-written DAOs. Our advice is therefore not to hand write DAOs. Simply + use the entity methods provided by @RooJpaActiveRecord, as + it's engineering-wise desirable and it's also far less effort for you to + write and maintain. + + If you are interested in DAO support despite the above Roo offers + support for different repository layers as of release 1.2.0. Please refer + to the application layering chapter for + details. +
    + +
    + Maven + +
    + Packaging + + Roo supports a number of Maven packaging types out of the box, + such as jar, war, pom, and + bundle. These are provided via Roo's + PackagingProvider interface. If you wish to customise the + POMs or other artifacts that Roo generates for a given packaging type + when creating a project or module, either for one of the above packaging + types or a completely different one, you can implement your own + PackagingProvider that creates exactly the files you want + with the contents you want. The procedure for doing this is as follows: + + + In a new directory, start Roo and run "addon create simple" + to create a simple addon. + + + + Delete: + + + + the four .java files created in + src/main/java + + + + the two .tagx files created in + src/main/resources + + + + + + Create your custom packaging class (e.g. + MyPackaging.java) in your preferred package. + + + + Pick a unique ID for the Roo shell to use when referring to + your PackagingProvider (e.g. "custom-jar"). Do not use any of the + core Maven packaging type names, as these are reserved for use by + Roo. + + + + Make your packaging class implement the + o.s.r.project.packaging.PackagingProvider interface, + either by: + + + + Implementing PackagingProvider directly, + with full control over (but no assistance with) artifact + generation, or + + + + Extending + o.s.r.project.packaging.AbstractPackagingProvider + to have Roo create the POM from a template you specify, with + various substitutions made automatically (e.g. groupId and + artifactId). This approach requires you to: + + + + Create your custom POM template in + src/main/resources plus whatever package you + chose above. + + + + Create a public no-arg constructor that calls the + AbstractPackagingProvider constructor with + the following arguments: + + + + The unique ID of your custom packaging type (see + above). + + + + The Maven name of your packaging type (typically + jar/war/ear/etc, but could be something else if you've + extended Maven to support custom packaging + types). + + + + The path to your POM template relative to your + concrete PackagingProvider (e.g. + "my-pom-template.xml" if it's in the same package). + Note that this POM can contain as much or as little + content as you like, with the following + caveats: + + + + It must have the standard Maven "project" root + element with all the usual namespace details. + + + + If you extend + AbstractPackagingProvider, that class + will ensure that the POM's coordinates can be + resolved either from a "parent" element or from + explicit "groupId", "artifactId", and "version" + elements. + + + + + + + + + + + + Add the Felix annotations @Component and @Service to your + concrete PackagingProvider, so that it's detected by Roo's + PackagingProviderRegistry. + + + + Build and install the addon in the usual way, i.e.: + + + + Run "mvn install" in the addon directory to + create the OSGi bundle. + + + + Change to the directory of the project that will be + using the custom packaging provider. + + + + Run "osgi start --url + file:///path/to/addon/project/target/com.example.foo-0.1.0.BUILD-SNAPSHOT.jar" + + + + Run "osgi scr list"; your custom + PackagingProvider component should appear somewhere in the + list. + + + + + + Whenever you run the "project" or "module create" commands, + your custom PackagingProvider's ID should appear in the list of + possible completions for the "--packaging" option + + +
    + +
    + Multi-Module Support + + Since version 1.2.0, Roo supports multi-module + Maven projects, i.e. those containing multiple projects in a + nested directory structure, each with their own POM. The non-leaf POMs + have "pom" packaging and the leaf POMs usually have an artifact creation + packaging (jar, war, etc). If you're not familiar with multi-module + projects and want to see how they're structured, there's an embedded + multimodule.roo script that generates a simple multi-module + project; used as follows: + + + + At your operating system prompt, type "roo script + multimodule.roo". + + + + Change into the "ui/mvc"" directory. + + + + Run "mvn tomcat:run" or "mvn + jetty:run". + + + + Point your browser to + http://localhost:8080/mvc. + + + + The rest of this section assumes that you are familiar with + multi-module projects, in particular the difference between POM + inheritance (one POM has another as its parent) and project nesting (one + project is in a sub-directory of another, i.e. is a module of that + parent project). + +
    + Features + + Roo's multi-module support has the following features (a formal + list of Roo's Maven-related commands appears in Appendix C): + + + Roo now has the concept of a module, which in practice + means a directory tree whose root contains a Maven POM. A + project consists of zero or more modules. When you run Roo from + the operating system prompt, you do so from the directory of the + root module. + + Once any modules exist, one of them always has the + "focus", in other words will be used as the context for any + shell commands that interact with the user project (as opposed + to housekeeping commands such as "osgi ps"). For + example, running the "web flow" command will add + Spring Web Flow support to the currently focused module. + + + + The "module focus" command, available once + the project contains more than one module, changes the currently + focused module. Tab completion is available, with the module + name "~" signifying the root module. + + + + The "module create" command creates a new + module as a sub-directory of the currently focused module. The + latter module's POM will be updated to ensure it has "pom" + packaging, allowing the Maven reactor to properly recurse the + module tree at build time. Note that the newly created POM will + by default not inherit from the parent + module's POM. If the new module's POM should have a parent, + specify it via the "module create" command's + optional "parent" parameter. The parent POM need + not be located within the user project. A typical use case is + that a development team might have a standard base POM from + which all their projects inherit, or a standard web POM from + which all their web modules inherit. As with the + "project" command, the new module's Maven packaging + can be specified via the optional "packaging" + parameter. Custom packaging behaviour is supported, as described + above. + + +
    + +
    + Limitations + + Roo's multi-module support has the following limitations: + + + Limited automatic creation of dependencies between + modules. If your project needs any inter-module dependencies + beyond those added by Roo, simply create them using the "dependency + add" command. + + + + No command for removing a module; this is in line with the + absence of commands for removing other project artifacts such as + classes, enums, JSPs, and POMs. In any event, it's simple enough + to do manually; just delete the directory, delete the relevant + "<module>" element from the parent module's + POM, and delete the module as a dependency from any other + modules' POMs. + + + + One area where there's considerable scope for improvement + is in the management of dependencies in general. In an ideal + Maven project, dependency information in the form of both + "dependencyManagement" entries and live + "dependency" elements themselves would be pushed as + far up the POM inheritance hierarchy as possible, in order to + minimise duplication and reduce the incidence of version + conflicts. As it stands, Roo adds and removes dependencies to + and from the currently focused module in response to shell + commands, regardless of what dependencies are in effect for + other modules in the project. + + + + Likewise, plugin management is currently quite basic. Roo + adds/removes plugins to the POM of the currently focused module + with no attempt to rationalise them in concert with the POMs of + other modules (for example, two Spring MVC modules will + independently have the Jetty plugin declared in their own POMs + rather than having this plugin declared in the lowest common + ancestor POM). As with dependencies (see above), this is an area + in which Roo could conceivably take some of the load off + developers. + + + + There's no Roo command for changing a module’s packaging + between two arbitrary values, as this could require too many + other changes to the user’s project. However, Roo does change a + module's packaging in two specific circumstances: + + + + Adding a module to the currently focused module + changes the latter's packaging to "pom", as described above + under the "module create" command. + + + + Adding web support to a module changes its packaging + to "war". + + + + + + Roo does not create any parent-child relationships between + different modules’ Spring application contexts; the user can + always create these relationships manually, and Roo will not + remove them. + + +
    +
    +
    +
    diff --git a/deployment-support/src/site/docbook/reference/welcome-beginning.xml b/deployment-support/src/site/docbook/reference/welcome-beginning.xml new file mode 100644 index 000000000..b6d6d6561 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/welcome-beginning.xml @@ -0,0 +1,950 @@ + + + Beginning With Roo: The Tutorial + + In this chapter we'll build an app step-by-step together in a + relatively fast manner so that you can see how to typically use Roo in a + normal project. We'll leave detailed features and side-steps to other + sections of this manual. + +
    + What You'll Learn + + In this tutorial you will learn to create a complete Web application + from scratch using Roo. The application we are going to develop will + demonstrate many of the core features offered by Roo. In particular you + will learn how to use the Roo shell for: + + + + project creation + + + + creation and development of domain objects (JPA entities) + + + + adding fields of different types to the domain objects + + + + creating relationships between domain objects + + + + automatic creation of integration tests + + + + creating workspace artifacts to import the project into your + IDE + + + + automatic scaffolding of a Web tier + + + + running the application in a Web container + + + + controlling and securing access to different views in the + application + + + + customizing the look and feel of the Web UI for our business + domain + + + + creating and running Selenium tests + + + + deployment and backup of your application + + +
    + +
    + Alternative Tutorial: The Wedding RSVP Application + + In addition to the tutorial in this chapter, we've published a + separate step-by-step tutorial in the form of a blog entry. This blog + entry covers the process of building a wedding RSVP application. It is + kept updated to reflect the current major version of Roo, and features a + number of interesting Roo capabilities: + + + + Standard MVC web application with JPA entities etc + + + + Spring Security usage, including login page customisation + + + + Sending emails via SMTP + + + + Testing both via JUnit and Selenium + + + + Usage with Eclipse + + + + Creating a WAR for deployment + + + + You can find the wedding tutorial at http://blog.springsource.com/2009/05/27/roo-part-2/. +
    + +
    + Tutorial Application Details + + To demonstrate the development of an application using Spring Roo we + will create a Web site for a Pizza Shop. The requirements for the Roo + Pizza Shop application include the ability to create new Pizza types by + the staff of the Roo Pizza Shop. A pizza is composed of a base and one or + more toppings. Furthermore, the shop owner would like to allow online + orders of Pizzas by his customers for delivery. + + After this short discussion with the Pizza Shop owner, we have + created a simple class diagram for the initial domain model: + + + + + + + + + + While this class diagram represents a simplified model of the + problem domain for the pizza shop problem domain, it is a good starting + point for the project at hand in order to deliver a first prototype of the + application to the Pizza Shop owner. Later tutorials will expand this + domain model to demonstrate more advanced features of Spring Roo. +
    + +
    + Step 1: Starting a Typical Project + + Now that we have spoken with our client (the Pizza Shop owner) to + gather the first ideas and requirements for the project we can get started + with the development of the project. After installing a JDK, Spring Roo and Maven, we create a new directory for + our project: + + > mkdir pizza +> cd pizza +pizza> + + Next, we start Spring Roo and type 'hint' to obtain + context-sensitive guidance from the Roo shell:pizza> roo + ____ ____ ____ + / __ \/ __ \/ __ \ + / /_/ / / / / / / / + / _, _/ /_/ / /_/ / +/_/ |_|\____/\____/ 1.2.1.RELEASE [rev 6eae723] + + +Welcome to Spring Roo. For assistance press TAB or type "hint" then hit ENTER. +roo> +roo> hint +Welcome to Roo! We hope you enjoy your stay! + +Before you can use many features of Roo, you need to start a new project. + +To do this, type 'project' (without the quotes) and then hit TAB. + +Enter a --topLevelPackage like 'com.mycompany.projectname' (no quotes). +When you've finished completing your --topLevelPackage, press ENTER. +Your new project will then be created in the current working directory. + +Note that Roo frequently allows the use of TAB, so press TAB regularly. +Once your project is created, type 'hint' and ENTER for the next suggestion. +You're also welcome to visit http://forum.springframework.org for Roo help. +roo> +There are quite a few usability features within the Roo + shell. After typing hint you may have + noticed that this command guides you in a step-by-step style towards the + completion of your first project. Or if you type + help you will see a + list of all commands available to you in the particular context you are + in. In our case we have not created a new project yet so the help command + only reveals higher level commands which are available to you at this + stage. To create an actual project we can use the project command: + + roo> project --topLevelPackage com.springsource.roo.pizzashop +Created ROOT/pom.xml +Created SRC_MAIN_RESOURCES +Created SRC_MAIN_RESOURCES/log4j.properties +Created SPRING_CONFIG_ROOT +Created SPRING_CONFIG_ROOT/applicationContext.xml +com.springsource.roo.pizzashop roo> +When you used the project command, Roo created you a + Maven pom.xml file as well as a Maven-style directory + structure. The top level package you nominated in this command was then + used as the <groupId> within the pom.xml. + When typing later Roo commands, you can use the "~" shortcut + key to refer to this top-level-package (it is read in by the Roo shell + from the pom.xml each time you load Roo). + + The following folder structure now exists in your file + system: + + + + + + + + + + For those familiar with Maven you will notice that + this folder structure follows standard Maven conventions by creating + separate folders for your main project resources and tests. Roo also + installs a default application context and a log4j configuration for you. + Finally, the project pom file contains all required dependencies and + configurations to get started with our Pizza Shop project. + + Once the project structure is created by Roo you can go ahead and + install a persistence configuration for your application. Roo leverages + the Java Persistence API (JPA) which provides a convenient abstraction to + achieve object-relational mapping. JPA takes care of mappings between your + persistent domain objects (entities) and their underlying database tables. + To install or change the persistence configuration in your project you can + use the jpa + setup command (note: try using the <TAB> as often as you can to auto-complete + your commands, options and even obtain contextual help): + + com.springsource.roo.pizzashop roo> hint +Roo requires the installation of a persistence configuration, +for example, JPA or MongoDB. + +For JPA, type 'jpa setup' and then hit TAB three times. +We suggest you type 'H' then TAB to complete "HIBERNATE". +After the --provider, press TAB twice for database choices. +For testing purposes, type (or TAB) HYPERSONIC_IN_MEMORY. +If you press TAB again, you'll see there are no more options. +As such, you're ready to press ENTER to execute the command. + +Once JPA is installed, type 'hint' and ENTER for the next suggestion. + +Similarly, for MongoDB persistence, type 'mongo setup' and ENTER. +com.springsource.roo.pizzashop roo> +com.springsource.roo.pizzashop roo> jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY +Created SPRING_CONFIG_ROOT/database.properties +Updated SPRING_CONFIG_ROOT/applicationContext.xml +Created SRC_MAIN_RESOURCES/META-INF/persistence.xml +Updated ROOT/pom.xml [added dependencies org.hsqldb:hsqldb:1.8.0.10, org.hibernate:hibernate-core:3.6.9.Final, +org.hibernate:hibernate-entitymanager:3.6.9.Final, org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final, +org.hibernate:hibernate-validator:4.2.0.Final, javax.validation:validation-api:1.0.0.GA, cglib:cglib-nodep:2.2.2, +javax.transaction:jta:1.1, org.springframework:spring-jdbc:${spring.version}, +org.springframework:spring-orm:${spring.version}, commons-pool:commons-pool:1.5.6, commons-dbcp:commons-dbcp:1.3] +com.springsource.roo.pizzashop roo> +So in this case we have installed Hibernate as the + object-relational mapping (ORM)-provider. Hibernate is one of ORM + providers which Roo currently offers. EclipseLink, OpenJPA, and + DataNucleus represent the alternative choices. In a similar fashion we + have chosen the Hypersonic in-memory database as our target database. + Hypersonic is a convenient database for Roo application development + because it relieves the developer from having to install and configure a + production scale database. + + When you are ready to test or install your application in a + production setting, the jpa setup command can + be repeated. This allows you to nominate a different database, or even + ORM. Roo offers TAB completion for production databases including + Postgres, MySQL, Microsoft SQL Server, Oracle, DB2, Sybase, H2, Hypersonic + and more. Another important step is to edit the + SRC_MAIN_RESOURCES/META-INF/persistence.xml file and modify + your JPA provider's DDL (schema management) configuration setting so it + preserves the database between restarts of your application. To help you + with this, Roo automatically lists the valid settings for your JPA + provider as a comment in that file. Note that by default your JPA provider + will drop all database tables each time it reloads. As such you'll + certainly want to change this setting. + + Please note: The Oracle and DB2 JDBC drivers are not available in + public maven repositories. Roo will install standard dependencies for + these drivers (if selected) but you may need to adjust the version number + or package name according to your database version. You can use the + following maven command to install your driver into your local maven + repository: mvn install:install-file -DgroupId=com.oracle + -DartifactId=ojdbc14 -Dversion=10.2.0.2 -Dpackaging=jar + -Dfile=/path/to/file (example for the Oracle driver) +
    + +
    + Step 2: Creating Entities and Fields + + Now it is time to create our domain objects and fields which we have + identified in our class diagram. First, we can use the entity + jpa command to create the actual domain object. The + entity jpa command has a number of optional attributes and one + required attribute which is --class. In addition to the + required --class attribute we use the + --testAutomatically attribute which conveniently creates + integration tests for a domain object. So let's start with the + Topping domain object: + + com.springsource.roo.pizzashop roo> hint +You can create entities either via Roo or your IDE. +Using the Roo shell is fast and easy, especially thanks to the TAB completion. + +Start by typing 'ent' and then hitting TAB twice. +Enter the --class in the form '~.domain.MyEntityClassName' +In Roo, '~' means the --topLevelPackage you specified via 'create project'. + +After specify a --class argument, press SPACE then TAB. Note nothing appears. +Because nothing appears, it means you've entered all mandatory arguments. +However, optional arguments do exist for this command (and most others in Roo). +To see the optional arguments, type '--' and then hit TAB. Mostly you won't +need any optional arguments, but let's select the --testAutomatically option +and hit ENTER. You can always use this approach to view optional arguments. + +After creating an entity, use 'hint' for the next suggestion. +com.springsource.roo.pizzashop roo> +com.springsource.roo.pizzashop roo> entity jpa --class ~.domain.Topping --testAutomatically +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping.java +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingDataOnDemand.java +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingIntegrationTest.java +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping_Roo_Configurable.aj +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping_Roo_ToString.aj +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping_Roo_Jpa_Entity.aj +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping_Roo_Jpa_ActiveRecord.aj +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingDataOnDemand_Roo_Configurable.aj +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingDataOnDemand_Roo_DataOnDemand.aj +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingIntegrationTest_Roo_Configurable.aj +Created SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingIntegrationTest_Roo_IntegrationTest.aj +You will notice that besides the creation of Java and AspectJ + sources, the entity jpa command in + the Roo shell takes care of creating the appropriate folder structure in + your project for the top level package you defined earlier. You will + notice that we used the '~' character as + a placeholder for the project's top level package. While this serves a + convenience to abbreviate long commands, you can also tab-complete the + full top level package in the Roo shell. + + As a next step we need to add the 'name' field to our + Topping domain class. This can be achieved by using the + field command as + follows: + + ~.domain.Topping roo> hint +You can add fields to your entities using either Roo or your IDE. + +To add a new field, type 'field' and then hit TAB. Be sure to select +your entity and provide a legal Java field name. Use TAB to find an entity +name, and '~' to refer to the top level package. Also remember to use TAB +to access each mandatory argument for the command. + +After completing the mandatory arguments, press SPACE, type '--' and then TAB. +The optional arguments shown reflect official JSR 303 Validation constraints. +Feel free to use an optional argument, or delete '--' and hit ENTER. + +If creating multiple fields, use the UP arrow to access command history. + +After adding your fields, type 'hint' for the next suggestion. +To learn about setting up many-to-one fields, type 'hint relationships'. +~.domain.Topping roo> +~.domain.Topping roo> field string --fieldName name --notNull --sizeMin 2 +Updated SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping.java +Updated SRC_TEST_JAVA/com/springsource/roo/pizzashop/domain/ToppingDataOnDemand_Roo_DataOnDemand.aj +Created SRC_MAIN_JAVA/com/springsource/roo/pizzashop/domain/Topping_Roo_JavaBean.aj +As explained in the documentation by typing the hint command you + can easily add constraints to your fields by using optional attributes + such as --notNull and --sizeMin 2. These + attributes result in standards-compliant JSR-303 + annotations which Roo will add to your field definition in your Java + sources. You will also notice that the Roo shell is aware of the current + context within which you are using the field command. It + knows that you have just created a Topping entity and therefore assumes + that the field command should be applied to the Topping Java source. Roo's + current context is visible in the shell prompt. + + If you wish to add the field to a different target type you can + specify the --class attribute as part of the field command + which then allows you to tab complete to any type in your project. + + As a next step you can create the Base and the + Pizza domain object in a similar fashion by issuing the + following commands (shell output omitted): + + entity jpa --class ~.domain.Base --testAutomatically +field string --fieldName name --notNull --sizeMin 2 +entity jpa --class ~.domain.Pizza --testAutomatically +field string --fieldName name --notNull --sizeMin 2 +field number --fieldName price --type java.lang.Float After + adding the name and the price field to the Pizza domain class + we need to deal with its relationships to Base and + Topping. Let's start with the m:m (one Pizza can + have many Toppings and one Topping can be + applied to many Pizzas) relationship between + Pizza and Toppings. To create such many-to-many + relationships Roo offers the field set + command: + + ~.domain.Pizza roo> field set --fieldName toppings --type ~.domain.ToppingAs + you can see it is easy to define this relationship even without knowing + about the exact JPA annotations needed to create this mapping in our + Pizza domain entity. In a similar way you can define the m:1 + relationship between the Pizza and Base domain + entities by using the field reference + command: + + ~.domain.Pizza roo> field reference --fieldName base --type ~.domain.BaseIn + a similar fashion we can then continue to create the + PizzaOrder domain object and add its fields by leveraging the + field date and + field + number commands: + + entity jpa --class ~.domain.PizzaOrder --testAutomatically +field string --fieldName name --notNull --sizeMin 2 +field string --fieldName address --sizeMax 30 +field number --fieldName total --type java.lang.Float +field date --fieldName deliveryDate --type java.util.Date +field set --fieldName pizzas --type ~.domain.Pizza + + + This concludes this step since the initial version of the domain + model is now complete. +
    + +
    + Step 3: Integration Tests + + Once you are done with creating the first iteration of your domain + model you naturally want to see if it works. Luckily we have instructed + Roo to create integration tests for our domain objects all along. Hint: if + you have not created any integration tests while developing your domain + model you can still easily create them using the test + integration command. Once your tests are in place it is + time to run them using the perform tests + command: + + ~.domain.PizzaOrder roo> perform tests +... +------------------------------------------------------- + T E S T S +------------------------------------------------------- + +Tests run: 36, Failures: 0, Errors: 0, Skipped: 0 + +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 3.860s +[INFO] Finished at: Tue Feb 14 18:01:45 EST 2012 +[INFO] Final Memory: 6M/81M +[INFO] ------------------------------------------------------------------------ + + + As you can see Roo has issued a Maven command (equivalent to running + 'mvn test' outside the Roo shell) in order to execute the + integration tests. All tests have passed, Roo has generated 9 integration + tests per domain object resulting in a total of 36 integration tests for + all 4 domain objects. +
    + +
    + Step 4: Using Your IDE + + Of course Roo projects can be used in your favorite IDE. We + recommend the use of SpringSource Tool + Suite (STS), which is available at no charge from SpringSource. If + you're not using SpringSource Tool Suite, please refer to the IDE usage section of this reference guide for a + more detailed discussion of IDE interoperability. + + By default Roo projects do not contain any IDE-specific workspace + configuration artifacts. This means your IDE won't be able to import your + Pizza Shop project by default. The Roo shell can help us create + IDE-specific workspace configuration artifacts by means of the perform + eclipse command. However, you should not use this + command if you have the m2eclipse plugin installed. If you're an STS user, + you have the m2eclipse plugin installed and as such you can skip the + "perform eclipse" command. All people not using STS or m2eclipse should + use the following command: + + ~.domain.PizzaOrder roo> perform eclipse +... +[INFO] Adding support for WTP version 2.0. +[INFO] Using Eclipse Workspace: null +[INFO] Adding default classpath container: org.eclipse.jdt.launching.JRE_CONTAINER +[INFO] Wrote settings to /Users/stewarta/projects/roo-test/pizzashop/.settings/org.eclipse.jdt.core.prefs +[INFO] Wrote Eclipse project for "pizzashop" to /Users/stewarta/projects/roo-test/pizzashop. +[INFO] n.PizzaOrder roo> + Javadoc for some artifacts is not available. + Please run the same goal with the -DdownloadJavadocs=true parameter in order to check remote repositories for javadoc. + List of artifacts without a javadoc archive: + o org.springframework.roo:org.springframework.roo.annotations:1.2.1.RELEASE +... +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 1.685s +[INFO] Finished at: Tue Feb 14 18:04:20 EST 2012 +[INFO] Final Memory: 7M/81M +[INFO] ------------------------------------------------------------------------ +Note, when issuing this command for the first time you can + expect delays while Maven downloads the dependencies and their sources + into your local repository. Once this command has completed you are ready + to import your project into STS by clicking 'File > Import > General + > Existing Projects into Workspace'. Once your project is imported into + STS you can take a look at the Java sources. For example you can run the + included JUnit tests by right clicking the pizzashop project and then + selecting 'Run As > JUnit Test'. + + If you're using STS or have installed m2eclipse into an + Eclipse-based IDE, as mentioned earlier you can skip the perform + eclipse command entirely. In this case you simply need + to select in STS/Eclipse the 'File > Import > General > Maven + Projects' menu option. + + As detailed in the Application + Architecture chapter of this documentation Roo projects leverage + AspectJ Intertype declarations extensively. This does not, however, affect + your ability to use code completion features offered by STS. To see code + completion working in action you can open an existing integration test and + use the testMarkerMethod() method to test it. For example you + can open the BaseIntegrationTest.java source file and try it + out: + + + + + + + + Note, most of the methods visible in the STS code assist are + actually not in the Java sources but rather part of the AspectJ ITD and + are therefore introduced into the Java bytecode at compile time. +
    + +
    + Step 5: Creating A Web Tier + + As a next step we want to scaffold a Web tier for the Pizza Shop + application. This is accomplished via the web mvc + commands. The most convenient way to generate controllers and all relevant + Web artifacts is to use the web mvc setup command + followed by the web mvc + all command: + + ~.domain.PizzaOrder roo> web mvc setup + +~.domain.PizzaOrder roo> web mvc all --package ~.web +This command will scan the Pizza Shop + project for any domain entities and scaffold a Spring MVC controller for + each entity detected. The --package attribute is needed to + specify in which package the controllers should be installed. This command + can be issued from your normal Roo shell or from the Roo shell, which + ships with STS. In order to use the integrated Roo shell within STS you + need to right click on the pizzashop application and select 'Spring Tools + > Open Roo Shell'. + + Note, that with the web + mvc setup command the nature of the project changes from a normal + Java project nature to a Web project nature in STS. This command will also + add additional dependencies such as Spring MVC, Tiles, etc to your + project. In order to update the project classpath within STS with these + new dependencies you can issue 'perform eclipse' again, followed by a + project refresh in STS. + + All newly added Web artifacts which are needed for the view + scaffolding can be found under the src/main/webapp folder. + This folder includes graphics, cascading style sheets, Java Server pages, + Tiles configurations and more. The purpose of these folders is summarized + in the UI customization section. The + Roo generated Spring MVC controllers follow the REST pattern as much as + possible by leveraging new features introduced with the release of Spring + Framework v3. The following URI - Resource mappings are applied in Roo + generated controllers: + + + + + + +
    + +
    + Step 6: Loading the Web Server + + To deploy your application in a Web container during project + development you have several options available: + + + + Deploy from your shell / command line (without the need to + assemble a war archive): + + + + run 'mvn tomcat:run' in the root of your project (not + inside the Roo shell) to deploy to a Tomcat + container + + + + run 'mvn jetty:run' in the root of your project (not + inside the Roo shell) to deploy to a Jetty + container + + + + + + Deploy to a integrated Web container configured in STS: + + + + Drag your project to the desired Web container inside the + STS server view + + + + Right-click your project and select 'Run As > Run on + Server' to deploy to the desired Web container + + + + After selecting your preferred deployment method you + should see the Web container starting and the application should be + available under the following URL http://localhost:8080/pizzashop + + + + + + + + +
    + +
    + Securing the Application + + As discussed with the Pizza Shop owner we need to control access to + certain views in the Web frontend. Securing access to different views in + the application is achieved by installing the Spring Security addon via + the security setup command: + + ~.web roo> security setup +Created SPRING_CONFIG_ROOT/applicationContext-security.xml +Created SRC_MAIN_WEBAPP/WEB-INF/views/login.jspx +Updated SRC_MAIN_WEBAPP/WEB-INF/views/views.xml +Updated ROOT/pom.xml [added property 'spring-security.version' = '3.1.0.RELEASE'; added dependencies +org.springframework.security:spring-security-core:${spring-security.version}, +org.springframework.security:spring-security-config:${spring-security.version}, +org.springframework.security:spring-security-web:${spring-security.version}, +org.springframework.security:spring-security-taglibs:${spring-security.version}] +Updated SRC_MAIN_WEBAPP/WEB-INF/web.xml +Updated SRC_MAIN_WEBAPP/WEB-INF/spring/webmvc-config.xml +Note, the Roo shell will hide the security + setup command until you have created a Web layer. As + shown above, the security setup command manages the project + pom.xml file. This means additional dependencies have been + added to the project. To add these dependencies to the STS workspace you + should run the perform eclipse command again followed by a + project refresh (if you're using STS or m2eclipse, the "perform eclipse" + command should be skipped as it will automatically detect and handle the + addition of Spring Security to your project). + + In order to secure the views for the Topping, + Base, and Pizza resources in the Pizza Shop + application you need to open the + applicationContext-security.xml file in the + src/main/resources/META-INF/spring folder: + + <!-- HTTP security configurations --> +<http auto-config="true" use-expressions="true"> + <form-login login-processing-url="/static/j_spring_security_check" login-page="/login" ↩ + authentication-failure-url="/login?login_error=t"/> + <logout logout-url="/static/j_spring_security_logout"/> + <!-- Configure these elements to secure URIs in your application --> + <intercept-url pattern="/pizzas/**" access="hasRole('ROLE_ADMIN')"/> + <intercept-url pattern="/toppings/**" access="hasRole('ROLE_ADMIN')"/> + <intercept-url pattern="/bases/**" access="hasRole('ROLE_ADMIN')"/> + <intercept-url pattern="/resources/**" access="permitAll" /> + <intercept-url pattern="/static/**" access="permitAll" /> + <intercept-url pattern="/**" access="permitAll" /> +</http>As a next step you can use the Spring Security + JSP tag library to restrict access to the relevant menu items in the + menu.jspx file: + + <div xmlns:jsp="..." xmlns:sec="http://www.springframework.org/security/tags" id="menu" version="2.0"> + <jsp:directive.page contentType="text/html;charset=UTF-8"/> + <jsp:output omit-xml-declaration="yes"/> + <menu:menu id="_menu" z="nZaf43BjUg1iM0v70HJVEsXDopc="> + <sec:authorize ifAllGranted="ROLE_ADMIN"> + <menu:category id="c_topping" z="Xm13w68rCIyzL6WIzqBtcpfiNQU="> + <menu:item id="i_topping_new" .../> + <menu:item id="i_topping_list" .../> + </menu:category> + <menu:category id="c_base" z="yTpmmNMm/hWoy3yf+aPcdUX2At8="> + <menu:item id="i_base_new" .../> + <menu:item id="i_base_list" .../> + </menu:category> + <menu:category id="c_pizza" z="mXqKC1ELexS039/pkkCrZVcSry0="> + <menu:item id="i_pizza_new" .../> + <menu:item id="i_pizza_list" .../> + </menu:category> + </sec:authorize> + <menu:category id="c_pizzaorder" z="gBYiBODEJrzQe3q+el5ktXISc4U="> + <menu:item id="i_pizzaorder_new" .../> + <menu:item id="i_pizzaorder_list" .../> + </menu:category> + </menu:menu> +</div> +This leaves the pizza order view visible to the public. + Obviously the delete and the update use case for the pizza order view are + not desirable. The easiest way to take care of this is to adjust the + @RooWebScaffold annotation in the + PizzaOrderController.java source: + + @RooWebScaffold(path = "pizzaorder", + formBackingObject = PizzaOrder.class, + delete=false, + update=false)This + will trigger the Roo shell to remove the delete and the update method from + the PizzaOrderController and also adjust the relevant view + artifacts. + + With these steps completed you can restart the application and the + 'admin' user can navigate to http://localhost:8080/pizzashop/login + to authenticate. +
    + +
    + Customizing the Look & Feel of the Web + UI + + Roo generated Web UIs can be customized in various ways. To find + your way around the installed Web-tier artifacts take a look at the + following table: + + + + + + + + + + The easiest way to customize the look & feel of the Roo Web UI + is to change CSS and image resources to suit your needs. The following + look & feel was created for the specific purpose of the Pizza Shop + application: + + + + + + + + + + Spring Roo also configures theming + support offered by Spring framework so you can leverage this + feature with ease. + + To achieve a higher level of customization you can change the + default Tiles template (WEB-INF/layouts/default.jspx) and adjust the JSP + pages (WEB-INF/views/*.jspx). WIth release 1.1 of Spring Roo jspx + artifacts can now be adjusted by the user while Roo can still make + adjustments as needed if domain layer changes are detected. See the JSP Views section for details. + + Furthermore the Spring Roo 1.1 release introduced a set of JSP tags + which not only reduce the scaffolded jspx files by 90% but also offer the + most flexible point for view customization. Roo will install these tags + into the user project where they can be accessed and customized to meet + specific requirements of the project. For example it would be fairly easy + to remove the integrated Spring JS / Dojo artifacts and replace them with + your JS framework of choice. To make these changes available for + installation in other projects you can create a simple add-on which replaces the default + tags installed by Roo with your customized tags. +
    + +
    + Selenium Tests + + Roo offers a core addon which can generate Selenium test scripts for you. + You can create the Selenium scripts by using the selenium + test command. Tests are generated for each controller + and are integrated in a test suite: + + ~.web roo> selenium test --controller ~.web.ToppingController +~.web roo> selenium test --controller ~.web.BaseController +~.web roo> selenium test --controller ~.web.PizzaController +~.web roo> selenium test --controller ~.web.PizzaOrderControllerThe + generated tests are located in the src/main/webapp/selenium + folder and can be run via the following maven command (executed from + command line, not the Roo shell): + + pizza> mvn selenium:selenese + + Running the maven selenium addon will start a new instance of the + FireFox browser and run tests against the Pizza Shop Web UI by using Roo + generated seed data. + + Please note that the maven selenium plugin configured in the project + pom.xml assumes that the FireFox + Web browser is already installed in your environment. Running the maven + selenium plugin also assumes that your application is already started as + discussed in step 6. Finally, there are limitations with regards to + locales used by the application. Please refer to the known issues section for + details. +
    + +
    + Backups and Deployment + + One other very useful command is the backup command. + Issuing this command will create you a backup of the current workspace + with all sources, log files and the script log file (excluding the target + directory): + + ~.web roo> backup +Created ROOT/pizzashop_2012-02-14_18:10:19.zip +Backup completed in 35 ms +~.web roo> +Finally, you may wish to deploy your application to a + production Web container. For this you can easily create a war archive by + taking advantage of the perform + package command: + + ~.web roo> perform package +[INFO] Scanning for projects... +[INFO] ------------------------------------------------------------------------ +[INFO] Building pizzashop +[INFO] task-segment: [package] +[INFO] ------------------------------------------------------------------------ +... +[INFO] [war:war {execution: default-war}] +[INFO] Exploding webapp... +[INFO] Assembling webapp pizzashop in /Users/stewarta/projects/roo-test/pizzashop/target/pizzashop-0.1.0-SNAPSHOT +[INFO] Copy webapp webResources to /Users/stewarta/projects/roo-test/pizzashop/target/pizzashop-0.1.0-SNAPSHOT +[INFO] Generating war /Users/stewarta/projects/roo-test/pizza/target/pizzashop-0.1.0-SNAPSHOT.war +[INFO] Building war: /Users/stewarta/projects/roo-test/pizza/target/pizzashop-0.1.0-SNAPSHOT.war +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 5.881s +[INFO] Finished at: Tue Feb 14 18:07:54 EST 2012 +[INFO] Final Memory: 8M/81M +[INFO] ------------------------------------------------------------------------ +~.web roo> + This command produces your war file which can then be easily + copied into your production Web container. +
    + +
    + Where To Next + + Congratuations! You've now completed the Roo Pizza Shop tutorial. + You're now in a good position to try Roo for your own projects. While + reading the next few chapters of this reference guide will help you + understand more about how to use Roo, we suggest the following specific + sections if you'd like to know more about commonly-used Roo + add-ons: + + + + Dynamic + Finders + + + + Spring Web + Flow addon + + + + Logging + addon + + + + JMS + addon + + + + Email + (SMTP) addon + + +
    +
    diff --git a/deployment-support/src/site/docbook/reference/welcome-existing.xml b/deployment-support/src/site/docbook/reference/welcome-existing.xml new file mode 100644 index 000000000..a1375e936 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/welcome-existing.xml @@ -0,0 +1,85 @@ + + + Existing Building Blocks + + Sometimes you have an existing project or database. This chapter + covers how to make Spring Roo work with it. + +
    + Existing Projects + + If you have an existing project that you'd like to use with Roo, we + recommend that you follow these steps: + + + + Decide whether your project files are easier to migrate to a new + Roo project or it's easier to amend your current project into a Roo + project. Both approaches are valid. The following steps reflect + migrating your current project into a Roo project. + + + + Convert the project to use Maven. Ensure you use the correct + Maven directory layouts. + + + + Move your Spring configuration and other files to the same + directories as used by Roo. Start a new Roo-based project if you're + unsure where these files are typically stored. + + + + Add the Roo annotations JAR and Maven AspectJ plugin to your + POM. Use the same syntax as a new Roo-based project would use. + + + + Load Roo on your project and verify it does not report any + errors. Resolve any errors before continuing. + + + + Add a test @RooToString annotation to one of your existing + classes. Verify the ITD is created and can be used within your IDE (if + you're using an IDE). Check the new toString() method is used. + + + + Start incrementally using the simpler Roo add-ons like toString + support and JavaBeans. When you're confident, move onto other Roo + commands and add-ons. + + + + If you encounter any difficulty, we recommend you consult the Roo Resources section of the reference guide + for help. +
    + +
    + Existing Databases + + Many organisations have existing databases that they'd like to use + with Roo. + + A significant new feature added to Spring Roo 1.1 was support for + incremental database reverse engineering. This feature is robust and + comprehensive, and allows you to reverse engineer an existing database in + a single command. The single command doesn't even ask you any questions as + it operates, and it gracefully handles changes to your schema over time. + + + We recommend that you consult the incremental database reverse engineering + chapter if you'd like to work with an existing relational + database. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/welcome-intro.xml b/deployment-support/src/site/docbook/reference/welcome-intro.xml new file mode 100644 index 000000000..a105b14c3 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/welcome-intro.xml @@ -0,0 +1,736 @@ + + + Introduction + +
    + What is Roo? + + Spring Roo + is an easy-to-use + productivity tool for rapidly building enterprise applications in the Java + programming language. It allows you to build high-quality, + high-performance, lock-in-free enterprise + applications in just + minutes. Best of all, Roo works alongside your existing Java knowledge, + skills and experience. You probably won't need to learn anything new to + use Roo, as there's no new language or runtime platform needed. You simply + program in your normal Java way and Roo just works, sitting in the + background taking care of the things you don't want to worry about. + It's an approach unlike anything you've ever seen before, we + guarantee it! + + You work with Roo by loading its "shell" in a window and leaving it + running. You can interact with Roo via commands typed into the shell if + you like, but most of the time you'll just go about programming in your + text editor or IDE as usual. As you make changes to your project, Roo + intelligently determines what you're trying to do and takes care of doing + it for you automatically. This usually involves automatically detecting + file system changes you've made and then maintaining files in response. We + say "maintaining files" because Roo is fully round-trip + aware. This means you can change any code you like, at any time + and without telling Roo about it, yet Roo will intelligently and + automatically deal with whatever changes need to be made in response. It + might sound magical, but it isn't. This documentation will clearly explain + how Roo works and you'll find yourself loving the approach - just like so + the many other people who are already using Roo. + + Before you start wondering how Roo works, let's confirm a few things + it is NOT: + + + + Roo is not a runtime. Roo is not involved + with your project when it runs in production. You won't find any Roo + JARs in your runtime classpath or Roo annotations compiled into your + classes. This is actually a wonderful thing. It means you have no + lock-in to worry about (you can remove Roo from your + project in just a couple of minutes!). It probably also means you + won't need to get approval to use Roo (what's to approve when it's + more like a command line tool than a critical runtime library like + Spring + Framework?). It also means there is no technical way possible + for Roo to slow your project down at runtime, waste memory or bloat + your deployment artefacts with JARs. We're really proud of the fact + that Roo imposes no engineering + trade-offs, as it was one of our central design + objectives. + + + + Roo is not an IDE plugin. There is no + requirement for a "Roo Eclipse plugin" or "Roo IntelliJ plugin". Roo + works perfectly fine in its own operating system command window. It + sits there and monitors your file system, intelligently and + incrementally responding to changes as appropriate. This means you're + perfectly able to use vi or emacs if you'd like (Roo doesn't mind how + your project files get changed). + + + + Roo is not an annotation processing + library. There is a Java 6 feature known as the annotation + processing API. Roo does not use this API. This allows Roo to work + with Java 5, and also gives us access to a much more sophisticated and + extensible internal model. + + + + So how does Roo actually work then? The answer to that question + depends on how much detail you'd like. In super-summary form, Roo uses an + add-on based architecture that performs a combination of passive and + active code generation of inter-type + declarations. If you're interested in how that works at a practical + project level, we cover that shortly in the "Beginning With Roo: The Tutorial" chapter. Or + for an advanced look at Roo internals, we've covered that in Part III: Internals and Add-On + Development. +
    + +
    + Why Use It + + There are dozens of reasons people like to use Roo. We've worked + hard to make it an attractive tool that delivers real value without + imposing unpleasant trade-offs. Nonetheless, there are five major reasons + why people like Roo and use it. Let's discuss these major reasons + below. + +
    + Higher Productivity + + With Roo it is possible for Java developers to build sophisticated + enterprise applications in a best-practice manner within minutes. This + is not just a marketing claim, but it's a practical fact you can + experience yourself by trying the ten + minute test. + + Anyone who has programmed Java for a few years and looked at the + alternatives on other platforms will be fully aware that enterprise Java + suffers from productivity problems. It takes days to start a new project + and incredibly long cycle times as you go about normal development. + Still, we remain with Java because it's a highly attractive platform. + It's the most + widely used programming language on the planet, with millions + of competent developers. It has first-class tooling, excellent runtime + performance, numerous mature libraries and widely-supported standards. + Java is also open source, has multiple vendors and countless + choice. + + We built Roo because we want enterprise Java developers to enjoy + the same productivity levels that developers on other platforms take for + granted. Thanks to Roo, Java developers can now enjoy this higher + productivity plus a highly efficient, popular, + scalable, open, reliable platform. Best of all, in five years time it + will still be possible to hire millions of people who can look at those + Roo-based projects and understand what is going on and maintain them + (even if you've stopped + using Roo by then). + + Roo's higher productivity is provided both at original project + creation, and also as a developer builds out the rest of the project. + Because Roo provides round-trip support, the higher productivity is + automatically provided over the full lifespan of a project. This is + particularly important given the long-term maintenance costs of a + project far outweigh the initial development costs. While you can use + Roo just for an initial jump-start if you so wish, your return on + investment is exponential as you continue using it throughout a project + lifespan. + + Finally, while individual productivity is important, most of us + work in teams and know that someday someone else will probably maintain + the code we've written. As professionals we follow architectural + standards and conventions to try and ensure that our present and future + colleagues will be able to understand what we did, why, and have an easy + time maintaining it. Our organisations often establish standards for us + to follow in an effort to ensure other projects are tackled in similar + ways, thus allowing people to transfer between projects and still be + productive. Of course, most organisations also have people of greatly + differing backgrounds and experiences, with new graduates typically + working alongside more experienced developers and architect-level + experts. Roo helps significantly in this type of real-world environment + because it automatically implements specific design patterns in an + optimal convention-over-configuration manner. This ensures consistency + of implementation within a given Roo-based project, as well as across + all other Roo-based projects within an organisation (and even outside + your organisation, which greatly helps with hiring). Of course, the fact + Roo builds on stock-standard Java also means people of vastly different + experience levels can all be highly productive and successful with + Roo. +
    + +
    + Stock-Standard Java + + It's no longer necessary to switch platform or language to achieve + extremely high levels of productivity! We designed Roo from the outset + so those people with existing Java 5 knowledge, skills and experience + would feel right at home. If you've ever built an enterprise application + with Java, some or all of the technologies that Roo uses by default will + already be familiar to you. + + Some of the common technologies Roo projects use include Spring (such + as Spring Framework, Spring Security and Spring Web Flow), Maven, Java + Server Pages (JSP), Java Persistence API (JPA, such as Hibernate), Tiles + and AspectJ. + We've chosen technologies which are extremely commonly used in + enterprise Java projects, ensuring you've probably either already used + them or at least will have no difficulty finding hundreds of thousands + of other people who have (and the resultant books, blogs, samples etc + that exist for each). Also, because most of these technologies are + implemented using add-ons, if you'd like Roo + to use a different technology on your project it's quite easy to do + so. + + By using standard Java technologies, Roo avoids reinventing the + wheel or providing a limited-value abstraction over them. The + technologies are available to you in their normal form, and you can use + them in the same way as you always have. What Roo brings to the table is + automatic setup of those technologies into a Spring-certified + best-practice application architecture and, if you wish, automatic + maintenance of all files required by those technologies (such as XML, + JSP, Java etc). You'll see this in action when you complete the ten minute test. + + You'll also find that Roo adopts a very conservative, incremental + approach to adding technologies to your project. This means when you + first start a new project Roo will only assume you want to build a + simple JAR. As such it will have next to no dependencies. Only when you + ask to add a persistence provider will JPA be installed, and only when + you add a field using JavaBean Validation annotations will that library + be installed. The same holds true for Spring Security, Spring Web Flow + and the other technologies Roo supports. With Roo you really do start + small and incrementally add technologies if and when you want to, which + is consistent with Roo's philosophy of there being no engineering + trade-offs. +
    + +
    + Usable and Learnable + + There are many examples of promising technologies that are simply + too hard for most people to learn and use. With Roo we were inspired by + the late Jef Raskin's book, "The + Humane Interface". In the book Raskin argued we have a duty to + make things so easy to use that people naturally "habituate" to the + interface, that text-based interfaces are often more appropriate than + GUIs, and that your "locus of attention" is all that matters to you and + a machine should never disrupt your locus of attention and randomly + impose its idiosyncratic demands upon you. + + With Roo we took these ideas to heart and designed a highly usable + interface that lets you follow your locus of attention. This means you + can do things in whatever order you feel is appropriate and never be + subservient to the Roo tool. You want to delete a file? Just do it. You + want to edit a file? Just do it. You want to change the version of + Spring you're using? Just do it. You want to remove Roo? Just do it. You + want to hand-write some code Roo was helping you with? Just do it. You + want to use Emacs and Vim at the same time? No problem. You forgot to + load Roo when you were editing some files? That's no problem either (in + fact you can elect to never load Roo again and your project will remain + just fine). + + Because Roo uses a text-based interface, there is the normal + design trade-off between learnability, expressability and conciseness. + No text-based interface can concurrently satisfy all three dimensions. + With Roo we decided to focus on learnability and expressability. We + decided conciseness was less important given the Roo shell would provide + an intuitive, tab-based completion system. We also added other features + to deliver conciseness, such as contextual awareness (which means Roo + determines the target of your command based on the command completed + before it) and command abbreviation (which means you need only type in + enough of the command so Roo recognises what you're trying to + do). + + The learnability of Roo is concurrently addressed on three fronts. + First, we favor using standard Java + technologies that you probably already know. Second, we are + careful to keep Roo out of your way. The more Roo simply works in the + background automatically without needing your involvement, the less you + need to learn about it in the first place. This is + consistent with Raskin's recommendation to never interrupt your locus of + attention. Third, we offer a lot of learnability features in Roo itself. + These include the "hint" command, + which suggests what you may wish to do next based on your present + project's state. It's quite easy to build an entire Roo project simply + by typing "hint", pressing enter, and following the instructions Roo + presents (we do this all the time during conference talks; it's always + easier than remembering commands!). + There's also the intelligent tab + completion, which has natural, friendly conventions like + completing all mandatory arguments step-by-step (without distracting you + with unnecessary optional arguments). There's also the online "help" command, sample scripts, this + documentation and plenty of other + resources. + + Roo also follows a number of well-defined conventions so that you always know what it's + doing. Plus it operates in a "fail safe" manner, like automatically + undoing any changes it makes to the file system should something go + wrong. You'll quickly discover that Roo is a friendly, reliable + companion on your development journey. It doesn't require special + handling and it's always there for you when you need it. + + In summary, we've spent a lot of time thinking about usability and + learnability to help ensure you enjoy your Roo experience. +
    + +
    + No Engineering Trade-Offs + + Roo doesn't impose any engineering trade-offs on your project. In + fact, compared with most Spring-based enterprise applications, we're + almost certain you'll find a Roo application will have a smaller + deployment artefact, operate more quickly in terms of CPU time, and + consume less memory. You'll also find you don't miss out on any of the + usual IDE services like code assist, debugging and profiling. We'll + explore how Roo achieves this below, but this information is relatively + advanced and is provided mainly for architects who are interested in + Roo's approach. As this knowledge is not required + to simply use Roo, feel free to jump ahead to the next section if you + wish. + + Smaller deployment artefacts are achieved due to Roo's incremental + dependency addition approach. You start out with a small JAR and then we + add dependencies only if you actually need them. As of Roo 1.0.0, a + typical Roo-based web application WAR is around 13 Mb. This includes + major components like Spring, Spring JavaScript (with embedded Dojo) and + Hibernate, plus a number of smaller components like URL rewriting. As + such Roo doesn't waste disk space or give you 30+ Mb WARs, which results + in faster uploads and container startup times. + + Speaking of startup times, Roo uses AspectJ's excellent + compile-time weaving approach. This gives us a lot more power and + flexibility than we'd ordinarily have, allowing us to tackle advanced + requirements like advising domain objects and dependency injecting them + with singletons. It also means the dynamic proxies typically created + when loading Spring are no longer required. Roo applications therefore + startup more quickly, as there's no dynamic proxy creation overhead. + Plus Roo applications operate more quickly, as there's no dynamic proxy + objects adding CPU time to the control flow. + + Because Roo's AspectJ usage means there are no proxy objects, you + also save the memory expense of having to hold them. Furthermore, Roo + has no runtime component, so you won't lose any memory or CPU time there + either. Plus because Roo applications use Java as their programming + language, there won't be any classes being created at runtime. This + means a normal Roo application won't suffer exhaustion of permanent + generation memory space. + + While some people would argue these deployment size, CPU and + memory considerations are minor, the fact is they add up when you have a + large application that needs to scale. With Roo your applications will + use your system resources to their full potential. Plus as we move more + and more enterprise applications into virtualized and cloud-hosted + environments, the requirement for performant operation on shared + hardware will become even more relevant. + + You'll also find that Roo provides a well thought out application architecture that delivers + pragmatism, flexibility and ease of maintainability. You'll see we've + made architectural decisions like eliminating the DAO layer, using + annotation-based dependency injection, and automatically providing + dependency injection on entities. These decisions dramatically reduce + the amount of Java and XML code you have to write and maintain, plus + improve your development cycle times and refactoring experiences. + + With Roo, you don't have to make a trade-off between productivity + or performance. Now it's easy to have both at the same time. +
    + +
    + Easy Roo Removal + + One of the biggest risks when adopting a new tool like Roo is the + ease at which you can change your mind in the future. You might decide + to remove a tool from your development ecosystem for many different + reasons, such as changing requirements, a more compelling alternative + emerging, the tool having an unacceptable number of bugs, or the tool + not adequately supporting the versions of other software you'd like to + use. These risks exist in the real world and it's important to mitigate + the consequences if a particular tool doesn't work out in the + long-term. + + Because Roo does not exist at runtime, your risk exposure from + using Roo is already considerably diminished. You can decide to stop + using Roo and implement that decision without even needing to change any + production deployment of the application. + + If you do decide to stop using Roo, this can be achieved in just a + few minutes. There is no need to write any code or otherwise make + significant changes. We've covered the short removal process in a + dedicated removing Roo chapter, but in + summary you need to perform a "push in refactor" command within Eclipse + and then do a quick regular expression-based find and replace. That's + all that is needed to 100% remove Roo from your project. We often remove + Roo from a project during conference demonstrations just to prove to + people how incredibly easy it is. It really only takes two to three + minutes to complete. + + We believe that productivity tools should earn their keep by + providing you such a valuable service that you want + to continue using them. We've ensured Roo will never lock you + in because (a) it's simply the right and credible thing to do + engineering-wise and (b) we want Roo to be such an ongoing help on your + projects that you actually choose to keep it. If + you're considering alternative productivity tools, consider whether they + also respect your right to decide to leave and easily implement that + decision, or if they know you're locked in and can't do much about + it. +
    +
    + +
    + Installation + + Roo is a standard Java application that is fully self-contained + within the Roo distribution ZIPs. You can download Roo from one of the + download sites, or build a distribution ZIP yourself from + our source control + repository. + + If you are upgrading from an existing version of Spring Roo, you + should consult the upgrade notes for + important information. + + Before attempting to install Roo, please ensure you have the + following system dependencies: + + + + A Linux, Apple or Windows-based operating system (other + operating systems may work but are not guaranteed) + + + + A Java 6 or 7 installation, with the + $JAVA_HOME environment variable pointing to the + installation. Note that Java 8 is currently not supported. + + + + Apache Maven 2.0.9 or above installed and in the path + + + + We have listed various considerations concerning the Java + Development Kit (JDK) and operating systems in the known issues section of this + documentation. We always recommend you use the latest version of Java and + Maven that are available for your platform. We also recommend that you use + Spring + Tool Suite (STS), which is our free Eclipse-based IDE that includes + a number of features that make working with Roo even easier (you can of + course use Roo with normal Eclipse or without an IDE at all if you + prefer). + + Once you have satisfied the initial requirements, you can install + Roo by following these steps: + + + + Unzip the Roo installation ZIP to a directory of your choice; + this will be known as $ROO_HOME in the directions + below + + + + If using Windows, add $ROO_HOME\bin to your + %PATH% environment variable + + + + If using Linux or Apple, create a symbolic link using a command + such as sudo ln -s $ROO_HOME/bin/roo.sh + /usr/bin/roo + + + + Next verify Roo has been installed correctly. This can be done using + the following commands: + + $ mkdir roo-test +$ cd roo-test +$ roo quit + ____ ____ ____ + / __ \/ __ \/ __ \ + / /_/ / / / / / / / + / _, _/ /_/ / /_/ / +/_/ |_|\____/\____/ W.X.Y.ZZ [rev RRR] + + +Welcome to Spring Roo. For assistance press TAB or type "hint" then hit ENTER. +$ cd .. +$ rmdir roo-test + + If you see the logo appear, you've installed Roo successfully. For + those curious, the "[rev RRR]" refers to the Git commit ID used to compile + that particular build of Roo. +
    + +
    + Optional ROO_OPTS Configuration + + The standalone Roo shell supports fine-tuning display-related + configuration via the ROO_OPTS environment variable. An environment + variable is used so that these configuration settings can be applied + before the shell is instantiated and the first messages displayed. The + ROO_OPTS settings does not apply within Spring Tool Suite's embedded + Roo shell. + + At present the only configuration settings available is roo.bright. + This causes foreground messages in the shell to be displayed with brighter + colors. This is potentially useful if your background color is light (e.g. + white). You can set the variable using the following commands: + + $ export ROO_OPTS="-Droo.bright=true" // Linux or Apple +$ set ROO_OPTS="-Droo.bright=true" // Windows usersThere + is an enhancement request within our issue tracker for customisable shell + color schemes. If you're interested in seeing this supported by Roo, you + may wish to consider voting for ROO-549. +
    + +
    + First Steps: Your Own Web App in Under 10 Minutes + + Now that you have installed Roo, let's spend a couple of minutes + building an enterprise application using Roo. + + The purpose of this application is just to try out Roo. We won't + explain what's going on in these steps, but don't worry - we'll do that in + the next chapter, Beginning With Roo: The + Tutorial. We will try to teach you about some usability features as + we go along, though. + + Please start by typing the following commands: + + $ mkdir ten-minutes +$ cd ten-minutes +$ roo + ____ ____ ____ + / __ \/ __ \/ __ \ + / /_/ / / / / / / / + / _, _/ /_/ / /_/ / +/_/ |_|\____/\____/ W.X.Y.ZZ [rev RRR] + + +Welcome to Spring Roo. For assistance press TAB or type "hint" then hit ENTER. +roo> hint +Welcome to Roo! We hope you enjoy your stay! + +Before you can use many features of Roo, you need to start a new project. + +To do this, type 'project' (without the quotes) and then hit TAB. + +Enter a --topLevelPackage like 'com.mycompany.projectname' (no quotes). +When you've finished completing your --topLevelPackage, press ENTER. +Your new project will then be created in the current working directory. + +Note that Roo frequently allows the use of TAB, so press TAB regularly. +Once your project is created, type 'hint' and ENTER for the next suggestion. +You're also welcome to visit http://forum.springframework.org for Roo help. + + Notice the output from the "hint" command guides you through what to + do next. Let's do that: + + roo> project --topLevelPackage com.tenminutes +Created /home/balex/ten-minutes/pom.xml +Created SRC_MAIN_JAVA +Created SRC_MAIN_RESOURCES +Created SRC_TEST_JAVA +Created SRC_TEST_RESOURCES +Created SRC_MAIN_WEBAPP +Created SRC_MAIN_RESOURCES/META-INF/spring +Created SRC_MAIN_RESOURCES/META-INF/spring/applicationContext.xml +roo> hint +Roo requires the installation of a JPA provider and associated database. + +Type 'jpa setup' and then hit TAB three times. +We suggest you type 'H' then TAB to complete "HIBERNATE". +After the --provider, press TAB twice for database choices. +For testing purposes, type (or TAB) HYPERSONIC_IN_MEMORY. +If you press TAB again, you'll see there are no more options. +As such, you're ready to press ENTER to execute the command. + +Once JPA is installed, type 'hint' and ENTER for the next suggestion. + + At this point you've now got a viable Maven-based project setup. But + let's make it more useful by setting up JPA. In the interests of time, + I'll just include the commands you should type below. Be sure to try using + the TAB key when using the shell, as it will save you from having to type + most of these commands: + + roo> jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY +roo> hint +roo> entity jpa --class ~.Timer --testAutomatically +roo> hint +roo> field string --fieldName message --notNull +roo> hint web mvc +roo> web mvc setup +roo> web mvc all --package ~.web +roo> selenium test --controller ~.web.TimerController +roo> perform tests +roo> perform package +roo> perform eclipse +roo> quit +$ mvn tomcat:run + + The "perform" + commands could have been easily undertaken from the command prompt using + "mvn" instead. We just did them from within Roo to benefit from TAB + completion. You could have also skipped the "perform eclipse" command if + you are using the m2eclipse plugin. If you are using Spring Tool + Suite (STS), it automatically includes m2eclipse and as such you do not + need to use the "perform eclipse" command. Indeed if you're an STS user, + you could have started your Roo project right from within the IDE by + selecting the File > New > Spring Roo menu option and completing the + steps. In that case a Roo Shell view will open within STS and from there + you can enter the remaining commands. + + Now that you've loaded Tomcat, let's run the Selenium tests. You can + do this by loading a new command window, changing into the ten-minutes + directory, and then executing mvn selenium:selenese. You + should see your FireFox web browser execute the generated Selenium tests. + You can also visit your new web application at http://localhost:8080/tenminutes, + which should look similar to the picture below. + + + + + + + + Naturally in this short ten minute test we've skipped dozens of + features that Roo can provide, and didn't go into any detail on how you + could have customised the application. We just wanted to show you that Roo + works and you can build an application in record-time. The Beginning With Roo: The Tutorial chapter will + go through the process of building an application in much more depth, + including how to work with your IDE and so on. +
    + +
    + Exploring the Roo Samples + + Now that you've built your first application during the ten minute test, you have a rough idea + of how Roo works. To help you learn Roo we ship several sample scripts + that can be used to build new applications. These sample scripts can be + found in your $ROO_HOME/classpath/src/main/resources/ directory. + These sample scripts available from roo classpath. You can + run any sample script by using the following command format: + + $ mkdir sample +$ cd sample +$ roo +roo> script --file filename.roo +roo> quit +$ mvn tomcat:run + + The filename.roo shown in the statements above should + be substituted with one of the filenames from this list (note that you get + filename completion using TAB): + + + + clinic.roo: The Petclinic sample script is + our most comprehensive. It builds a large number of entities, + controllers, Selenium tests and dynamic finders. It also sets up Log4J + and demonstrates entity relationships of different + cardinalities. + + + + vote.roo: The Voting sample script was + built live on-stage during SpringOne Europe 2009, as detailed in the + project history section. + This is a nice sample script because it's quite small and only has two + entities. It also demonstrates Spring Security usage. + + + + wedding.roo: The Wedding RSVP sample script + is the result of the wedding RSVP tutorial. If + you're looking for another Roo tutorial, this sample script (along + with the associated blog entry) is a good choice. This project + includes Selenium tests, dynamic finders and Log4j + configuration. + + + + pizzashop.roo: The PizzaShop sample script + demonstrates Roo's integration of JPA composite primary keys. It + produces a headless application which is accessible via JSON + (available through Spring MVC REST integration). To add a Web UI on + top of it, simply run the web mvc all command. The + application is described in greater detail in our tutorial. + + +
    + +
    + Suggested Steps to Roo Productivity + + As we draw to the close of this first chapter, you know what Roo is, + why you'd like to use it, have installed it and completed the ten minute + test, plus you know which samples are available. You could probably stop + at this point and apply Roo productively to your projects, but we + recommend that you spend a couple of hours learning more about Roo. It + will be time well spent and easily recouped by the substantially greater + productivity Roo will soon deliver on your projects. + + The next step is to complete the Beginning + With Roo: The Tutorial chapter. In the tutorial chapter you'll + learn how to use Roo with your preferred IDE and how flexible and natural + it is to develop with Roo. After that you should read the application architecture chapter to + understand what Roo applications look like. From there you might wish to + wrap up the recommended tour of Roo with a skim over the usage and conventions chapter. This final + recommended chapter will focus more on using the Roo tool and less on the + applications that Roo creates. + + If you can't find the information you're looking for in this + reference guide, the resources chapter + contains numerous Roo-related web sites and other community + resources. + + We welcome your comments and suggestions as you go about using Roo. + One convenient way to share your experiences is to Tweet with the @springroo + hash code. You can also follow Roo's core development team via Twitter for + the latest Roo updates. In any event, we thank you for exploring Roo and + hope that you enjoy your Roo journey. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/welcome-removing.xml b/deployment-support/src/site/docbook/reference/welcome-removing.xml new file mode 100644 index 000000000..2cb7746b9 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/welcome-removing.xml @@ -0,0 +1,325 @@ + + + Removing Roo + + While we'll be sad to see you go, we're happy that Roo was able to + help you in some way with your Spring-based projects. We also know that most + people reading this chapter aren't actually likely to remove Roo at all, and + are simply wondering how they'd go about it in the unlikely event they ever + actually wanted to. If you have a source control system, it's actually a + good idea to complete these instructions (without checking in the result!) + just to satisfy yourself that it's very easy and reliable to remove + Roo. + +
    + How Roo Avoids Lock-In + + At the time we created the mission statement for Roo, a key + dimension was "without compromising engineering integrity or + flexibility". To us that meant not imposing an unacceptable + burden on projects like forcing them to use the Roo API or runtime or + locking them in. While it complicated our design to achieve this, we're + very proud of the fact Roo's approach has no downside at runtime or + lock-in or future flexibility. You really can have your cake and eat it + too, to reflect on the common English expression. + + Roo avoids locking you in by adopting an active code generation + approach, but unlike other code generators, we place Roo generated code in + separate compilation units that use AspectJ inter-type declarations. This + is vastly better than traditional active code generation alternatives like + forcing you to extend a particular class, having the code generator extend + one of your classes, or forcing you to program a model in an unnatural + diagrammatic abstraction. With Roo you just get on with writing Java code + and let Roo take care of writing and maintaining the code you don't want + to bother writing. + + The other aspect of how Roo avoids lock-in is using annotations with + source-level + retention. What this means is the annotations are not preserved in + your .class files by the time they are compiled. This in turn + means you do not need the Roo annotation library in your runtime + classpath. If you look at your WEB-INF/lib directory (if + you're building a web project), you will find absolutely no Roo-related + JARs. They simply don't exist. In fact if you look at your + development-time classpath, only the Roo annotation JAR library will be + present - and that JAR doesn't contain a single executable line of code. + The entire behaviour of Roo is accomplished at development time when you + load the Roo shell. If you also think about the absence of executable code + anywhere in your project classpath, there is no scope for possible Roo + bugs to affect your project, and there is no risk of upgrading to a later + version of Roo. + + Because we recommend people check their Roo-generated + *_Roo_*.aj files into source control, you don't even need to + load Roo to perform a build of your project. The source-level annotation + library referred to in the previous paragraph is in a public Maven + repository and will automatically be downloaded to your computer if it's + not already present. This means Roo is not part of your build process and + your normal source control system branching and tagging processes will + work. + + This also means that a project can "stop using Roo" by simply never + loading the Roo shell again. Because the *_Roo_*.aj files are + written to disk by the Roo shell when it last ran, even if it's never + loaded again those files will still be present. The removal procedures in + this chapter therefore focus on a more complete removal, in that you no + longer even want the *_Roo_*.aj files any more. That said, + there's nothing wrong with just never loading Roo again and keeping the + *_Roo_*.aj files. The only possible problem of adopting the + "never load Roo again" approach is that someone might load Roo again and + those files will be updated to reflect the latest optimisations that Roo + can provide for you. +
    + +
    + Pros and Cons of Removing Roo + + By removing Roo, you eliminate the Roo-generated source files from + your project. These are inter-type declarations stored in + *_Roo_*.aj files. You also remove the Roo annotation library + from your project. This might be attractive if you've made a decision to + no longer use Roo for some reason, or you'd like to ship the finished + project to your client and they'd prefer a simple Java project where every + piece of code is in standard .java files. Another reason you + might like to remove Roo is to simply satisfy yourself it's easy to do so + and therefore eliminate a barrier to adopting Roo for real projects in the + first place. + + Even though it's easy to do so, there are downsides of removing Roo + from your project: + + + + Cluttered Java classes: If the + *_Roo_*.aj files are removed, their contents need to go + somewhere. That somewhere is into your .java source + files. This means your .java source files will be + considerably longer and contain code that no developer actually wrote. + When developers open your .java source files, they'll + need to figure out what was written by hand and is unique to the + class, what was automatically generated and then modified, and what + was automatically generated and never modified. If using Roo this + problem is eliminated, as anything automatically generated is in a + separate, easily-identified source file. + + + + No round-trip support: Let's imagine for a + moment that you've written (either by hand or via your IDE's code + generation feature) a toString() method and getter/setter + pairs for all your fields. You then decide to rename a field. Suddenly + the getter, setter and toString() methods are all in + error. If you use Roo, it automatically detects your change and + appropriately updates the generated code. If you remove Roo, you'll + lose this valuable round-trip support and be doing a lot more tedious + work by hand. + + + + No optimisations to generated files: With + each version of Roo we make improvements to the automatically-created + *_Roo_*.aj files. These improvements are automatically + made to your *_Roo_*.aj files when you load a new version + of Roo. These improvements occasionally fix bugs, but more often + provide new features and implement existing features more efficiently + (remember eliminating engineering trade-offs and therefore maximising + efficiency is a major objective in our mission statement). If you remove + the *_Roo_*.aj files, you'll receive the code as of that + date and you'll miss out on further improvements we make. + + + + Loss of Roo commands: There are dozens of + Roo commands available to assist you adapt to evolving project + requirements. Next month you might be asked to add JMS services to + your project. With Roo you just "jms setup". The month after + you're asked about SMTP, so you just "email sender setup". + If you've eliminated Roo, you'll need to resort to much more + time-consuming manual configuration (with its associated trial and + error). + + + + Deprecated library versions: Because Roo + automatically updates your code and has a good knowledge of your + project, it's easy to always use the latest released versions of + important runtime technologies like Spring and JPA. If you stop using + Roo, you'll need to manually do all of the work involved in upgrading + your project to newer versions. This will mean you're likely to end up + on older runtime library versions that have bugs, fewer features and + are not maintained or supported. With Roo you significantly mitigate + this risk. + + + + Undesirable architectural outcomes: With + Roo you achieve team-wide consistency and a solution with a high level + of engineering integrity. If developers are forced to write + repetitious code themselves and no longer enjoy optimised Roo + commands, you'll likely find that over time you lose some of the + consistency and engineering advantages of having used Roo in the first + place. + + + + Higher cost: With the above in mind, you'll + probably find development takes longer, maintenance takes longer and + your runtime solution will be less efficient than if you'd stayed with + Roo. + + + + As such we believe using Roo and continuing to use Roo makes a lot + of sense. But if you're willing to accept the trade-offs of removing Roo + (which basically means you switch to writing your project the unproductive + "old fashioned way"), you can remove Roo very easily. Don't forget when in + doubt you can always defer the decision. It's not as if Roo won't let you + remove it just as easily in six months or two years from now! +
    + +
    + Step-by-Step Removal Instructions + + The following instructions explain how to remove Spring Roo from one + of your projects that has to date been using Roo. Naturally if you'd + simply like to remove Roo from your computer (as opposed to from an + existing project), the process is as simple as removing the Roo + installation directory and symbolic link. This section instead focuses on + the removal from your projects. + + As mentioned above, a simple way of stopping to use Roo is to simply + never load it again. The *_Roo_*.aj files will still be on + disk and your project will continue to work regardless of whether the Roo + shell is never launched again. You can even uninstall the Roo system from + your computer and your project will still work. The advantage of this + approach is you haven't lost most of the benefits of using Roo and it's + very easy to simply reload the Roo shell again in the future. This section + covers the more complete removal option should you not even want the + *_Roo_*.aj files any more. + + Please be aware that enhancement request ROO-222 + exists to replace step 1 with a Roo command, and ROO-330 + similarly focuses on steps 2 and 3. Please vote for these enhancement + requests if you'd like them actioned, although the instructions below + still provide a fast and usable removal procedure. + +
    + Step 1: Push-In Refactor + + Before proceeding, ensure you have quit any running Roo shell. We + also recommend you run any tests and load your web application interface + (if there is one) to verify your project works correctly before starting + this procedure. We also recommend that you create a branch or tag in + your source control repository that represents the present + "Roo-inclusive" version, as it will help you should you ever wish to + reenable Roo after a + removal. + + To remove Roo from a project, you need to import the project into + Eclipse or SpringSource Tool Suite. Once the project has been imported + into Eclipse, right-click the project name in Package Explorer and + select Refactor > Push-In Refactor. If this option is missing, ensure + that you have a recent version of AJDT installed. After selecting the + push-in refactor menu option, a list of all Roo inter-type declarations + will be displayed. Simply click OK. AJDT will have now moved all of the + Roo inter-type declarations into your standard .java files. + The old *_Roo_*.aj files will have automatically been + deleted. +
    + +
    + Step 2: Annotation Source Code Removal + + While your project is now free of inter-type declarations, your + .java files will still have @Roo annotations + within them. In addition, there will be import directives + at the top of your .java files to import those + @Roo annotations. You can easily remove these unwanted + members by clicking Search > Search > File Search, containing text + "\n.*[@\.]Roo[^t_]+?.*$" (without the quotes), file name + pattern "*.java" (without the quotes), ticking the "Regular + expression" and "Case sensitive" check-boxes and clicking "Replace". When + the next window appears and asks you for a replacement pattern, leave + it blank and continue. All of the Roo statements will have now been + removed. We have noticed for an unknown reason that sometimes this + operation needs to be repeated twice in Eclipse. +
    + +
    + Step 3: Annotation JAR Removal + + By now your .java files do not contain any Roo + references at all. You therefore don't require the + org.springframework.roo.annotations-*.jar library in your + development-time classpath. Simply open your pom.xml and + locate the <dependency> element which contains + <artifactId>org.springframework.roo.annotations</artifactId>. + Delete (or comment out) the entire <dependency> + element. If you're running m2Eclipse, there is no need to do anything + further. If you used the command-line mvn command to create + your Eclipse .classpath file, you'll need to execute + mvn eclipse:clean eclipse:eclipse to rebuild the + .classpath file. + + Roo has now been entirely removed from your project and you should + re-run your tests and user interface for verification of expected + operation. It's probably a good idea to perform another branch or tag in + your source control repository so the change set is documented. +
    +
    + +
    + Reenabling Roo After A Removal + + If you decide to change your mind and start using Roo again, the + good news is that it's relatively easy. This is because your project + already uses the correct directory layout and has AspectJ etc properly + configured. To re-enable Roo, simply open your pom.xml and re-add the + org.springframework.roo.annotations + <dependency> element. You can obtain the correct syntax + by simply making a new directory, changing into that directory, executing + roo script vote.roo, and inspecting the resulting + pom.xml. + + Once you've added the dependency, you're free to load Roo from + within your project's directory and start using the Roo commands again. + You're also free to add @Roo annotations to any + .java file that would benefit from them, but remember that + Roo is "hands off by default". What that means is if you used the push-in refactor command to move + members (e.g. fields, methods, annotations etc) into the .java + file, Roo has no way of knowing that they originated from a push-in + refactor as opposed to you having written them by hand. Roo therefore + won't delete any members from your .java file or override + them in an inter-type declaration. + + Our advice is therefore (a) don't remove Roo in the first place or + (b) if you have removed Roo and go back to using Roo again, delete the + members from your .java files that Roo is able to + automatically manage for you. By deleting the members that Roo can manage + for you from the .java files, you'll gain the maximum benefit + of your decision to resume using Roo. If you're unsure which members Roo + can automatically manage, simply comment them out and see if Roo provides + them automatically for you. Naturally you'll need the relevant + @Roo annotation(s) in your .java files before + Roo will create any members automatically for you. + + A final tip if you'd like to return to having ITDs again is that + AJDT 2.0 and above offers a Refactor > Push Out command. This may + assist you in moving back to ITDs. The Edit > Undo command also + generally works if you decide to revert immediately after a Refactor > + Push In operation. +
    +
    diff --git a/deployment-support/src/site/docbook/reference/welcome-usage.xml b/deployment-support/src/site/docbook/reference/welcome-usage.xml new file mode 100644 index 000000000..8db039745 --- /dev/null +++ b/deployment-support/src/site/docbook/reference/welcome-usage.xml @@ -0,0 +1,807 @@ + + + Usage and Conventions + + In this chapter we'll introduce how to use the Roo tool itself. We'll + cover typical conventions you'll experience when using Spring Roo. + +
    + Usability Philosophy + + As mentioned in earlier chapters and is easily experienced by simply + using Spring Roo for a project, we placed a great deal of emphasis on + usability during Roo's design. It is our experience that a normal + enterprise Java developer is able to pass the ten minute test with Roo and + build a new project without referring to documentation. There are several + conventions that we use within Roo to ensure a highly usable + experience: + + + + Numerous shell features which + ensure the primary Roo-specific user interface is friendly and + learnable + + + + Only using popular, mainstream technologies and + standards within Roo applications + + + + Ensuring Roo works with your choice of + IDE or no IDE at all + + + + Delivering an application + architecture that is easy to understand and avoids + "magic" + + + + Making sure Roo works the way a reasonable person would expect + it to + + + + Forgiving mistakes + + + + The last two points are what we're going to discuss in this + section. + + Making sure Roo works the way you would expect it to is reflected in + a number of key design decisions that basically boil down to "you can do + whatever you want, whenever you want, and Roo will automatically work in + with you". There are obviously limits to how far we can take this, but as + you use Roo you'll notice a few operational conventions that underpin + this. + + Let's start by looking at file conventions. Roo will never change a + .java file in your project unless you explicitly ask it to + via a shell command. In particular, Roo will not modify a + .java file just because you apply an annotation. Roo also + handles .xml files in the same manner. There are only two file types that + may be created, updated or deleted by Roo in an automatic manner, those + being .jspx files and also + AspectJ files which match the *_Roo_*.aj wildcard. + + In terms of the AspectJ files, Roo operates in a very specific + manner. A given AspectJ filename indicates the "target type" the members + will be introduced into and also the add-on which governs the file. Roo + will only ever permit a given AspectJ file to be preserved if the target + type exists and the corresponding add-on requests an ITD for that target + type. Nearly all add-ons will only create an ITD if there is a "trigger + annotation" on the target type, with the trigger annotation always + incorporating an @Roo prefix. As such, if you never put any @Roo + annotation on a given .java file, you can be assured Roo will never create + any AspectJ ITD for that target type. Refer to the file system conventions section for + related information. + + You'll also notice when using Roo that it automatically responds to + changes you make outside Roo. This is achieved by an auto-scaling file system + monitoring mechanism. This basically allows you to create, edit or + delete any file within your project and if the Roo shell is running it + will immediately detect your change and take the necessary action in + response. This is how round-tripping works without you needing to include + Roo as part of your build system or undertake any crude mass generation + steps. + + What happens if the Roo shell isn't running? Will there be a problem + if you forget to load it and make a change? No. When Roo starts up it + performs a full scan of your full project file system and ensures every + automatically-managed file that should be created, updated or deleted is + handled accordingly. This includes a full in-memory rebuild of each file, + and a comparison with the file on disk to detect changes. This results in + a lot more robust approach than relying on relatively coarsely-grained + file system timestamp models. It also explains why if you have a very big + project it can take a few moments for the Roo shell to startup, as there + is no alternative but to complete this check for actions that happened + when Roo wasn't running. + + The automated startup-time scan is also very useful as you upgrade + to newer versions of Roo. Often a new version of Roo will incorporate + enhancements to the add-ons that generate files in your project. The + startup-time scan will therefore automatically deliver improvements to all + generated files. This is also why you cannot edit files that Roo is + responsible for managing, because Roo will simply consider your changes as + some "old format" of the file and rewrite the file in accordance with its + current add-ons. + + Not being able to edit the generated files may sound restrictive, as + often you'll want to fine-tune just some part of the file that Roo has + emitted. In this case you can either write a Roo add-on, or more commonly + just write the method (or field or constructor etc) directly in your .java + file. Roo has a convention of detecting if any member it intends to + introduce already exists in the target type, and if it does Roo will not + permit the ITD to include that member. In plain English that means if you + write a method that Roo was writing, Roo will remove the method from its + generated file automatically and without needing an explicit directive to + do so. In fact the Roo core infrastructure explicitly detects buggy + add-ons that are trying to introduce members that an end user has written + and it will throw an exception to prevent the add-on from doing so. + + This talk of exceptions also lets us cover the related usability + feature of being forgiving. Every time Roo changes your file system or + receives a shell command, it is executed within a quasi-transactional + context that supports rollback. As a result, if anything goes wrong (such + as you made a mistake when entering a command or an add-on has a problem + for whatever reason) the file system will automatically rollback to the + state it was before the change was attempted. The cascading nature of many + changes (i.e. you add a field to a .java file and that + changes an AspectJ ITD and that in turn changes a web .jspx + etc) is handled in the same unit of work and therefore rolled back as an + atomic group when required. + + Before leaving this discussion on usability, it's probably worth + pointing out that although the Roo shell contains numerous commands, you don't need to use + them. You are perfectly free to perform any change to your file system by + hand (without the help of the Roo shell). For example, there are commands + which let you create .java files or add fields to them. You + can use these commands or you can simply do this within your IDE or text + editor. Roo's automatic file system monitoring will detect the changes and + respond accordingly. Just work the way you feel most comfortable - Roo + will respect it. +
    + +
    + Shell Features + + Many people who first look at Roo love the shell. In fact when we + first showed Roo to an internal audience, one of the developers present + said tounge-in-cheek, "That could only have come from someone with a deep + love of the Linux command line!". All jokes aside, the shell is only one + part of the Roo usability story - + although it's a very important part. Here are some of the usability + features that make the shell so nice to work with: + + + + Tab completion: The cornerstone of + command-line usability is tab assist. Hit TAB (or CTRL+SPACE if + you're in SpringSource Tool Suite) + and Roo will show you the applicable options. + + + + Command hiding: Command hiding will + remove commands which do not make sense given the current context of + your project. For example, if you're in an empty directory, you can + type project, hit TAB, + and see the options for creating a project. But once you've created + the project, the project command is no longer visible. The same + applies for most Roo commands. This is nice as it means you only see + commands which you can actually use right now. Of course, a full + list of commands applicable to your version of Roo is available in + the command index appendix and + also via help. + + + + Contextual awareness: Roo remembers the + last Java type you are working with in your current shell session + and automatically treats it as the argument to a command. You always + know what Roo considers the current context because the shell prompt + will indicate this just before it writes roo>. In the command index you might find + some options which have a default value of '*'. This is + the marker which indicates "the current context will be used for + this command option unless you explicitly specify otherwise". You + change the context by simply working with a different Java type + (i.e. specify an operation that involves a different Java type and + the context will change to that Java type). + + + + Hinting: Not sure what to do next? Just + use the hint command. It's + the perfect lightweight substitute for documentation if you're in a + hurry! + + + + Inbuilt help: If you'd like to know all + the options available for a given command, use the help command. It lists every + option directly within the shell. + + + + Automatic inline help: Of course, it's a + bit of a pain to have to go to the trouble of typing help then hitting enter if + you're in the middle of typing a command. That's why we offer inline + help, which is automatically displayed whenever you press TAB. It is + listed just before the completion options. To save screen space, we + only list the inline help once for a given command option. So if you + type project --template TAB TAB TAB, the first time you + press TAB you'd see the inline help and the completion + options + + + + Scripting and script recording: Save your + Roo commands and play them again later. + + The scripting and script recording features are + particularly nice, because they let you execute a series of Roo commands + without typing them in. + + To execute a Roo script, just use the script command. When you use the + script command you'll need to indicate the script to run. We ship a number + of sample scripts with Roo, as discussed earlier in the Exploring Roo Samples + section. + + What if you want to create your own scripts? All you need is a text + editor. The syntax of the script is identical to what you'd type at the + Roo shell. Both the Roo shell and your scripts can contain inline comments + using the ; and // markers, as well as block + comments using the /* */ syntax. + + A really nice script-related feature of the Roo shell is that it + will automatically build a script containing the commands you entered. + This file is named log.roo and exists in your current working + directory. Here's a quick example of the contents: + + // Spring Roo ENGINEERING BUILD [rev 553:554M] log opened at 2009-12-31 08:10:58 +project --topLevelPackage roo.shell.is.neat +// [failed] jpa setup --database DELIBERATE_ERROR --provider HIBERNATE +jpa setup --database HYPERSONIC_IN_MEMORY --provider HIBERNATE +quit +// Spring Roo ENGINEERING BUILD [rev 553:554M] log closed at 2009-12-31 08:11:37 + + In the recorded script, you can see the version number, session + start time and session close times are all listed. Also listed is a + command I typed that was intentionally incorrect, and Roo has turned that + command into a comment within the script (prefixed with // + [failed]) so that I can identify it and it will not execute should + I run the script again later. This is a great way of reviewing what you've + done with Roo, and sharing the results with others. +
    + +
    + IDE Usage + + Despite Roo's really nice shell, in reality most people develop most + of their application using an IDE or at least text editor. Roo fully + expects this usage and supports it. + + Before we cover how to use an IDE, it's worth mentioning that you + don't strictly need one. With Roo you can build an application at the + command line, although to be honest you'll get more productivity via an + IDE if it's anything beyond a trivial application. If you would prefer to + use the command line, you can start a fresh application using the Roo + shell, edit your .java and other files using any text editor, + and use the perform + commands to compile, test and package your application ready for + deployment. You can even use mvn tomcat:run to execute a + servlet container, and Roo add-ons let you deploy straight to a cloud + environment like Google App Engine. Again, you'll be more productive in an + IDE, but it's nice to know Roo doesn't force you to use an IDE unless + you'd like to use one. + + In relation to IDEs, we highly recommend that you use SpringSource Tool + Suite (STS). STS is a significantly extended version (and free!) of + the pervasive Eclipse IDE. From a Roo perspective, STS preintegrates the + latest AspectJ Development + Tools (AJDT) and also offers an inbuilt Roo shell. The inbuilt Roo + shell means you do not need to run the normal Roo shell if you are using + STS. You'll also have other neat Roo-IDE integation features, like the + ability to press CTRL+R (or Apple+R if you're on an Apple) and a popup + will allow you to type a Roo command from anywhere within the IDE. Another + nice feature is the shell message hotlinking, which means all shell + messages emitted by Roo are actually links that you can click to open the + corresponding file in an Eclipse editor. There are other goodies too, like + extra commands to deploy to SpringSource tc Server. + + You'll need to use STS 2.5 if you'd like to use Roo 1.1, which at + the time of writing represents the latest version of both tools. Because + the release cycle of STS and Roo differ, when you download STS you'll + generally find it includes a version of Roo that might not be the absolute + latest. This is not a problem. All you need to do is ensure you're using + the latest release of STS and then within the IDE select Window > + Preferences > Spring > Roo Support. Next select "Add..." and find + the directory which contains the latest Roo release. You probably also + want to tick the newly-selected Roo release, making it the default for + your projects when they're imported into STS. + + Naturally Roo works well with standard Eclipse as well. All you need + to do is ensure you install the latest AspectJ Development Tools + (AJDT) plugin. This will ensure code assist and incremental compilation + works well. We also recommend you go into Window > Preferences > + General > Workspace and switch on the "Refresh automatically" box. That + way Eclipse will detect changes made to the file system by the + externally-running Roo shell. It's also recommended to install the + m2eclipse plugin, which is automatically included if you use STS and is + particularly suitable for Roo-based projects. + + When using AJDT you may encounter a configuration option enabling + you to "weave" the JDT. This is on by default in STS, so you're unlikely + to see the message if using STS. If you are prompted (or locate the + configuration settings yourself under the Window > Preferences > JDT + Weaving menu), you should enable weaving. This ensures the Java Editor in + Eclipse (or STS) gives the best AspectJ-based experience, such as code + assist etc. You can also verify this setting is active by loading Eclipse + (or STS) and selecting Window > Preferences > JDT Weaving. + + If you're using m2eclipse, you won't need to use the perform eclipse command to + setup your environment. A simple import of the project using Eclipse's + File > Import > General > Maven Projects menu option is + sufficient. + + Irrespective of how you import your project into Eclipse (i.e. via + the perform eclipse + command or via m2eclipse) you should be aware that the project will not be + a Web Tools Project (WTP) until such time as you install your first web + controller. This is usually undertaken via the web mvc all or web mvc controller + command. If you have already imported your project into Eclipse, simply + complete the relevant web mvc command and then + re-import. The project will then be a WTP and offer the ability to deploy + to an IDE-embedded web container. If you attempt to start a WTP server and + receive an error message, try right-clicking the project and selecting + Maven > Update Project Configuration. This often resolves the + issue. + + If you're using IntelliJ, we are pleased to report that IntelliJ now + supports Roo. This follows the completion of ticket IDEA-26959, + where you can obtain more information about the AspectJ support now + available in IntelliJ. + + If you're using any IDE other than STS, the recommended operating + pattern is to load the standalone Roo shell in one operating system window + and leave it running while you interact with your IDE. There is no formal + link between the IDE and Roo shell. The only way they "talk" to each other + is by both monitoring the file system for changes made by the other. This + happens so quickly that you're unlikely to notice, and indeed internally + to Roo we have an API that allows the polling-based approach to be + replaced with a formal notification API should it ever become necessary. + As discussed in the usability + section, if you forget to load the Roo shell and start modifying + your project anyway, all you need to do is load the Roo shell again and it + will detect any changes it needs to make automatically. +
    + +
    + Build System Usage + + Roo currently supports the use of Apache Maven. This is a common + build system used in many enteprise applications. We routinely poll our + community and look at public surveys which consistently show that nearly + all enterprise development projects use either Maven or Ant, so we believe + this is a good default for Roo projects. As per the installation instructions, you must + ensure you are using Maven 2.0.9 or above. We do recommend you use Maven + 2.2 for best results, though. + + Roo will create a new pom.xml file whenever you use the + project command. The POM will + contain the following Roo-specific considerations: + + + + A reference to the Roo annotations JAR. This JAR exists at + development time only and has a scope that prevents it from being + included in resultant WAR files. + + + + A correct configuration of the Maven AspectJ plugin. This + includes a reference to the Spring Aspects library, which is important + to Roo-based applications. Spring Aspects is included within Spring + Framework. + + + + There are no other Roo changes to the POM. In particular, there is + no requirement for the POM to include Roo as part of any code generation + step. Roo is never used in this "bulk generation style". + + If you are interested in ensuring a build includes the latest Roo + code generation output, you can cause Maven or equivalent build system to + execute roo quit. The presentation of the quit command line + option will cause the Roo shell to load, perform its startup-time scan + (which identifies and completes any required changes to generated files) + and then exit. + + Those seeking Ant/Ivy instead of Maven support are encouraged to + vote for issue ROO-91. + The internals of Roo do not rely on Maven at all. Nonetheless we have + deferred it until we see sufficient community interest to justify + maintaining two build system environments. +
    + +
    + File System Conventions + + We have already covered some of Roo's file system conventions in the + Usability Philosophy section. In + summary Roo will automatically monitor the file system for changes and + code generate only those files which match the *_Roo_*.aj + wildcard. It will also code generate those JSPs associated with scaffolded MVC controllers that have + the annotation @RooWebScaffold. + + Roo applications follow the standard Maven-based directory layout. + We have also placed Spring application context-related files (both + .xml and .properties) in the recommended + classpath sub-directory for Spring applications, + META-INF/spring. +
    + +
    + Add-On Installation and Removal + + Roo supports the installation and removal of third-party add-ons. + Roo 1.1 added significant enhancements to its add-on model, as more + thoroughly discussed in Part III of this + manual. +
    + +
    + Recommended Practices + + Following some simple recommendations will ensure you have the best + possible experience with Roo: + + + + Don't edit any files that Roo code generates (see the Usability Philosophy for + details). + + + + Before installing any new technology, check if Roo offers a + setup command and use it if present (this will ensure the seutp + reflects our recommendations and the expectations of other + add-ons). + + + + Ensure you leave the Roo shell running when creating, updating + or deleting files in your project. + + + + Remember you'll still need to write Java code (and JSPs for + custom controllers). Have the right expectations before you start + using Roo. It just helps you - it doesn't replace the requirement to + program. + + + + Check the Known + Issues section before upgrading or if you experience any + problems. + + + + Refer to the Roo Resources + section for details of how to get assistance with Roo, such as the + forum and issue tracking database. We're happy to hear from + you. + + +
    + +
    + Managing Roo Add-Ons + + + Modifying PGP Trusts For httppgp:// Scheme Operation + + As detailed in the main text, Roo supports a special protocol + scheme called httppgp://. This performs a Pretty Good + Privacy (PGP) detached signature verification before proceeding to + download the main resource. We use this as a key foundation of our + add-on security model. Many Roo commands download items from the + Internet, and anytime a httppgp:// scheme is encountered + a PGP verification will take place. + + One common case is if you are using the addon install command. + An example of the error if the PGP detached signature is untrusted is + shown below: + + roo> addon install --bundleSymbolicName de.saxsys.roo.equals.addon +Download URL 'http://[...]equals.addon-1.2.0.jar' failed +This resource was signed with PGP key ID '0xC3A61B10', +which is not currently trusted +Use 'pgp key view' to view this key, 'pgp trust' to trust it, +or 'pgp automatic trust' to trust any keysEssentially you + need to decide if you trust the PGP key ID or not. There is a pgp key view command that + will help you learn more about a given key ID if you would like to use + it. You can also view keys at public PGP key servers such as http://pgp.mit.edu/. You + essentially have two options to cause an untrusted httppgp download to + be performed by Roo: + + + + Use the pgp + trust command to trust the PGP key ID shown in the error + message. This will permanently trust the key ID, and it will show + up if you use the pgp list trusted + keys command (you can of course remove it via the pgp untrust command as + well). All of the keys you trust are stored in + ~/.spring_roo_pgp.bpg, which is a binary encoded PGP + key store which you can also view and manage using normal PGP + tools. An example of the command to trust a key is shown + below:roo> pgp trust --keyId 0xC3A61B10 + + + + Alternately, you can decide to simply switch off key + verification and automatically trust any keys encountered. Such + keys are stored in your ~/.spring_roo_pgp.bpg file. + You should use caution with this command, although it can be + convenient if you'd simply like to install some new add-ons and + their dependencies without considering every key used to sign + them. To use automatic trust, simply type pgp automatic + trust and press enter:roo> pgp automatic trust +Automatic PGP key trusting enabled (this is potentially unsafe); +disable by typing 'pgp automatic trust' again + + + + Once one of the above have been completed, you can repeat the + command that attempted to download a httppgp:// resource + and it should succeed. + + + It is easy to extend the capabilities of Spring Roo with installable + add-ons. This section will offer a basic overview of Roo's add-on + distribution model and explain how to install new add-ons. If you're + considering writing an add-on, please refer to the more advanced + information in Part III of this reference + guide. + + First of all, it's important to recognize that Roo ships with a + large number of base add-ons. These built-in add-ons may be all you ever + require. Nevertheless, there is a growing community of add-ons written by + people outside the core Roo team. Because the core Roo team do not write + these add-ons, we've needed to implement an infrastructure so that + external people can share their add-ons and make it easy for you to + install them. + + Roo's add-on distribution system encourages individual add-on + developers to host their add-on web site (we don't believe in a central + model where we must host add-ons on our servers). The main requirement an + add-on developer needs to fulfill is their add-ons must be in OSGi format + and their web site must include an OSGi Bundle Repository (OBR) index + file. While Roo internally uses OSGi and all modules are managed as OSGi + bundles, this is transparent and you do not need any familiarity with OSGi + or bundles to work with the Roo add-on installation system. An OBR file is + usually named repository.xml and it is available over HTTP. + If you're curious what these OBR files look like, you can view the Spring + Roo OBR repository at http://spring-roo-repository.springsource.org/repository.xml. + Within an OBR file each available Roo-related add-on is listed, along with + the URL where it is published. The URLs look similar to normal URLs, + except they will usually specify a httppgp:// protocol scheme + (instead of the more common http://). + + The httppgp:// protocol scheme is how we achieve a + level of security with add-ons. Obviously with every add-on developer able + to host add-ons on any web site they nominate, it would be difficult for + you to know whether a particular add-on can be trusted. You probably only + want to trust add-ons from people you already trust or have cause to + trust. To this end Roo offers automatic PGP-related signature capabilities + for any URL that uses the httppgp:// scheme. Most Roo add-ons + use this scheme. The internal step-by-step process that takes place is Roo + essentially downloads the URL + ".asc" over HTTP. This file is a standard + PGP detached signature file. PGP detached signature files are increasingly + common, with most Maven Central artifacts now also offering a signature + file. If the user's Roo installation trusts the key ID that signed the PGP + detached signature, Roo will proceed to download the URL. If the user's + Roo installation does not trust the key ID, an error will be displayed and + the download will fail (and in turn the add-on installation process will + fail if the bundle was specified as a httppgp:// URL). Please + see the side-bar for details on how you can trust different key IDs and + use the PGP-related commands in Roo. + + Completing the picture of Roo's add-on distribution infrastructure + is RooBot. This is a VMware-hosted service that essentially indexes the + important content in all public Roo OBR files. RooBot ensures that add-ons + it indexes are only available over httppgp://, reflecting the + security model above. Add-on developers can be added into RooBot's index + in just a couple of minutes via an automated process. Every time Roo + loads, it automatically downloads the latest RooBot index file. This is + how it knows which public add-ons are available. + + Enough with the theory, let's move on to the fun piece. In Spring + Roo you simply use the shell to locate new add-ons. To review the list of + known add-ons you can use the addon list or addon search command. This + lists all add-ons that are in the RooBot-maintained index mentioned + above: + + roo> addon search +1234 found, sorted by rank; T = trusted developer; R = Roo 1.1 compatible +ID T R DESCRIPTION ------------------------------------------------------------- +01 Y Y 2.3.0.0001 This bundle wraps the standard Maven artifact: + protobuf-java-2.3.0-lite. +02 Y - 0.3.0.RELEASE Addon for Spring Roo to provide generic DAO and query + methods based on Hades. +03 Y Y 0.9.94.0001 This bundle wraps the standard Maven artifact: + jline-0.9.94.S2-A (S2-A is a private patched version; see ROO-350 for... +04 - - 1.1.6 Addons that adds Content Negotiating View Resolver configuration + to your application context: MVC multiple representations By default... +...(output truncated for reference guide inclusion)... +[HINT] use 'addon info id --searchResultId ..' to see details about a search result +[HINT] use 'addon install id --searchResultId ..' to install a specific search result, or +[HINT] use 'addon install bundle --bundleSymbolicName TAB' to install a specific add-on versionThere + are various options you can pass to the search command to see more lines + per result, perform filtering and so on. Just use --TAB as usual to see + these options. + + If you can't see the add-on you're looking for, you can repeat the + command with the optional --refresh option. This will refresh + your local RooBot index from our server. + + To review details about a specific add-on, use the addon info id command as + mentioned in the hint at the bottom of the search results. There is also a + related command called addon info bundle which + requires a "bundle symbolic name", which is usually the add-on's top-level + package. However, it's often more convenient to use the search result "ID" + number (to the left hand side of each row) rather than typing out a bundle + symbolic name. Let's try this. To view details about the second add-on + listed, enter this command: + + roo> addon info id --searchResultId 02An + example of the output of addon + info id is shown below: + + roo> addon info id --searchResultId 02 +Name.........: Hades - Roo addon +BSN..........: org.synyx.hades.roo.addon +Version......: 0.3.0.RELEASE +Roo Version..: 1.1.0 +Ranking......: 1.0 +JAR Size.....: 20458 bytes +PGP Signature: 0xF2C57936 signed by Oliver Gierke (info@olivergierke.de) +OBR URL......: http://hades.synyx.org/static/roo/repo/repository.xml +JAR URL......: httppgp://hades.synyx.org/static/roo/repo/org/synyx/hades/org.syn + yx.hades.roo.addon/0.3.0.RELEASE/org.synyx.hades.roo.addon-0.3.0. + RELEASE.jar +Commands.....: 'hades install' [Installs Hades for the project] +Commands.....: 'hades repository' [Creates a Hades repository interface] +Description..: Addon for Spring Roo to provide generic DAO and query methods + based on Hades. +Comment 1....: Rating [GOOD], Date [17/12/10], Comment [Nice add-on for those + who want to use a separate repository layer, can be improved in + functionality] +In the above output "BSN" means bundle symbolic name, which + is the alternate way of referring to a given add-on. The output also shows + you the Roo shell commands that are available via the add-on. These + commands are automatically seen by the Roo shell, so if you typed in this + case "hades install" without first having installed the add-on, Roo would + have performed a search and shown you this add-on offered the command. + This is a great feature and means you can often just type commands you + think you might need and find out which add-ons offer them without + performing an explicit search. A similar feature exists for JDBC + resolution if you try to reverse engineer a database for which there is no + installed JDBC driver (Roo will automatically suggest the add-on you need + and instruct you which command to use to install it). + + If you decide to install a specific add-on, simply use the addon install id + command: + + roo> addon install id --searchResultId 02 +Successfully installed add-on: org.synyx.hades.roo.addon +[Hint] Please consider rating this add-on with the following command: +[Hint] addon feedback bundle --bundleSymbolicName org.synyx.hades.roo.addon --rating ... --comment "..." + + If the add-on installation is aborted with a warning that the add-on + author is currently not trusted, please review the sidebar about modifying + PGP trusts. To simplify identifying add-ons from developers you already + trust, the addon search + results include a "T" column which means "trusted developer". If you see a + "Y" in that column, you've already trusted that developer's PGP key and + thus installation will work without needing to add their key. If you see a + "-" in that column, you'll need to first tell Roo you trust their key (as + explained in the PGP sidebar). + + As per the [HINT] messages that appear immediately after installing + an add-on, we appreciate your feedback about the add-ons you use. You can + use the addon feedback + bundle command for this purpose, as shown in the console text + above. If you provide a rating or comment, it will show up for other + people to see when they use the addon info command. + + It is generally recommended to restart Roo to ensure the add-on is + properly initialized. This theoretically isn't necessary in most cases, + but it doesn't hurt. + + You can also upgrade your existing add-ons by using the addon + upgrade commands. To do this you should first run the addon upgrade + settings command which allows you to define the desired stability + level which is taken into account when performing the addon upgrade all + command:roo> addon upgrade settings --addonStabilityLevel ANY|MILESTONE|RELEASE|RELEASE_CANDIDATE + + If you don't define a stability level through the addon upgrade + settings command it defaults to RELEASE - meaning only release versions + will be upgraded (if upgrades for this level are available). Other + stability levels to choose from are RELEASE_CANDIDATE, MILESTONE, and ANY + (i.e. snapshots). + + To list all available upgrades for currently installed add-ons you + can use the addon + upgrade available command. This will provide an overview of add-ons + which can be upgraded and their respective stability levels. Furthermore, + you can also upgrade individual add-ons by using the addon upgrade bundle + command which allows you to specify the add-on bundle symbolic name (and + the add-on version in case multiple versions are available). Finally, you + can use the addon upgrade + id command to upgrade a specific add-on which has appeared in a + search result to the latest version available. + + Of course, you can remove add-ons as well. To uninstall any given + add-on, just use the addon + remove command. On this occasion we'll use the bundle symbolic name + (which is available via TAB completion as is usual with + Roo):roo> addon remove --bundleSymbolicName de.saxsys.roo.equals.addon +Successfully removed add-on: de.saxsys.roo.equals.addon + + Note that all of the "addon" commands only work with add-ons listed + in the central RooBot index file. This is fine, as most public Roo add-ons + are listed there. However, sometimes an add-on cannot be published into + the RooBot index file. The most common reason is that it's an add-on + internal to your organization, or perhaps it's simply not ready for public + consumption. + + Even if an add-on is not listed in RooBot, you can still install it. + The "osgi obr url + add" command can be used to add the add-on's OBR URL to your Roo + installation. This command is typically followed by an "osgi obr start" command to + download and start the add-on. Importantly, the additional security + verifications performed by RooBot are skipped given RooBot is not used + with these commands (or other related commands such as osgi start). That means bundles + you start using the "osgi obr start" command may not use + httppgp:// for PGP signature verification. As such you should + exercise caution when using any installation-related commands that do not + start with "addon", as such commands do not use resources subject to the + RooBot security verifications. Noneless there remain legitimate use cases + for such distribution styles, so it's good to know Roo supports them as + well as the more common, user-friendly and more secure "addon" + commands. +
    +
    diff --git a/deployment-support/src/site/resources/images/logos/i21-banner-1.jpg b/deployment-support/src/site/resources/images/logos/i21-banner-1.jpg new file mode 100644 index 000000000..c72b017b9 Binary files /dev/null and b/deployment-support/src/site/resources/images/logos/i21-banner-1.jpg differ diff --git a/deployment-support/src/site/site.xml b/deployment-support/src/site/site.xml new file mode 100644 index 000000000..3cda53f54 --- /dev/null +++ b/deployment-support/src/site/site.xml @@ -0,0 +1,33 @@ + + + + + ${project.name} + http://projects.spring.io/spring-roo/ + + + + + images/shim.gif + + + + + + + + org.springframework.maven.skins + + maven-spring-skin + 1.0.5 + + + + + + + + + + diff --git a/felix/legal-felix.txt b/felix/legal-felix.txt new file mode 100644 index 000000000..960ac29e4 --- /dev/null +++ b/felix/legal-felix.txt @@ -0,0 +1,45 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: Legion of the Bouncy Castle Java Cryptography APIs +Software Web Site: http://www.bouncycastle.org/java.html +Effective License: Bouncy Castle License (MIT X11 License adaptation) +License Info Page: http://www.bouncycastle.org/licence.html + +Bouncy Castle is a runtime dependency of this module. It provides PGP +key management and related signature verification features. Some of +the source files in org.springframework.roo.felix.pgp were derived +from Bouncy Castle sample code and tests. + +License Text (from the above License Info Page): + +Copyright (c) 2000 - 2009 The Legion Of The Bouncy Castle +(http://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to + the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/felix/pom.xml b/felix/pom.xml new file mode 100644 index 000000000..ec1b2ee7d --- /dev/null +++ b/felix/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.felix + bundle + Spring Roo - Felix Interoperability + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + org.apache.felix + org.apache.felix.gogo.command + + + org.apache.felix + org.apache.felix.gogo.runtime + + + + org.apache.felix + org.apache.felix.log + + + + org.apache.felix + org.apache.felix.bundlerepository + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + org.springframework.roo + org.springframework.roo.url.stream + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.shell.osgi + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.bcpg-jdk15 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.bcprov-jdk15 + + + diff --git a/felix/src/main/java/org/springframework/roo/felix/BundleSymbolicName.java b/felix/src/main/java/org/springframework/roo/felix/BundleSymbolicName.java new file mode 100644 index 000000000..b871eee5a --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/BundleSymbolicName.java @@ -0,0 +1,95 @@ +package org.springframework.roo.felix; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * Represents a Bundle Symbolic Name. + * + * @author Ben Alex + * @since 1.0 + */ +public class BundleSymbolicName implements Comparable { + + private final String key; + + public BundleSymbolicName(final String key) { + Validate.notBlank(key, "Key required"); + this.key = key; + } + + public final int compareTo(final BundleSymbolicName o) { + if (o == null) { + return -1; + } + return key.compareTo(o.key); + } + + @Override + public final boolean equals(final Object obj) { + return obj instanceof BundleSymbolicName + && compareTo((BundleSymbolicName) obj) == 0; + } + + /** + * Locates the bundle ID for this BundleSymbolicName, if available. + * + * @param context to search (required) + * @return the ID (or null if cannot be found) + */ + public Long findBundleIdWithoutFail(final BundleContext context) { + Validate.notNull(context, "Bundle context is unavailable"); + final Bundle[] bundles = context.getBundles(); + if (bundles == null) { + throw new IllegalStateException( + "Bundle IDs cannot be retrieved as BundleContext unavailable"); + } + for (final Bundle b : bundles) { + if (getKey().equals(b.getSymbolicName())) { + return b.getBundleId(); + } + } + throw new IllegalStateException("Bundle symbolic name '" + getKey() + + "' has no local bundle ID at this time"); + } + + /** + * Locates the Bundle for this BundleSymbolicName, if available + * + * @param context + * @return + */ + public Bundle findBundleWithoutFail(final BundleContext context) { + Validate.notNull(context, "Bundle context is unavailable"); + final Bundle[] bundles = context.getBundles(); + if (bundles == null) { + throw new IllegalStateException( + "Bundle cannot be retrieved as BundleContext unavailable"); + } + for (final Bundle b : bundles) { + if (getKey().equals(b.getSymbolicName())) { + return b; + } + } + throw new IllegalStateException("Bundle symbolic name '" + getKey() + + "' has no local bundle at this time"); + } + + public String getKey() { + return key; + } + + @Override + public final int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("key", key); + return builder.toString(); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/BundleSymbolicNameConverter.java b/felix/src/main/java/org/springframework/roo/felix/BundleSymbolicNameConverter.java new file mode 100644 index 000000000..f17381b45 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/BundleSymbolicNameConverter.java @@ -0,0 +1,103 @@ +package org.springframework.roo.felix; + +import java.util.List; + +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.Bundle; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link BundleSymbolicName}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class BundleSymbolicNameConverter implements + Converter { + + private ComponentContext context; + // Handler service field is solely to ensure it starts before + // BundleSymbolicNameConverter + @Reference protected HttpPgpUrlStreamHandlerService handlerService; + @Reference private RepositoryAdmin repositoryAdmin; + + protected void activate(final ComponentContext context) { + this.context = context; + } + + public BundleSymbolicName convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new BundleSymbolicName(value.trim()); + } + + protected void deactivate(final ComponentContext context) { + this.context = null; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String originalUserInput, + final String optionContext, final MethodTarget target) { + boolean local = false; + boolean obr = false; + + if ("".equals(optionContext)) { + local = true; + } + + if (optionContext.contains("local")) { + local = true; + } + + if (optionContext.contains("obr")) { + obr = true; + } + + if (local) { + final Bundle[] bundles = context.getBundleContext().getBundles(); + if (bundles != null) { + for (final Bundle bundle : bundles) { + final String bsn = bundle.getSymbolicName(); + if (bsn != null && bsn.startsWith(originalUserInput)) { + completions.add(new Completion(bsn)); + } + } + } + } + + if (obr) { + final Repository[] repositories = repositoryAdmin + .listRepositories(); + if (repositories != null) { + for (final Repository repository : repositories) { + final Resource[] resources = repository.getResources(); + if (resources != null) { + for (final Resource resource : resources) { + if (resource.getSymbolicName().startsWith( + originalUserInput)) { + completions.add(new Completion(resource + .getSymbolicName())); + } + } + } + } + } + } + + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return BundleSymbolicName.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/felix/src/main/java/org/springframework/roo/felix/FelixDelegator.java b/felix/src/main/java/org/springframework/roo/felix/FelixDelegator.java new file mode 100644 index 000000000..d9422a5d1 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/FelixDelegator.java @@ -0,0 +1,363 @@ +package org.springframework.roo.felix; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.ExitShellRequest; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.converters.StaticFieldConverter; +import org.springframework.roo.shell.event.ShellStatus; +import org.springframework.roo.shell.event.ShellStatus.Status; +import org.springframework.roo.shell.event.ShellStatusListener; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.logging.LoggingOutputStream; + +/** + * Delegates to commands provided via Felix's Shell API. + *

    + * Also monitors the Roo Shell to determine when it wishes to shutdown. This + * shutdown request is then passed through to Felix for processing. + * + * @author Ben Alex + */ +@Component +@Service +public class FelixDelegator implements CommandMarker, ShellStatusListener { + private BundleContext context; + @Reference private Shell rooShell; + @Reference private CommandProcessor commandProcessor; + @Reference private StaticFieldConverter staticFieldConverter; + + protected static final Logger LOGGER = HandlerUtils + .getLogger(LoggingOutputStream.class); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + rooShell.addShellStatusListener(this); + staticFieldConverter.add(LogLevel.class); + staticFieldConverter.add(PsOptions.class); + } + + protected void deactivate(final ComponentContext context) { + this.context = null; + rooShell.removeShellStatusListener(this); + staticFieldConverter.remove(LogLevel.class); + staticFieldConverter.remove(PsOptions.class); + } + + @CliCommand(value = "osgi headers", help = "Display headers for a specific bundle") + public void headers( + @CliOption(key = "bundleSymbolicName", mandatory = false, help = "Limit results to a specific bundle symbolic name") final BundleSymbolicName bsn) + throws Exception { + + if (bsn == null) { + perform("headers"); + } + else { + // ROO-3573: Gets Bundle using context and show headers + Bundle bundle = bsn.findBundleWithoutFail(context); + Dictionary bundleHeaders = bundle.getHeaders(); + + LOGGER.log(Level.INFO, String.format("%s - (%s)",bundleHeaders.get("Bundle-Name"), bundle.getBundleId())); + LOGGER.log(Level.INFO, "-------------------------------------------"); + + Enumeration bundleHeadersKeys = bundleHeaders.keys(); + Enumeration bundleHeadersElements = bundleHeaders.elements(); + + while(bundleHeadersKeys.hasMoreElements()){ + String key = bundleHeadersKeys.nextElement(); + String element = bundleHeadersElements.nextElement(); + LOGGER.log(Level.INFO, String.format("%s = %s", key, element)); + } + + LOGGER.log(Level.INFO, ""); + + } + } + + @CliCommand(value = "osgi install", help = "Installs a bundle JAR from a given URL") + public void install( + @CliOption(key = "url", mandatory = true, help = "The URL to obtain the bundle from") final String url) + throws Exception { + + perform("install " + url); + } + + @CliCommand(value = "osgi log", help = "Displays the OSGi log information") + public void log( + @CliOption(key = "maximumEntries", mandatory = false, help = "The maximum number of log messages to display") final Integer maximumEntries, + @CliOption(key = "level", mandatory = true, help = "The minimum level of messages to display") final LogLevel logLevel) + throws Exception { + + final StringBuilder sb = new StringBuilder(); + sb.append("log"); + if (maximumEntries != null) { + sb.append(" ").append(maximumEntries); + } + if (logLevel != null) { + sb.append(" ").append(logLevel.getFelixCode()); + } + perform(sb.toString()); + } + + @CliCommand(value = "osgi ps", help = "Displays OSGi bundle information") + public void log( + @CliOption(key = "format", mandatory = false, specifiedDefaultValue = "BUNDLE_NAME", unspecifiedDefaultValue = "BUNDLE_NAME", help = "The format of bundle information") final PsOptions format) + throws Exception { + + final StringBuilder sb = new StringBuilder(); + sb.append("lb"); + if (format != null) { + sb.append(format.getFelixCode()); + } + perform(sb.toString()); + } + + @CliCommand(value = "osgi obr deploy", help = "Deploys a specific OSGi Bundle Repository (OBR) bundle") + public void obrDeploy( + @CliOption(key = "bundleSymbolicName", mandatory = true, optionContext = "obr", help = "The specific bundle to deploy") final BundleSymbolicName bsn) + throws Exception { + + perform("obr:deploy " + bsn.getKey()); + } + + @CliCommand(value = "osgi obr info", help = "Displays information on a specific OSGi Bundle Repository (OBR) bundle") + public void obrInfo( + @CliOption(key = "bundleSymbolicName", mandatory = true, optionContext = "obr", help = "The specific bundle to display information for") final BundleSymbolicName bsn) + throws Exception { + + perform("obr:info " + bsn.getKey()); + } + + @CliCommand(value = "osgi obr list", help = "Lists all available bundles from the OSGi Bundle Repository (OBR) system") + public void obrList( + @CliOption(key = "keywords", mandatory = false, help = "Keywords to locate") final String keywords) + throws Exception { + + final StringBuilder sb = new StringBuilder(); + sb.append("obr:list -v"); + if (keywords != null) { + sb.append(" ").append(keywords); + } + perform(sb.toString()); + } + + @CliCommand(value = "osgi obr start", help = "Starts a specific OSGi Bundle Repository (OBR) bundle") + public void obrStart( + @CliOption(key = "bundleSymbolicName", mandatory = true, optionContext = "obr", help = "The specific bundle to start") final BundleSymbolicName bsn) + throws Exception { + + perform("start " + bsn.getKey()); + } + + @CliCommand(value = "osgi obr url add", help = "Adds a new OSGi Bundle Repository (OBR) repository file URL") + public void obrUrlAdd( + @CliOption(key = "url", mandatory = true, help = "The URL to add (eg http://felix.apache.org/obr/releases.xml)") final String url) + throws Exception { + + perform("obr:repos add " + url); + } + + @CliCommand(value = "osgi obr url list", help = "Lists the currently-configured OSGi Bundle Repository (OBR) repository file URLs") + public void obrUrlList() throws Exception { + perform("obr:repos list"); + } + + @CliCommand(value = "osgi obr url refresh", help = "Refreshes an existing OSGi Bundle Repository (OBR) repository file URL") + public void obrUrlRefresh( + @CliOption(key = "url", mandatory = true, help = "The URL to refresh (list existing URLs via 'osgi obr url list')") final String url) + throws Exception { + + perform("obr:repos refresh " + url); + } + + @CliCommand(value = "osgi obr url remove", help = "Removes an existing OSGi Bundle Repository (OBR) repository file URL") + public void obrUrlRemove( + @CliOption(key = "url", mandatory = true, help = "The URL to remove (list existing URLs via 'osgi obr url list')") final String url) + throws Exception { + + perform("obr:repos remove " + url); + } + + public void onShellStatusChange(final ShellStatus oldStatus, + final ShellStatus newStatus) { + if (newStatus.getStatus().equals(Status.SHUTTING_DOWN)) { + try { + if (rooShell != null) { + if (rooShell.getExitShellRequest() != null) { + // ROO-836 + System.setProperty("roo.exit", Integer + .toString(rooShell.getExitShellRequest() + .getExitCode())); + } + System.setProperty("developmentMode", + Boolean.toString(rooShell.isDevelopmentMode())); + } + perform("shutdown"); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + } + + private void perform(final String commandLine) throws Exception { + if("shutdown".equals(commandLine)) { + context.getBundle(0).stop(); + return; + } + + ByteArrayOutputStream sysOut = new ByteArrayOutputStream(); + ByteArrayOutputStream sysErr = new ByteArrayOutputStream(); + + final PrintStream printStreamOut = new PrintStream(sysOut); + final PrintStream printErrOut = new PrintStream(sysErr); + try { + final CommandSession commandSession = commandProcessor.createSession(System.in, printStreamOut, printErrOut); + Object result = commandSession.execute(commandLine); + + if(result != null) { + printStreamOut.println(commandSession.format(result, Converter.INSPECT)); + } + + if(sysOut.size() > 0) { + LOGGER.log(Level.INFO, new String(sysOut.toByteArray())); + } + + if(sysErr.size() > 0) { + LOGGER.log(Level.SEVERE, new String(sysErr.toByteArray())); + } + } + catch(Throwable ex) { + LOGGER.log(Level.SEVERE, ex.getMessage(), ex); + } + finally { + printStreamOut.close(); + printErrOut.close(); + } + } + + @CliCommand(value = { "exit", "quit" }, help = "Exits the shell") + public ExitShellRequest quit() { + return ExitShellRequest.NORMAL_EXIT; + } + + @CliCommand(value = "osgi resolve", help = "Resolves a specific bundle ID") + public void resolve( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The specific bundle to resolve") final BundleSymbolicName bsn) + throws Exception { + + perform("resolve " + + bsn.findBundleIdWithoutFail(context)); + } + + @CliCommand(value = "osgi scr config", help = "Lists the current SCR configuration") + public void scrConfig() throws Exception { + perform("scr:config"); + } + + @CliCommand(value = "osgi scr disable", help = "Disables a specific SCR-defined component") + public void scrDisable( + @CliOption(key = "componentId", mandatory = true, help = "The specific component identifier (use 'osgi scr list' to list component identifiers)") final Integer id) + throws Exception { + + perform("scr:disable " + id); + } + + @CliCommand(value = "osgi scr enable", help = "Enables a specific SCR-defined component") + public void scrEnable( + @CliOption(key = "componentId", mandatory = true, help = "The specific component identifier (use 'osgi scr list' to list component identifiers)") final Integer id) + throws Exception { + + perform("scr:enable " + id); + } + + @CliCommand(value = "osgi scr info", help = "Lists information about a specific SCR-defined component") + public void scrInfo( + @CliOption(key = "componentId", mandatory = true, help = "The specific component identifier (use 'osgi scr list' to list component identifiers)") final Integer id) + throws Exception { + + perform("scr:info " + id); + } + + @CliCommand(value = "osgi scr list", help = "Lists all SCR-defined components") + public void scrList( + @CliOption(key = "bundleId", mandatory = false, help = "Limit results to a specific bundle") final BundleSymbolicName bsn) + throws Exception { + + if (bsn == null) { + perform("scr:list"); + } + else { + perform("scr:list " + + bsn.findBundleIdWithoutFail(context)); + } + } + + @CliCommand(value = "osgi framework command", help = "Passes a command directly through to the Felix shell infrastructure") + public void shell( + @CliOption(key = "", mandatory = false, specifiedDefaultValue = "help", unspecifiedDefaultValue = "help", help = "The command to pass to Felix (WARNING: no validation or security checks are performed)") final String commandLine) + throws Exception { + + perform(commandLine); + } + + @CliCommand(value = "osgi start", help = "Starts a bundle JAR from a given URL") + public void start( + @CliOption(key = "url", mandatory = true, help = "The URL to obtain the bundle from") final String url) + throws Exception { + + perform("start " + url); + + LOGGER.log(Level.INFO, "Started!"); + LOGGER.log(Level.INFO, ""); + } + + @CliCommand(value = "osgi uninstall", help = "Uninstalls a specific bundle") + public void uninstall( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The specific bundle to uninstall") final BundleSymbolicName bsn) + throws Exception { + // ROO-3573: Gets Bundle using context and uninstall it + bsn.findBundleWithoutFail(context).uninstall(); + + LOGGER.log(Level.INFO, String.format("Bundle '%s' : Uninstalled!", bsn.getKey())); + LOGGER.log(Level.INFO, ""); + + } + + @CliCommand(value = "osgi update", help = "Updates a specific bundle") + public void update( + @CliOption(key = "bundleSymbolicName", mandatory = true, help = "The specific bundle to update ") final BundleSymbolicName bsn, + @CliOption(key = "url", mandatory = false, help = "The URL to obtain the updated bundle from") final String url) + throws Exception { + + // ROO-3573: Gets Bundle using context and update it + Bundle bundle = bsn.findBundleWithoutFail(context); + if (url == null) { + bundle.update(); + } + else { + bundle.update(new ByteArrayInputStream(url.getBytes())); + } + + LOGGER.log(Level.INFO, String.format("Bundle '%s' : Updated!", bsn.getKey())); + LOGGER.log(Level.INFO, ""); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/HttpPgpUrlStreamHandlerService.java b/felix/src/main/java/org/springframework/roo/felix/HttpPgpUrlStreamHandlerService.java new file mode 100644 index 000000000..b750e193e --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/HttpPgpUrlStreamHandlerService.java @@ -0,0 +1,11 @@ +package org.springframework.roo.felix; + +/** + * Marker interface for SCR reference usage of + * {@link HttpPgpUrlStreamHandlerServiceImpl}. + * + * @author Ben Alex + * @since 1.1 + */ +public interface HttpPgpUrlStreamHandlerService { +} diff --git a/felix/src/main/java/org/springframework/roo/felix/HttpPgpUrlStreamHandlerServiceImpl.java b/felix/src/main/java/org/springframework/roo/felix/HttpPgpUrlStreamHandlerServiceImpl.java new file mode 100644 index 000000000..e3a9f9d57 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/HttpPgpUrlStreamHandlerServiceImpl.java @@ -0,0 +1,159 @@ +package org.springframework.roo.felix; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Hashtable; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.url.AbstractURLStreamHandlerService; +import org.osgi.service.url.URLConstants; +import org.osgi.service.url.URLStreamHandlerService; +import org.springframework.roo.felix.pgp.PgpService; +import org.springframework.roo.felix.pgp.SignatureDecision; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.url.stream.UrlInputStreamService; + +/** + * Processes httppgp:// URLs. Does not handle HTTPS URLs. + *

    + * This implementation offers two main features: + *

      + *
    • It delegates the downloading process to {@link UrlInputStreamService} so + * that an alternate implementation can be added that may offer more advanced + * capabilities or configuration (eg as available from a hosting IDE)
    • + *
    • It downloads an .asc file (computed by the original URL + ".asc") and + * verifies the signature and that the user trusts the signing key (the .asc + * must be a detached armored signature, as produced via + * "gpg --armor --detach-sign file_to_sign.ext")
    • + *
    + *

    + * As such this module simplifies security management and proxy server + * compatibility for Spring Roo. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class HttpPgpUrlStreamHandlerServiceImpl extends + AbstractURLStreamHandlerService implements + HttpPgpUrlStreamHandlerService { + + private static final Logger LOGGER = HandlerUtils + .getLogger(HttpPgpUrlStreamHandlerServiceImpl.class); + + @Reference private PgpService pgpService; + @Reference private UrlInputStreamService urlInputStreamService; + + protected void activate(final ComponentContext context) { + final Hashtable dict = new Hashtable(); + dict.put(URLConstants.URL_HANDLER_PROTOCOL, "httppgp"); + context.getBundleContext().registerService( + URLStreamHandlerService.class.getName(), this, dict); + } + + @Override + public URLConnection openConnection(final URL u) throws IOException { + // Convert httppgp:// URL into a standard http:// URL + final URL resourceUrl = new URL(u.toExternalForm().replace("httppgp", + "http")); + // Add .asc to the end of the standard resource URL + final URL ascUrl = new URL(resourceUrl.toExternalForm() + ".asc"); + + // Start with the ASC file, as if this is for an untrusted key, there's + // no point download the larger resource + final File ascUrlFile = File.createTempFile("roo_asc", null); + ascUrlFile.deleteOnExit(); + + InputStream inputStream = null; + FileOutputStream outputStream = null; + try { + outputStream = new FileOutputStream(ascUrlFile); + inputStream = urlInputStreamService.openConnection(ascUrl); + IOUtils.copy(inputStream, outputStream); + } + catch (final IOException ioe) { + // This is not considered fatal; it is likely the ASC isn't + // available, so we will continue + ascUrlFile.delete(); + } + finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + + // Abort if a signature wasn't downloaded (this is a httppgp:// URL + // after all, so it should be available) + Validate.isTrue( + ascUrlFile.exists(), + "Signature verification file is not available at '%s'; continuing", + ascUrl.toExternalForm()); + + // Decide if this signature file is well-formed and of a key ID that is + // trusted by the user + InputStream resource = null; + InputStream signature = null; + try { + signature = new FileInputStream(ascUrlFile); + final SignatureDecision decision = pgpService + .isSignatureAcceptable(signature); + if (!decision.isSignatureAcceptable()) { + LOGGER.log(Level.SEVERE, + "Download URL '" + resourceUrl.toExternalForm() + + "' failed"); + LOGGER.log( + Level.SEVERE, + "This resource was signed with PGP key ID '" + + decision.getSignatureAsHex() + + "', which is not currently trusted"); + LOGGER.log( + Level.SEVERE, + "Use 'pgp key view' to view this key, 'pgp trust' to trust it, or 'pgp automatic trust' to trust any keys"); + throw new IOException("Download URL '" + + resourceUrl.toExternalForm() + + "' has untrusted PGP signature " + + JdkDelegatingLogListener.DO_NOT_LOG); + } + + // So far so good. Next we need the actual resource to ensure the + // ASC file really did sign it + final File resourceFile = File.createTempFile("roo_resource", null); + resourceFile.deleteOnExit(); + + inputStream = urlInputStreamService.openConnection(resourceUrl); + outputStream = new FileOutputStream(resourceFile); + IOUtils.copy(inputStream, outputStream); + + resource = new FileInputStream(resourceFile); + signature = new FileInputStream(ascUrlFile); + Validate.isTrue( + pgpService.isResourceSignedBySignature(resource, signature), + "PGP signature illegal for URL '%s'", + resourceUrl.toExternalForm()); + + // Excellent it worked! We don't need the ASC file anymore, so get + // rid of it + ascUrlFile.delete(); + + return resourceFile.toURI().toURL().openConnection(); + } + finally { + IOUtils.closeQuietly(resource); + IOUtils.closeQuietly(signature); + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/JdkDelegatingLogListener.java b/felix/src/main/java/org/springframework/roo/felix/JdkDelegatingLogListener.java new file mode 100644 index 000000000..fb8f4c80a --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/JdkDelegatingLogListener.java @@ -0,0 +1,145 @@ +package org.springframework.roo.felix; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogListener; +import org.osgi.service.log.LogReaderService; +import org.osgi.service.log.LogService; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.osgi.AbstractFlashingObject; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Delegates OSGi log messages to the JDK logging infrastructure. This in turn + * makes it compatible with Spring Roo's standard approach to log messages + * appearing on the console. + *

    + * For convenience all low-priority messages are output as flash messages. All + * high priority messages are sent to the JDK logger. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Reference(name = "shell", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = Shell.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY) +public class JdkDelegatingLogListener extends AbstractFlashingObject implements + LogListener { + + public static final String DO_NOT_LOG = "#DO_NOT_LOG"; + private final static Logger LOGGER = HandlerUtils + .getLogger(JdkDelegatingLogListener.class); + + public static String cleanThrowable(final Throwable throwable) { + final StringBuilder result = new StringBuilder(); + result.append(LINE_SEPARATOR); + result.append(throwable.toString().replace(DO_NOT_LOG, "")); + result.append(LINE_SEPARATOR); + for (final StackTraceElement ste : throwable.getStackTrace()) { + result.append(ste); + result.append(LINE_SEPARATOR); + } + return result.toString(); + } + + @Reference private LogReaderService logReaderService; + + @SuppressWarnings("unchecked") + protected void activate(final ComponentContext context) { + logReaderService.addLogListener(this); + final Enumeration latestLogs = logReaderService.getLog(); + if (latestLogs.hasMoreElements()) { + logNow(latestLogs.nextElement(), false); + } + } + + private String buildMessage(final LogEntry entry) { + final StringBuilder sb = new StringBuilder(); + sb.append("[").append(entry.getBundle()).append("] ") + .append(entry.getMessage()); + return sb.toString(); + } + + private boolean containsDoNotLogTag(final Throwable throwable) { + if (throwable == null) { + return false; + } + if (throwable.getMessage().contains(DO_NOT_LOG)) { + return true; + } + final StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + return sw.toString().contains(DO_NOT_LOG); + } + + protected void deactivate(final ComponentContext context) { + logReaderService.removeLogListener(this); + } + + public void logged(final LogEntry entry) { + if (containsDoNotLogTag(entry.getException())) { + // Only log Felix stack trace in development mode, discard log + // otherwise + if (isDevelopmentMode()) { + logNow(entry, true); + } + } + else { + logNow(entry, false); + } + } + + private void logNow(final LogEntry entry, final boolean removeDoNotLogTag) { + final int osgiLevel = entry.getLevel(); + Level jdkLevel = Level.FINEST; + + // Convert the OSGi level into a JDK logger level + if (osgiLevel == LogService.LOG_DEBUG) { + jdkLevel = Level.FINE; + } + else if (osgiLevel == LogService.LOG_INFO) { + jdkLevel = Level.INFO; + } + else if (osgiLevel == LogService.LOG_WARNING) { + jdkLevel = Level.WARNING; + } + else if (osgiLevel == LogService.LOG_ERROR) { + jdkLevel = Level.SEVERE; + } + + if (jdkLevel.intValue() <= Level.INFO.intValue()) { + // Not very important message, so just flash it if possible and + // we're in development mode + if (isDevelopmentMode()) { + flash(jdkLevel, buildMessage(entry), MY_SLOT); + // Immediately clear it once the timeout has been reached + flash(jdkLevel, "", MY_SLOT); + } + } + else { + // Important log message, so log it via JDK + if (removeDoNotLogTag) { + LOGGER.log( + jdkLevel, + buildMessage(entry) + + cleanThrowable(entry.getException())); + } + else { + LOGGER.log(jdkLevel, buildMessage(entry), entry.getException()); + } + } + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/LogLevel.java b/felix/src/main/java/org/springframework/roo/felix/LogLevel.java new file mode 100644 index 000000000..bcabae364 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/LogLevel.java @@ -0,0 +1,66 @@ +package org.springframework.roo.felix; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides levels for the Felix "log" command. + * + * @author Ben Alex + * @since 1.0 + */ +public class LogLevel implements Comparable { + + public static final LogLevel DEBUG = new LogLevel("DEBUG", "debug"); + public static final LogLevel ERROR = new LogLevel("ERROR", "error"); + + public static final LogLevel INFORMATION = new LogLevel("INFORMATION", + "info"); + public static final LogLevel WARNING = new LogLevel("WARNING", "warn"); + private final String felixCode; + private final String key; + + public LogLevel(final String key, final String felixCode) { + Validate.notBlank(key, "Key required"); + Validate.notBlank(felixCode, "Felix code required"); + this.key = key; + this.felixCode = felixCode; + } + + public final int compareTo(final LogLevel o) { + if (o == null) { + return -1; + } + final int result = key.compareTo(o.key); + if (result == 0) { + return felixCode.compareTo(o.felixCode); + } + return result; + } + + @Override + public final boolean equals(final Object obj) { + return obj instanceof LogLevel && compareTo((LogLevel) obj) == 0; + } + + public String getFelixCode() { + return felixCode; + } + + public String getKey() { + return key; + } + + @Override + public final int hashCode() { + return key.hashCode() * felixCode.hashCode(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("key", key); + builder.append("felixCode", felixCode); + return builder.toString(); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/PsOptions.java b/felix/src/main/java/org/springframework/roo/felix/PsOptions.java new file mode 100644 index 000000000..27ae69fa4 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/PsOptions.java @@ -0,0 +1,68 @@ +package org.springframework.roo.felix; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Provides display formats for the Felix "ps" command. + * + * @author Ben Alex + * @since 1.0 + */ +public class PsOptions implements Comparable { + + public static final PsOptions BUNDLE_NAME = new PsOptions("BUNDLE_NAME", ""); // default + public static final PsOptions LOCATION_PATH = new PsOptions( + "LOCATION_PATH", " -l"); + + public static final PsOptions SYMBOLIC_NAME = new PsOptions( + "SYMBOLIC_NAME", " -s"); + public static final PsOptions UPDATE_PATH = new PsOptions("UPDATE_PATH", + " -u"); + private final String felixCode; + private final String key; + + public PsOptions(final String key, final String felixCode) { + Validate.notBlank(key, "Key required"); + Validate.notNull(felixCode, "Felix code required"); + this.key = key; + this.felixCode = felixCode; + } + + public final int compareTo(final PsOptions o) { + if (o == null) { + return -1; + } + final int result = key.compareTo(o.key); + if (result == 0) { + return felixCode.compareTo(o.felixCode); + } + return result; + } + + @Override + public final boolean equals(final Object obj) { + return obj instanceof PsOptions && compareTo((PsOptions) obj) == 0; + } + + public String getFelixCode() { + return felixCode; + } + + public String getKey() { + return key; + } + + @Override + public final int hashCode() { + return key.hashCode() * felixCode.hashCode(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("key", key); + builder.append("felixCode", felixCode); + return builder.toString(); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/help/HelpCommands.java b/felix/src/main/java/org/springframework/roo/felix/help/HelpCommands.java new file mode 100644 index 000000000..68a6c7d89 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/help/HelpCommands.java @@ -0,0 +1,33 @@ +package org.springframework.roo.felix.help; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Enables a user to obtain Help + * + * @author Juan Carlos García + * @since 1.3 + */ +@Service +@Component +public class HelpCommands implements CommandMarker { + + @Reference HelpService helpService; + + @CliCommand(value = "reference guide", help = "Writes the reference guide XML fragments (in DocBook format) into the current working directory") + public void helpReferenceGuide() { + helpService.helpReferenceGuide(); + } + + @CliCommand(value = "help", help = "Shows system help") + public void obtainHelp( + @CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for") final String buffer) { + + helpService.obtainHelp(buffer); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/help/HelpService.java b/felix/src/main/java/org/springframework/roo/felix/help/HelpService.java new file mode 100644 index 000000000..7c54ab2bb --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/help/HelpService.java @@ -0,0 +1,22 @@ +package org.springframework.roo.felix.help; + + +/** + * Provides Help Operations + * + * @author Juan Carlos García + * @since 1.3 + */ +public interface HelpService { + + /** + * Obtains Help Reference Guide + */ + void helpReferenceGuide(); + + /** + * Obtains Help + */ + void obtainHelp(String buffer); + +} diff --git a/felix/src/main/java/org/springframework/roo/felix/help/HelpServiceImpl.java b/felix/src/main/java/org/springframework/roo/felix/help/HelpServiceImpl.java new file mode 100644 index 000000000..68d9fd314 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/help/HelpServiceImpl.java @@ -0,0 +1,674 @@ +package org.springframework.roo.felix.help; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.Transformer; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.AbstractShell; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; +import org.springframework.roo.shell.NaturalOrderComparator; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.CDATASection; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Default implementation of {@link HelpService}. + * + * @author Juan Carlos García + * @since 1.3 + */ +@Component +@Service +public class HelpServiceImpl implements HelpService { + + private static final Logger LOGGER = HandlerUtils + .getLogger(HelpServiceImpl.class); + + // ------------ OSGi component attributes ---------------- + public BundleContext context; + private static final Comparator COMPARATOR = new NaturalOrderComparator(); + + private final Map availabilityIndicators = new HashMap(); + private final Set commands = new HashSet(); + private final Set> converters = new HashSet>(); + + static final String NULL = "__NULL__"; + + private final Object mutex = new Object(); + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + public void helpReferenceGuide() { + synchronized (mutex) { + + if (commands.isEmpty()) { + // Get all Services implement CommandMarker interface + try { + ServiceReference[] references = this.context + .getAllServiceReferences( + CommandMarker.class.getName(), null); + + for (ServiceReference ref : references) { + add((CommandMarker) this.context.getService(ref)); + } + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CommandMarker on SimpleParser."); + } + } + + final File f = new File("."); + final File[] existing = f.listFiles(new FileFilter() { + public boolean accept(final File pathname) { + return pathname.getName().startsWith("appendix_"); + } + }); + for (final File e : existing) { + e.delete(); + } + + // Compute the sections we'll be outputting, and get them into a + // nice order + final SortedMap sections = new TreeMap( + COMPARATOR); + next_target: for (final Object target : commands) { + final Method[] methods = target.getClass().getMethods(); + for (final Method m : methods) { + final CliCommand cmd = m.getAnnotation(CliCommand.class); + if (cmd != null) { + String sectionName = target.getClass().getSimpleName(); + final Pattern p = Pattern.compile("[A-Z][^A-Z]*"); + final Matcher matcher = p.matcher(sectionName); + final StringBuilder string = new StringBuilder(); + while (matcher.find()) { + string.append(matcher.group()).append(" "); + } + sectionName = string.toString().trim(); + if (sections.containsKey(sectionName)) { + throw new IllegalStateException("Section name '" + + sectionName + "' not unique"); + } + sections.put(sectionName, target); + continue next_target; + } + } + } + + // Build each section of the appendix + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + final Document document = builder.newDocument(); + final List builtSections = new ArrayList(); + + for (final Entry entry : sections.entrySet()) { + final String section = entry.getKey(); + final Object target = entry.getValue(); + final SortedMap individualCommands = new TreeMap( + COMPARATOR); + + final Method[] methods = target.getClass().getMethods(); + for (final Method m : methods) { + final CliCommand cmd = m.getAnnotation(CliCommand.class); + if (cmd != null) { + final StringBuilder cmdSyntax = new StringBuilder(); + cmdSyntax.append(cmd.value()[0]); + + // Build the syntax list + + // Store the order options appear + final List optionKeys = new ArrayList(); + // key: option key, value: help text + final Map optionDetails = new HashMap(); + for (final Annotation[] ann : m + .getParameterAnnotations()) { + for (final Annotation a : ann) { + if (a instanceof CliOption) { + final CliOption option = (CliOption) a; + // Figure out which key we want to use (use + // first non-empty string, or make it + // "(default)" if needed) + String key = option.key()[0]; + if ("".equals(key)) { + for (final String otherKey : option + .key()) { + if (!"".equals(otherKey)) { + key = otherKey; + break; + } + } + if ("".equals(key)) { + key = "[default]"; + } + } + + final StringBuilder help = new StringBuilder(); + if ("".equals(option.help())) { + help.append("No help available"); + } else { + help.append(option.help()); + } + if (option.specifiedDefaultValue().equals( + option.unspecifiedDefaultValue())) { + if (option.specifiedDefaultValue() + .equals(null)) { + help.append("; no default value"); + } else { + help.append("; default: '") + .append(option + .specifiedDefaultValue()) + .append("'"); + } + } else { + if (!"".equals(option + .specifiedDefaultValue()) + && !NULL.equals(option + .specifiedDefaultValue())) { + help.append( + "; default if option present: '") + .append(option + .specifiedDefaultValue()) + .append("'"); + } + if (!"".equals(option + .unspecifiedDefaultValue()) + && !NULL.equals(option + .unspecifiedDefaultValue())) { + help.append( + "; default if option not present: '") + .append(option + .unspecifiedDefaultValue()) + .append("'"); + } + } + help.append(option.mandatory() ? " (mandatory) " + : ""); + + // Store details for later + key = "--" + key; + optionKeys.add(key); + optionDetails.put(key, help.toString()); + + // Include it in the mandatory syntax + if (option.mandatory()) { + cmdSyntax.append(" ").append(key); + } + } + } + } + + // Make a variable list element + Element variableListElement = document + .createElement("variablelist"); + boolean anyVars = false; + for (final String optionKey : optionKeys) { + anyVars = true; + final String help = optionDetails.get(optionKey); + variableListElement + .appendChild(new XmlElementBuilder( + "varlistentry", document) + .addChild( + new XmlElementBuilder( + "term", document) + .setText(optionKey) + .build()) + .addChild( + new XmlElementBuilder( + "listitem", + document) + .addChild( + new XmlElementBuilder( + "para", + document) + .setText( + help) + .build()) + .build()).build()); + } + + if (!anyVars) { + variableListElement = new XmlElementBuilder("para", + document) + .setText( + "This command does not accept any options.") + .build(); + } + + // Now we've figured out the options, store this + // individual command + final CDATASection progList = document + .createCDATASection(cmdSyntax.toString()); + final String safeName = cmd.value()[0] + .replace("\\", "BCK").replace("/", "FWD") + .replace("*", "ASX"); + final Element element = new XmlElementBuilder( + "section", document) + .addAttribute( + "xml:id", + "command-index-" + + safeName.toLowerCase() + .replace(' ', '-')) + .addChild( + new XmlElementBuilder("title", document) + .setText(cmd.value()[0]) + .build()) + .addChild( + new XmlElementBuilder("para", document) + .setText(cmd.help()).build()) + .addChild( + new XmlElementBuilder("programlisting", + document).addChild(progList) + .build()) + .addChild(variableListElement).build(); + + individualCommands.put(cmdSyntax.toString(), element); + } + } + + final Element topSection = document.createElement("section"); + topSection.setAttribute("xml:id", "command-index-" + + section.toLowerCase().replace(' ', '-')); + topSection.appendChild(new XmlElementBuilder("title", document) + .setText(section).build()); + topSection.appendChild(new XmlElementBuilder("para", document) + .setText( + section + " are contained in " + + target.getClass().getName() + ".") + .build()); + + for (final Element value : individualCommands.values()) { + topSection.appendChild(value); + } + + builtSections.add(topSection); + } + + final Element appendix = document.createElement("appendix"); + appendix.setAttribute("xmlns", "http://docbook.org/ns/docbook"); + appendix.setAttribute("version", "5.0"); + appendix.setAttribute("xml:id", "command-index"); + appendix.appendChild(new XmlElementBuilder("title", document) + .setText("Command Index").build()); + appendix.appendChild(new XmlElementBuilder("para", document) + .setText( + "This appendix was automatically built from Roo " + + AbstractShell.versionInfo() + ".") + .build()); + appendix.appendChild(new XmlElementBuilder("para", document) + .setText( + "Commands are listed in alphabetic order, and are shown in monospaced font with any mandatory options you must specify when using the command. Most commands accept a large number of options, and all of the possible options for each command are presented in this appendix.") + .build()); + + for (final Element section : builtSections) { + appendix.appendChild(section); + } + document.appendChild(appendix); + + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + final Transformer transformer = XmlUtils + .createIndentingTransformer(); + // Causes an + // "Error reported by XML parser: Multiple notations were used which had the name 'linespecific', but which were not determined to be duplicates." + // when creating the DocBook + // transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, + // "-//OASIS//DTD DocBook XML V4.5//EN"); + // transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, + // "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"); + + XmlUtils.writeXml(transformer, byteArrayOutputStream, document); + try { + final File output = new File(f, "appendix-command-index.xml"); + FileUtils.writeByteArrayToFile(output, + byteArrayOutputStream.toByteArray()); + } catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } finally { + IOUtils.closeQuietly(byteArrayOutputStream); + } + } + } + + public void obtainHelp( + @CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for") String buffer) { + synchronized (mutex) { + if (buffer == null) { + buffer = ""; + } + + final StringBuilder sb = new StringBuilder(); + + // Figure out if there's a single command we can offer help for + final Collection matchingTargets = locateTargets( + buffer, false, false); + if (matchingTargets.size() == 1) { + // Single command help + final MethodTarget methodTarget = matchingTargets.iterator() + .next(); + + // Argument conversion time + final Annotation[][] parameterAnnotations = methodTarget + .getMethod().getParameterAnnotations(); + if (parameterAnnotations.length > 0) { + // Offer specified help + final CliCommand cmd = methodTarget.getMethod() + .getAnnotation(CliCommand.class); + Validate.notNull(cmd, "CliCommand not found"); + + for (final String value : cmd.value()) { + sb.append("Keyword: ").append(value) + .append(LINE_SEPARATOR); + } + + sb.append("Description: ").append(cmd.help()) + .append(LINE_SEPARATOR); + + for (final Annotation[] annotations : parameterAnnotations) { + CliOption cliOption = null; + for (final Annotation a : annotations) { + if (a instanceof CliOption) { + cliOption = (CliOption) a; + + for (String key : cliOption.key()) { + if ("".equals(key)) { + key = "** default **"; + } + sb.append(" Keyword: ") + .append(key).append(LINE_SEPARATOR); + } + + sb.append(" Help: ") + .append(cliOption.help()) + .append(LINE_SEPARATOR); + sb.append(" Mandatory: ") + .append(cliOption.mandatory()) + .append(LINE_SEPARATOR); + sb.append(" Default if specified: '") + .append(cliOption + .specifiedDefaultValue()) + .append("'").append(LINE_SEPARATOR); + sb.append(" Default if unspecified: '") + .append(cliOption + .unspecifiedDefaultValue()) + .append("'").append(LINE_SEPARATOR); + sb.append(LINE_SEPARATOR); + } + + } + Validate.notNull(cliOption, + "CliOption not found for parameter '%s'", + Arrays.toString(annotations)); + } + } + // Only a single argument, so default to the normal help + // operation + } + + final SortedSet result = new TreeSet(COMPARATOR); + for (final MethodTarget mt : matchingTargets) { + final CliCommand cmd = mt.getMethod().getAnnotation( + CliCommand.class); + if (cmd != null) { + for (final String value : cmd.value()) { + if ("".equals(cmd.help())) { + result.add("* " + value); + } else { + result.add("* " + value + " - " + cmd.help()); + } + } + } + } + + for (final String s : result) { + sb.append(s).append(LINE_SEPARATOR); + } + + LOGGER.info(sb.toString()); + LOGGER.warning("** Type 'hint' (without the quotes) and hit ENTER for step-by-step guidance **" + + LINE_SEPARATOR); + } + } + + private Collection locateTargets(final String buffer, + final boolean strictMatching, + final boolean checkAvailabilityIndicators) { + + if (commands.isEmpty()) { + // Get all Services implement CommandMarker interface + try { + ServiceReference[] references = this.context + .getAllServiceReferences(CommandMarker.class.getName(), + null); + + for (ServiceReference ref : references) { + add((CommandMarker) this.context.getService(ref)); + } + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CommandMarker on SimpleParser."); + } + } + + Validate.notNull(buffer, "Buffer required"); + final Collection result = new HashSet(); + + // The reflection could certainly be optimised, but it's good enough for + // now (and cached reflection + // is unlikely to be noticeable to a human being using the CLI) + for (final CommandMarker command : commands) { + for (final Method method : command.getClass().getMethods()) { + final CliCommand cmd = method.getAnnotation(CliCommand.class); + if (cmd != null) { + // We have a @CliCommand. + if (checkAvailabilityIndicators) { + // Decide if this @CliCommand is available at this + // moment + Boolean available = null; + for (final String value : cmd.value()) { + final MethodTarget mt = getAvailabilityIndicator(value); + if (mt != null) { + Validate.isTrue(available == null, + "More than one availability indicator is defined for '" + + method.toGenericString() + + "'"); + try { + available = (Boolean) mt.getMethod() + .invoke(mt.getTarget()); + // We should "break" here, but we loop over + // all to ensure no conflicting availability + // indicators are defined + } catch (final Exception e) { + available = false; + } + } + } + // Skip this @CliCommand if it's not available + if (available != null && !available) { + continue; + } + } + + for (final String value : cmd.value()) { + final String remainingBuffer = isMatch(buffer, value, + strictMatching); + if (remainingBuffer != null) { + result.add(new MethodTarget(method, command, + remainingBuffer, value)); + } + } + } + } + } + return result; + } + + public final void add(final CommandMarker command) { + synchronized (mutex) { + commands.add(command); + for (final Method method : command.getClass().getMethods()) { + final CliAvailabilityIndicator availability = method + .getAnnotation(CliAvailabilityIndicator.class); + if (availability != null) { + Validate.isTrue( + method.getParameterTypes().length == 0, + "CliAvailabilityIndicator is only legal for 0 parameter methods ('%s')", + method.toGenericString()); + Validate.isTrue( + method.getReturnType().equals(Boolean.TYPE), + "CliAvailabilityIndicator is only legal for primitive boolean return types (%s)", + method.toGenericString()); + for (final String cmd : availability.value()) { + Validate.isTrue( + !availabilityIndicators.containsKey(cmd), + "Cannot specify an availability indicator for '%s' more than once", + cmd); + availabilityIndicators.put(cmd, new MethodTarget( + method, command)); + } + } + } + } + } + + public final void add(final Converter converter) { + synchronized (mutex) { + converters.add(converter); + } + } + + private MethodTarget getAvailabilityIndicator(final String command) { + return availabilityIndicators.get(command); + } + + static String isMatch(final String buffer, final String command, + final boolean strictMatching) { + if ("".equals(buffer.trim())) { + return ""; + } + final String[] commandWords = StringUtils.split(command, " "); + int lastCommandWordUsed = 0; + Validate.notEmpty(commandWords, "Command required"); + + String bufferToReturn = null; + String lastWord = null; + + next_buffer_loop: for (int bufferIndex = 0; bufferIndex < buffer + .length(); bufferIndex++) { + final String bufferSoFarIncludingThis = buffer.substring(0, + bufferIndex + 1); + final String bufferRemaining = buffer.substring(bufferIndex + 1); + + final int bufferLastIndexOfWord = bufferSoFarIncludingThis + .lastIndexOf(" "); + String wordSoFarIncludingThis = bufferSoFarIncludingThis; + if (bufferLastIndexOfWord != -1) { + wordSoFarIncludingThis = bufferSoFarIncludingThis + .substring(bufferLastIndexOfWord); + } + + if (wordSoFarIncludingThis.equals(" ") + || bufferIndex == buffer.length() - 1) { + if (bufferIndex == buffer.length() - 1 + && !"".equals(wordSoFarIncludingThis.trim())) { + lastWord = wordSoFarIncludingThis.trim(); + } + + // At end of word or buffer. Let's see if a word matched or not + for (int candidate = lastCommandWordUsed; candidate < commandWords.length; candidate++) { + if (lastWord != null && lastWord.length() > 0 + && commandWords[candidate].startsWith(lastWord)) { + if (bufferToReturn == null) { + // This is the first match, so ensure the intended + // match really represents the start of a command + // and not a later word within it + if (lastCommandWordUsed == 0 && candidate > 0) { + // This is not a valid match + break next_buffer_loop; + } + } + + if (bufferToReturn != null) { + // We already matched something earlier, so ensure + // we didn't skip any word + if (candidate != lastCommandWordUsed + 1) { + // User has skipped a word + bufferToReturn = null; + break next_buffer_loop; + } + } + + bufferToReturn = bufferRemaining; + lastCommandWordUsed = candidate; + if (candidate + 1 == commandWords.length) { + // This was a match for the final word in the + // command, so abort + break next_buffer_loop; + } + // There are more words left to potentially match, so + // continue + continue next_buffer_loop; + } + } + + // This word is unrecognised as part of a command, so abort + bufferToReturn = null; + break next_buffer_loop; + } + + lastWord = wordSoFarIncludingThis.trim(); + } + + // We only consider it a match if ALL words were actually used + if (bufferToReturn != null) { + if (!strictMatching + || lastCommandWordUsed + 1 == commandWords.length) { + return bufferToReturn; + } + } + + return null; // Not a match + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/pgp/PgpCommands.java b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpCommands.java new file mode 100644 index 000000000..224df9d9a --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpCommands.java @@ -0,0 +1,221 @@ +package org.springframework.roo.felix.pgp; + +import java.text.SimpleDateFormat; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.TimeZone; + +import org.apache.commons.io.IOUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.encoders.Hex; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; + +/** + * Enables a user to manage the Roo PGP keystore. + * + * @author Ben Alex + * @since 1.1 + */ +@Service +@Component +public class PgpCommands implements CommandMarker { + + private static String getAlgorithm(final int algId) { + switch (algId) { + case PublicKeyAlgorithmTags.RSA_GENERAL: + return "RSA_GENERAL"; + case PublicKeyAlgorithmTags.RSA_ENCRYPT: + return "RSA_ENCRYPT"; + case PublicKeyAlgorithmTags.RSA_SIGN: + return "RSA_SIGN"; + case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT: + return "ELGAMAL_ENCRYPT"; + case PublicKeyAlgorithmTags.DSA: + return "DSA"; + case PublicKeyAlgorithmTags.EC: + return "EC"; + case PublicKeyAlgorithmTags.ECDSA: + return "ECDSA"; + case PublicKeyAlgorithmTags.ELGAMAL_GENERAL: + return "ELGAMAL_GENERAL"; + case PublicKeyAlgorithmTags.DIFFIE_HELLMAN: + return "DIFFIE_HELLMAN"; + } + return "unknown"; + } + + @Reference PgpService pgpService; + + private void appendLine(final StringBuilder sb, final String line) { + sb.append(line).append(IOUtils.LINE_SEPARATOR); + } + + @CliCommand(value = "pgp automatic trust", help = "Indicates to automatically trust all keys encountered until the command is invoked again") + public String automaticTrust() { + if (pgpService.isAutomaticTrust()) { + pgpService.setAutomaticTrust(false); + return "Automatic PGP key trusting disabled (this is the safest option)"; + } + pgpService.setAutomaticTrust(true); + return "Automatic PGP key trusting enabled (this is potentially unsafe); disable by typing 'pgp automatic trust' again"; + } + + @SuppressWarnings("unchecked") + private void formatKeyRing(final StringBuilder sb, + final PGPPublicKeyRing keyRing) { + final SimpleDateFormat sdf = new SimpleDateFormat( + "yyyy-MMM-dd HH:mm:ss Z"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + + final Iterator it = keyRing.getPublicKeys(); + boolean first = true; + while (it.hasNext()) { + final PGPPublicKey pgpKey = it.next(); + if (first) { + appendLine(sb, ">>>> KEY ID: " + new PgpKeyId(pgpKey) + " <<<<"); + appendLine( + sb, + " More Info: " + + pgpService + .getKeyServerUrlToRetrieveKeyInformation(new PgpKeyId( + pgpKey))); + appendLine(sb, + " Created: " + sdf.format(pgpKey.getCreationTime())); + appendLine( + sb, + " Fingerprint: " + + new String( + Hex.encode(pgpKey.getFingerprint()))); + appendLine(sb, + " Algorithm: " + + getAlgorithm(pgpKey.getAlgorithm())); + final Iterator userIdIterator = pgpKey.getUserIDs(); + while (userIdIterator.hasNext()) { + final String userId = userIdIterator.next(); + appendLine(sb, " User ID: " + userId); + final Iterator signatureIterator = pgpKey + .getSignaturesForID(userId); + while (signatureIterator.hasNext()) { + final PGPSignature signature = signatureIterator.next(); + appendLine(sb, " Signed By: " + + getKeySummaryIfPossible(new PgpKeyId( + signature))); + } + } + + first = false; + } + else { + appendLine(sb, " Subkey ID: " + new PgpKeyId(pgpKey) + " [" + + getAlgorithm(pgpKey.getAlgorithm()) + "]"); + } + } + } + + @SuppressWarnings("unchecked") + private String getKeySummaryIfPossible(final PgpKeyId keyId) { + final List keyRings = pgpService.getTrustedKeys(); + + for (final PGPPublicKeyRing keyRing : keyRings) { + final Iterator it = keyRing.getPublicKeys(); + while (it.hasNext()) { + final PGPPublicKey pgpKey = it.next(); + if (new PgpKeyId(pgpKey.getKeyID()).equals(keyId)) { + // We know about this key, so return a one-liner + final StringBuilder sb = new StringBuilder(); + sb.append("Key ").append(keyId).append(" ("); + final Iterator userIds = pgpKey.getUserIDs(); + if (userIds.hasNext()) { + final String userId = userIds.next(); + sb.append(userId); + } + else { + sb.append("no user ID in key"); + } + sb.append(")"); + return sb.toString(); + } + } + } + + return "Key " + keyId + " - not locally trusted"; + } + + @CliCommand(value = "pgp key view", help = "Downloads a remote key and displays it to the user (does not change any trusts)") + public String keyView( + @CliOption(key = "keyId", mandatory = true, help = "The key ID to view (eg 00B5050F or 0x00B5050F)") final PgpKeyId keyId) { + final PGPPublicKeyRing keyRing = pgpService.getPublicKey(keyId); + final StringBuilder sb = new StringBuilder(); + formatKeyRing(sb, keyRing); + return sb.toString(); + } + + @CliCommand(value = "pgp list trusted keys", help = "Lists the keys you currently trust and have not been revoked at the time last downloaded from a public key server") + public String listTrustedKeys() { + final List keyRings = pgpService.getTrustedKeys(); + if (keyRings.isEmpty()) { + final StringBuilder sb = new StringBuilder(); + appendLine( + sb, + "No keys trusted; use 'pgp trust' to add a key or 'pgp automatic trust' for automatic key addition"); + return sb.toString(); + } + final StringBuilder sb = new StringBuilder(); + for (final PGPPublicKeyRing keyRing : keyRings) { + formatKeyRing(sb, keyRing); + } + return sb.toString(); + } + + @CliCommand(value = "pgp status", help = "Displays the status of the PGP environment") + public String pgpStatus() { + final List keyRings = pgpService.getTrustedKeys(); + final StringBuilder sb = new StringBuilder(); + appendLine(sb, "File: " + pgpService.getKeyStorePhysicalLocation()); + appendLine(sb, "Automatic trust: " + + (pgpService.isAutomaticTrust() ? "enabled" : "disabled")); + appendLine(sb, "Key count: " + keyRings.size()); + return sb.toString(); + } + + @CliCommand(value = "pgp refresh all", help = "Refreshes all keys from public key servers") + public String refreshKeysFromServer() { + final StringBuilder sb = new StringBuilder(); + for (final Entry entry : pgpService.refresh() + .entrySet()) { + final PgpKeyId key = entry.getKey(); + final String outcome = entry.getValue(); + appendLine(sb, key + " : " + outcome); + } + return sb.toString(); + } + + @CliCommand(value = "pgp trust", help = "Grants trust to a particular key ID") + public String trust( + @CliOption(key = "keyId", mandatory = true, help = "The key ID to trust (eg 00B5050F or 0x00B5050F)") final PgpKeyId keyId) { + final PGPPublicKeyRing keyRing = pgpService.trust(keyId); + final StringBuilder sb = new StringBuilder(); + appendLine(sb, "Added trust for key:"); + formatKeyRing(sb, keyRing); + return sb.toString(); + } + + @CliCommand(value = "pgp untrust", help = "Revokes your trust for a particular key ID") + public String untrust( + @CliOption(key = "keyId", mandatory = true, help = "The key ID to remove trust from (eg 00B5050F or 0x00B5050F)") final PgpKeyId keyId) { + final PGPPublicKeyRing keyRing = pgpService.untrust(keyId); + final StringBuilder sb = new StringBuilder(); + appendLine(sb, "Revoked trust from key:"); + formatKeyRing(sb, keyRing); + return sb.toString(); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/pgp/PgpKeyId.java b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpKeyId.java new file mode 100644 index 000000000..3159b5a9a --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpKeyId.java @@ -0,0 +1,82 @@ +package org.springframework.roo.felix.pgp; + +import org.apache.commons.lang3.Validate; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSignature; + +/** + * Represents a 10 digit hexadecimal key ID (always starts with 0x, and the rest + * of the ID is uppercase). + * + * @author Ben Alex + * @since 1.1 + */ +public class PgpKeyId implements Comparable { + private static final long MASK = 0xFFFFFFFFL; + private String id; + + public PgpKeyId(final long keyId) { + id = "0x" + String.format("%08X", MASK & keyId); + } + + public PgpKeyId(final PGPPublicKey keyId) { + Validate.notNull(keyId, "Key ID required"); + id = "0x" + String.format("%08X", MASK & keyId.getKeyID()); + } + + public PgpKeyId(final PGPSignature signature) { + Validate.notNull(signature, "Signautre required"); + id = "0x" + String.format("%08X", MASK & signature.getKeyID()); + } + + public PgpKeyId(String keyId) { + Validate.notBlank(keyId, + "A key ID is required (eg 00B5050F or 0x00B5050F)"); + if (keyId.length() == 10) { + Validate.isTrue(keyId.toLowerCase().startsWith("0x"), + "10 character key IDs must start with 0x"); + keyId = keyId.toUpperCase(); // NB: the 0x will become uppercase, + // which it shouldn't + id = "0x" + keyId.substring(2); + } + else if (keyId.length() == 8) { + Validate.isTrue(!keyId.toLowerCase().startsWith("0x"), + "8 character key IDs must not start with 0x"); + keyId = keyId.toUpperCase(); + id = "0x" + keyId; + } + else { + throw new IllegalStateException( + "The key ID must be in a valid form (eg 00B5050F or 0x00B5050F)"); + } + } + + public int compareTo(final PgpKeyId o) { + if (o == null) { + return -1; + } + return id.compareTo(o.id); + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof PgpKeyId) { + return id.equals(((PgpKeyId) obj).id); + } + return false; + } + + public String getId() { + return id; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return id.toString(); + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/pgp/PgpKeyIdConverter.java b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpKeyIdConverter.java new file mode 100644 index 000000000..5612f4640 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpKeyIdConverter.java @@ -0,0 +1,46 @@ +package org.springframework.roo.felix.pgp; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link PgpKeyId}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class PgpKeyIdConverter implements Converter { + + @Reference private PgpService pgpService; + + public PgpKeyId convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new PgpKeyId(value.trim()); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String originalUserInput, + final String optionContext, final MethodTarget target) { + for (final PgpKeyId candidate : pgpService.getDiscoveredKeyIds()) { + final String id = candidate.getId(); + if (id.toUpperCase().startsWith(originalUserInput.toUpperCase())) { + completions.add(new Completion(id)); + } + } + + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return PgpKeyId.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/felix/src/main/java/org/springframework/roo/felix/pgp/PgpService.java b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpService.java new file mode 100644 index 000000000..dce9a880d --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpService.java @@ -0,0 +1,157 @@ +package org.springframework.roo.felix.pgp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; + +/** + * Provides a central location for all PGP key store and file verification + * activities. + * + * @author Ben Alex + * @since 1.1 + */ +public interface PgpService { + + /** + * Provides a way of discovered all Key IDs that have been encountered by + * the service since it started. This is mostly useful for the user + * interface building tab completion commands etc. + * + * @return an unmodifiable list of the Key IDs (never null, but may be + * empty) + */ + SortedSet getDiscoveredKeyIds(); + + /** + * Obtains a URL that should allow a human-friendly display of key + * properties. + *

    + * The key server may not contain the specified public key if it has never + * been uploaded. + * + * @param keyId hex-encoded key ID to display (required) + * @return the URL (never null) + */ + URL getKeyServerUrlToRetrieveKeyInformation(PgpKeyId keyId); + + /** + * @return the canonical file path to the key store (never null, although + * the file may not exist) + */ + String getKeyStorePhysicalLocation(); + + /** + * Attempts to download the specified key ID. + *

    + * This method requires internet access to complete. + * + * @param keyId hex-encoded key ID to download (required) + * @return the key (never null, but an exception is thrown if the key is + * unavailable) + */ + PGPPublicKeyRing getPublicKey(PgpKeyId keyId); + + /** + * Obtains all of the keys presently trusted by the user. + *

    + * Does not require internet access. + * + * @return the list of keys (may have zero elements, but will never be null) + */ + List getTrustedKeys(); + + /** + * Indicates if the service automatically trusts new keys. + * + * @return true if auto-trust is active + */ + boolean isAutomaticTrust(); + + /** + * Indicates if this resource has been signed by the presented ASC file. + * This does not make any decision whether the key used in the ASC is valid + * or not (use {@link #isSignatureAcceptable(InputStream)} for this + * instead). + *

    + * This method does not require internet access. + * + * @param resource the resource that was presented (required) + * @param signature the ASC signature that was presented (required) + * @return true if this signature file verified this resource, false + * otherwise + */ + boolean isResourceSignedBySignature(InputStream resource, + InputStream signature) throws IOException; + + /** + * Indicates if the signature is acceptable or not based on the presentation + * of an ASC file. This will determine if the ASC is valid and the key used + * to produce it is trusted. It does not verify a resource was actually + * signed using the ASC (use + * {@link #isResourceSignedBySignature(InputStream, InputStream)} for this + * instead). In practical terms this method will throw an exception if + * something is wrong with the signature (eg it is corrupted). If the method + * returns an object, it means the ASC signature was valid (although the key + * which signed it may not be trusted). + *

    + * This method requires internet access if the automatic trust mode is on + * and it is necessary to add a new key during processing. In no other case + * is internet access required. + * + * @param signature the ASC signature that was presented (required) + * @return the decision (never null) + */ + SignatureDecision isSignatureAcceptable(InputStream signature) + throws IOException; + + /** + * Instructs the implementation to refresh all keys it current trusts. This + * is to identify keys that may have been revoked, which will automatically + * be untrusted. + *

    + * This method requires internet access to complete. If a download fails, + * the key trust will be retained (but of course not refreshed). The outcome + * of each refresh request is included in the returned object. + * + * @return a map where the keys are the hexadecimal key IDs and the values + * are the status of the update (never returns null) + */ + SortedMap refresh(); + + /** + * Directs the service to automatically trust new keys it encounters. + * + * @param automaticTrust the new value + */ + void setAutomaticTrust(boolean automaticTrust); + + /** + * Trusts a new key ID (refreshing the existing key ID if it is already + * trusted). + *

    + * This method requires internet access to complete. + * + * @param keyId hex-encoded key ID to trust (required) + * @return the key information now trusted (as refreshed from the server) + */ + PGPPublicKeyRing trust(PgpKeyId keyId); + + /** + * Untrusts an existing key ID (method will throw an exception if the key + * isn't currently trusted). + *

    + * This method does not require internet access. + * + * @param keyId hex-encoded key ID to untrust (required) + * @return the key information that is no longer trusted (as last cached; + * never returns null) + */ + PGPPublicKeyRing untrust(PgpKeyId keyId); + +} diff --git a/felix/src/main/java/org/springframework/roo/felix/pgp/PgpServiceImpl.java b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpServiceImpl.java new file mode 100644 index 000000000..7e877b242 --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/pgp/PgpServiceImpl.java @@ -0,0 +1,508 @@ +package org.springframework.roo.felix.pgp; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Security; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.url.stream.UrlInputStreamService; + +/** + * Default implementation of {@link PgpService}. + *

    + * Stores the user's PGP information in the + * ~/.spring_roo_pgp.bpg file. Every key in this + * file is considered trusted by the user. Expiration times of keys are ignored. Default keys that + * ship with Roo are added to this file automatically when the file is not present on disk. + * + *

    + * This implementation will only verify "detached armored signatures". Produce such a file via + * "gpg --armor --detach-sign file_to_sign.ext". + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class PgpServiceImpl implements PgpService { + + private static final int BUFFER_SIZE = 1024; + private static String defaultKeyServerUrl = "http://keyserver.ubuntu.com/pks/lookup?op=get&search="; + // private static String defaultKeyServerUrl = + // "http://pgp.mit.edu/pks/lookup?op=get&search="; + + private static final File ROO_PGP_FILE = FileUtils.getFile( + FileUtils.getUserDirectory(), ".spring_roo_pgp.bpg"); + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private boolean automaticTrust; + private BundleContext context; + private final SortedSet discoveredKeyIds = new TreeSet(); + @Reference private UrlInputStreamService urlInputStreamService; + + public SortedSet getDiscoveredKeyIds() { + return Collections.unmodifiableSortedSet(discoveredKeyIds); + } + + public URL getKeyServerUrlToRetrieveKeyInformation(final PgpKeyId keyId) { + Validate.notNull(keyId, "Key ID required"); + final URL keyUrl = getKeyServerUrlToRetrieveKeyId(keyId); + try { + final URL keyIndexUrl = new URL(keyUrl.getProtocol() + "://" + + keyUrl.getAuthority() + keyUrl.getPath() + + "?fingerprint=on&op=index&search="); + return new URL(keyIndexUrl.toString() + keyId); + } + catch (final MalformedURLException e) { + throw new IllegalStateException(e); + } + } + + public String getKeyStorePhysicalLocation() { + try { + return ROO_PGP_FILE.getCanonicalPath(); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + public PGPPublicKeyRing getPublicKey(final InputStream in) { + Object obj; + try { + final PGPObjectFactory pgpFact = new PGPObjectFactory( + PGPUtil.getDecoderStream(in)); + obj = pgpFact.nextObject(); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + + if (obj instanceof PGPPublicKeyRing) { + final PGPPublicKeyRing keyRing = (PGPPublicKeyRing) obj; + rememberKey(keyRing); + return keyRing; + } + + throw new IllegalStateException("Pblic key not available"); + } + + public PGPPublicKeyRing getPublicKey(final PgpKeyId keyId) { + Validate.notNull(keyId, "Key ID required"); + InputStream in = null; + try { + final URL lookup = getKeyServerUrlToRetrieveKeyId(keyId); + in = urlInputStreamService.openConnection(lookup); + return getPublicKey(in); + } + catch (final Exception e) { + throw new IllegalStateException("Public key ID '" + keyId + + "' not available from key server", e); + } + finally { + IOUtils.closeQuietly(in); + } + } + + @SuppressWarnings("unchecked") + public List getTrustedKeys() { + if (!ROO_PGP_FILE.exists()) { + return new ArrayList(); + } + FileInputStream fis = null; + try { + fis = new FileInputStream(ROO_PGP_FILE); + final PGPPublicKeyRingCollection pubRings = new PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(fis)); + final Iterator rIt = pubRings.getKeyRings(); + final List result = new ArrayList(); + while (rIt.hasNext()) { + final PGPPublicKeyRing pgpPub = rIt.next(); + rememberKey(pgpPub); + result.add(pgpPub); + } + return result; + } + catch (final Exception e) { + throw new IllegalArgumentException( + "Unable to get trusted keys", + ObjectUtils.defaultIfNull(ExceptionUtils.getRootCause(e), e)); + } + finally { + IOUtils.closeQuietly(fis); + } + } + + public boolean isAutomaticTrust() { + return automaticTrust; + } + + public boolean isResourceSignedBySignature(final InputStream resource, + InputStream signature) { + PGPPublicKey publicKey = null; + PGPSignature pgpSignature = null; + + try { + if (!(signature instanceof ArmoredInputStream)) { + signature = new ArmoredInputStream(signature); + } + + pgpSignature = isSignatureAcceptable(signature).getPgpSignature(); + final PGPPublicKeyRing keyRing = getPublicKey(new PgpKeyId( + pgpSignature)); + rememberKey(keyRing); + publicKey = keyRing.getPublicKey(); + + Validate.notNull(publicKey, + "Could not obtain public key for signer key ID '%s'", + pgpSignature); + + pgpSignature.initVerify(publicKey, "BC"); + + // Now verify the signed content + final byte[] buff = new byte[BUFFER_SIZE]; + int chunk; + do { + chunk = resource.read(buff); + if (chunk > 0) { + pgpSignature.update(buff, 0, chunk); + } + } while (chunk >= 0); + + return pgpSignature.verify(); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + public SignatureDecision isSignatureAcceptable(final InputStream signature) + throws IOException { + Validate.notNull(signature, "Signature input stream required"); + PGPObjectFactory factory = new PGPObjectFactory( + PGPUtil.getDecoderStream(signature)); + final Object obj = factory.nextObject(); + Validate.notNull(obj, "Unable to retrieve signature from stream"); + + PGPSignatureList p3; + if (obj instanceof PGPCompressedData) { + try { + factory = new PGPObjectFactory( + ((PGPCompressedData) obj).getDataStream()); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + p3 = (PGPSignatureList) factory.nextObject(); + } + else { + p3 = (PGPSignatureList) obj; + } + + final PGPSignature pgpSignature = p3.get(0); + Validate.notNull(pgpSignature, + "Unable to retrieve signature from stream"); + + final PgpKeyId keyIdInHex = new PgpKeyId(pgpSignature); + + // Special case where we directly store the key ID, as we know it's + // valid + discoveredKeyIds.add(keyIdInHex); + + boolean signatureAcceptable = false; + + // Loop to see if the user trusts this key + for (final PGPPublicKeyRing keyRing : getTrustedKeys()) { + final PgpKeyId candidate = new PgpKeyId(keyRing.getPublicKey()); + if (candidate.equals(keyIdInHex)) { + signatureAcceptable = true; + break; + } + } + + if (!signatureAcceptable && automaticTrust) { + // We don't approve of this signature, but the user has told us it's + // OK + trust(keyIdInHex); + signatureAcceptable = true; + } + + return new SignatureDecision(pgpSignature, keyIdInHex, + signatureAcceptable); + } + + public SortedMap refresh() { + final SortedMap result = new TreeMap(); + // Get the keys we currently trust + final List trusted = getTrustedKeys(); + + // Build a new list of our refreshed keys + final List stillTrusted = new ArrayList(); + + // Locate the element to remove (we need to record it so the method can + // return it) + for (final PGPPublicKeyRing candidate : trusted) { + final PGPPublicKey firstKey = candidate.getPublicKey(); + final PgpKeyId candidateKeyId = new PgpKeyId(firstKey); + // Try to refresh + PGPPublicKeyRing newKeyRing; + try { + newKeyRing = getPublicKey(candidateKeyId); + } + catch (final Exception e) { + // Can't retrieve, so keep the old one for now + stillTrusted.add(candidate); + result.put(candidateKeyId, + "WARNING: Retained original (download issue)"); + continue; + } + // Do not store if the first key is revoked + if (newKeyRing.getPublicKey().isRevoked()) { + result.put(candidateKeyId, + "WARNING: Key revoked, so removed from trust list"); + } + else { + stillTrusted.add(newKeyRing); + result.put(candidateKeyId, "SUCCESS"); + } + } + + // Write back to disk + OutputStream fos = null; + try { + final PGPPublicKeyRingCollection newCollection = new PGPPublicKeyRingCollection( + stillTrusted); + fos = new FileOutputStream(ROO_PGP_FILE); + newCollection.encode(fos); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(fos); + } + + return result; + } + + public void setAutomaticTrust(final boolean automaticTrust) { + this.automaticTrust = automaticTrust; + } + + public PGPPublicKeyRing trust(final PgpKeyId keyId) { + Validate.notNull(keyId, "Key ID required"); + final PGPPublicKeyRing keyRing = getPublicKey(keyId); + return trust(keyRing); + } + + @SuppressWarnings("unchecked") + public PGPPublicKeyRing untrust(final PgpKeyId keyId) { + Validate.notNull(keyId, "Key ID required"); + // Get the keys we currently trust + final List trusted = getTrustedKeys(); + + // Build a new list of keys we'll continue to trust after this method + // ends + final List stillTrusted = new ArrayList(); + + // Locate the element to remove (we need to record it so the method can + // return it) + PGPPublicKeyRing removed = null; + for (final PGPPublicKeyRing candidate : trusted) { + boolean stillTrust = true; + final Iterator it = candidate.getPublicKeys(); + while (it.hasNext()) { + final PGPPublicKey pgpKey = it.next(); + final PgpKeyId candidateKeyId = new PgpKeyId(pgpKey); + if (removed == null && candidateKeyId.equals(keyId)) { + stillTrust = false; + removed = candidate; + break; + } + } + if (stillTrust) { + stillTrusted.add(candidate); + } + } + + Validate.notNull(removed, + "The public key ID '%s' is not currently trusted", keyId); + + // Write back to disk + OutputStream fos = null; + try { + final PGPPublicKeyRingCollection newCollection = new PGPPublicKeyRingCollection( + stillTrusted); + fos = new FileOutputStream(ROO_PGP_FILE); + newCollection.encode(fos); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(fos); + } + return removed; + } + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + final String keyserver = context.getBundleContext().getProperty( + "pgp.keyserver.url"); + if (StringUtils.isNotBlank(keyserver)) { + defaultKeyServerUrl = keyserver; + } + trustDefaultKeysIfRequired(); + // Seed the discovered keys database + getTrustedKeys(); + } + + protected void trustDefaultKeysIfRequired() { + // Setup default keys we trust automatically + trustDefaultKeys(); + } + + /** + * Obtains a URL that should allow the download of the specified public key. + *

    + * The key server may not contain the specified public key if it has never + * been uploaded. + * + * @param keyId hex-encoded key ID to download (required) + * @return the URL (never null) + */ + private URL getKeyServerUrlToRetrieveKeyId(final PgpKeyId keyId) { + try { + return new URL(defaultKeyServerUrl + keyId); + } + catch (final MalformedURLException e) { + throw new IllegalStateException(e); + } + } + + /** + * Simply stores the key ID in {@link #discoveredKeyIds} for future + * reference of all Key IDs we've come across. This method uses a + * {@link PGPPublicKeyRing} to ensure the input is actually a valid key, + * plus locating any key IDs that have signed the key. + *

    + * Please note {@link #discoveredKeyIds} is not used for any key functions + * of this class. It is simply for user interface convenience. + * + * @param keyRing the key ID to store (required) + */ + @SuppressWarnings("unchecked") + private void rememberKey(final PGPPublicKeyRing keyRing) { + final PGPPublicKey key = keyRing.getPublicKey(); + if (key != null) { + final PgpKeyId keyId = new PgpKeyId(key); + discoveredKeyIds.add(keyId); + final Iterator userIdIterator = key.getUserIDs(); + while (userIdIterator.hasNext()) { + final String userId = userIdIterator.next(); + final Iterator signatureIterator = key + .getSignaturesForID(userId); + while (signatureIterator.hasNext()) { + final PGPSignature signature = signatureIterator.next(); + final PgpKeyId signatureKeyId = new PgpKeyId(signature); + discoveredKeyIds.add(signatureKeyId); + } + } + } + } + + private PGPPublicKeyRing trust(final PGPPublicKeyRing keyRing) { + rememberKey(keyRing); + + // Get the keys we currently trust + final List trusted = getTrustedKeys(); + + // Do not store if the first key is revoked + Validate.validState( + !keyRing.getPublicKey().isRevoked(), + "The public key ID '%s' has been revoked and cannot be trusted", + new PgpKeyId(keyRing.getPublicKey())); + + // trust it and write back to disk + trusted.add(keyRing); + OutputStream fos = null; + try { + final PGPPublicKeyRingCollection newCollection = new PGPPublicKeyRingCollection( + trusted); + fos = new FileOutputStream(ROO_PGP_FILE); + newCollection.encode(fos); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(fos); + } + return keyRing; + } + + private void trustDefaultKeys() { + // Get the URIs of all PGP keystore files within installed OSGi bundles + final List urls = new ArrayList( + OSGiUtils.findEntriesByPattern(context, + "/org/springframework/roo/felix/pgp/*.asc")); + Collections.sort(urls, new Comparator() { + public int compare(final URL url1, final URL url2) { + return url1.toExternalForm().compareTo(url2.toExternalForm()); + } + }); + + // Trust each one + for (final URL url : urls) { + InputStream inputStream = null; + try { + inputStream = url.openStream(); + trust(getPublicKey(inputStream)); + } + catch (final IOException ignored) { + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + } +} diff --git a/felix/src/main/java/org/springframework/roo/felix/pgp/SignatureDecision.java b/felix/src/main/java/org/springframework/roo/felix/pgp/SignatureDecision.java new file mode 100644 index 000000000..84988568a --- /dev/null +++ b/felix/src/main/java/org/springframework/roo/felix/pgp/SignatureDecision.java @@ -0,0 +1,41 @@ +package org.springframework.roo.felix.pgp; + +import java.io.InputStream; + +import org.apache.commons.lang3.Validate; +import org.bouncycastle.openpgp.PGPSignature; + +/** + * Represents the result of a signature verification via + * {@link PgpService#isSignatureAcceptable(InputStream)}. + * + * @author Ben Alex + * @since 1.1 + */ +public class SignatureDecision { + private final PGPSignature pgpSignature; + private final boolean signatureAcceptable; + private final PgpKeyId signatureAsHex; + + public SignatureDecision(final PGPSignature pgpSignature, + final PgpKeyId signatureAsHex, final boolean signatureAcceptable) { + Validate.notNull(pgpSignature, "PGP Signature required"); + Validate.notNull(signatureAsHex, "PGP Key ID required"); + this.pgpSignature = pgpSignature; + this.signatureAsHex = signatureAsHex; + this.signatureAcceptable = signatureAcceptable; + } + + public PGPSignature getPgpSignature() { + return pgpSignature; + } + + public PgpKeyId getSignatureAsHex() { + return signatureAsHex; + } + + public boolean isSignatureAcceptable() { + return signatureAcceptable; + } + +} diff --git a/felix/src/main/resources/org/springframework/roo/felix/pgp/aswan.asc b/felix/src/main/resources/org/springframework/roo/felix/pgp/aswan.asc new file mode 100644 index 000000000..0cf5ff220 --- /dev/null +++ b/felix/src/main/resources/org/springframework/roo/felix/pgp/aswan.asc @@ -0,0 +1,26 @@ +Public Key Server -- Get ``0xdc3cbbbd3fbaea78 '' + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.0 + +mQENBE3B/N8BCAC8IT1m2+BMzzSt7ybARszx4AYC0Cq6LOCoYUpMyT5IrOxsP4mHuE4oAj5U +wIEd7FFD4e6QU8pG8wwuaZNfht/GjXF9HMRDKuYyl97jAY6zodceHdD1x4k5FnWaYjHq7KeO +UJxx5BF1np1azMbu2NIF2SCBT8/ymjPnZimx68hn7Ea76Xv1xKGvdPtn8CKvUdcPANSiK/Gg +MCNGHXzrIQH/K0ObvnFsDLMwOGb6XxP5ZiVZtcE6c8NuQMhI26Fv7NiCx1CfT9e1QPDTjrCY +ODKYN+3qjorED61tnqeDj0SUUxmtHDrcda7bqinVN+CQoRFq5+L0Of9qmPrGTbQ3S+8zABEB +AAG0KUFuZHJldyBTd2FuIChSb28gdGVhbSkgPGFzd2FuQHZtd2FyZS5jb20+iQE4BBMBAgAi +BQJNwfzfAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDcPLu9P7rqeBqBB/9LzDdK +/gJCvKcAO+a+wQHzRIH6phOJUn1C1eSzF5rcMTlbdvu5WqB1012n7tNYmh+USHJ1O5QpoQqS +Ka65JGWYFJUiU279zXBEDiB0G5DcwySqSOKuZJCEcUtKcXlixLgoecRIY3qt8kIjRnqZuH79 +blx+WALqOZM+gS9BcnpffcS3jqEVb2ZnEXhJep+/mheQeXd8VE+psdNzl3VOFpPAF8jrZCL6 +noBokkFS77Lh5pQoaWr31AgyxmkMujC3dLqPla9OupTJIMHYsqk31lSSYG2wAYnVfW/gl7Y7 +ZkuB+wZ/s7F6VCeNMvjRsuv0VzKqqBibKPlkJR/qhbkWRbK+tDBBbmRyZXcgU3dhbiAoUm9v +IHRlYW0pIDxhbmRyZXcuaS5zd2FuQGdtYWlsLmNvbT6JATgEEwECACIFAk3CABECGw8GCwkI +BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJENw8u70/uup4pEoH/2QSnwz6RJ0W2jzeF7cSu9+q +BuZSqhsP0B2SSTou+7rIPPjZg8MA+IRHmacoLyZznF/hC+aVeTkH3Jv1Qgr6Aai8zPzzZc6U +yWWe+d5QkFVvPtXxZrqPlJLhrw/KeXFhMx/Wl1Ok9k2zkvlrIis/JLm5HsRnLgFKBe5olqOf +GaumSa4cXitp2fM/wKdXjptmE2xLtaddozKXmkfzapFuEkLOkM3V3cXSFrkuaTj54xmx9O7q +SDZh5wQUPqeAifGWB3IQqoZSNMmIot7cZ8PpkMTvKd0SV0t6Tn53M/E82OMt16SaY7LyDsiQ +PEpECc8whj58NPRzNmGwzbnvL2EFloI= +=AEUV +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/felix/src/main/resources/org/springframework/roo/felix/pgp/balex.asc b/felix/src/main/resources/org/springframework/roo/felix/pgp/balex.asc new file mode 100644 index 000000000..28cb7cdc4 --- /dev/null +++ b/felix/src/main/resources/org/springframework/roo/felix/pgp/balex.asc @@ -0,0 +1,77 @@ +Public Key Server -- Get ``0x9580063200b5050f '' + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.0 + +mQGiBEnNtSQRBACqtzj08yDzSUHVFvs1iNQI44dg2rWgAQu835aq5mcDfXkDBhf4hwbCiwVd +l8iXNHNhh77c64EISEpJMuAzz2oTmB7717NXkbtjubVYCOGxqzOCUivzDN0JWx1hTJ4QD9GB +5SWSLkzfPwPX8gF22M/GsgxhNm1udGqJBSi4d/QXuwCgguEHFWXloQHpI04eo9VH03FBQ38D +/1Zxla2KmmWRgHmhINfFluXo2j0pDjheNs+nliQ/KZnTnRTXuAeQjpjYHK4i7kFbyAnBRRHa +mt/cZ+dk5NRmAgdIGolnNJRr83jlcrcMv3EvKjEQa+HEvUhGqH1jMppMqDxkuZaAwqS1IdDe +siaUk4nNDSQcVC9nSRubLxzRT6hsBACGaiRMwyHyPcwUZwXNgyBE5TQBjPwjrBfrlWuW8qo3 +t/P49vjNHRkIeV80cF0eqklqMkTaOnUNeVQL/RBJBoCkMWoQ2dkbOANKkuS5VyDgX0X9/7BP +pvifcQayiI8krk18hfMs73kln5JeuWOcFm6IbmVocOagPBfbSoB5uVN4vbQbQmVuIEFsZXgg +PGJhbGV4QHZtd2FyZS5jb20+iGAEExECACAFAkudnQgCGyMGCwkIBwMCBBUCCAMEFgIDAQIe +AQIXgAAKCRCVgAYyALUFD5RXAJ9GS5aFPBfqKEpzBj58/cYiYmclBQCfaG5GkW9O8B6WXp2F +yAPR1vLxSzCJARwEEAECAAYFAkwQndQACgkQhPIAKGFjy55czQgAs7HQPlvMI2T8hQhR5dwX +OTUIIUz93+npeNRIA397CNw3oJr7LAzPBRwDqDmaYRShnH7gXjiguaFc/xJrxAoN4PADAMQM +w2E6mh18l8Z4sG3I+5na863sLZuCeBg3KmKxCh7aItDebEeO6ro0yiR5xXBeiIVnnwQflkQp +yx6OhEF5Od4H0W3d4OG6y/qZFZOjp+avfLaSx16ybzWIARlvZOKtABonRT6iOeSCeVTQJFd1 +tU4rWE/mmNABu9uzQOGMLTuAajUz9f9+OaBxaPLfycPxmPsG6evzY6/4Gl/k7rWNxEwJYan3 +8pds+vZI6hTZ5S1uiVtTKsuFg84WrxH/WokBHAQQAQIABgUCTBGDLgAKCRCTlYHp7GezldmE +CACOiz44AAkpHNjkvxaOr+KYoZ1+TtcuKOhHNJVly/t8FVQiUmagH26Tvv39+uO8Aa1pD2Wi +gX0FNRf1MieJkDVYgOydxtqkFzNLtobrYKGN0WmQpukJKjtG/9wHxAA8AKdJkPQOu2fm6V5C +ASQmek1orx8Ve1KxyOLL9vvlh8ez7RVA79ySqeL2T4ojp2AQsc5JqgSOsiBsB1GHboXkpuoX +BA70fgFYpAACaW+Z9otuKqB1z88sf8kLQpqP4uOuzfUy0W2sKr4dxNCEC3B16VtP0amYoSVi +4m6V/qCQHETXzShyJAhz9xhZJEaiuwhI3PMsID52Tn3IDOJ8kdS0LHC7tCBCZW4gQWxleCA8 +YmVuLmFsZXhAYWNlZ2kuY29tLmF1PohgBBMRAgAgBQJJzbUkAhsjBgsJCAcDAgQVAggDBBYC +AwECHgECF4AACgkQlYAGMgC1BQ9TjwCeLHEq4TpR6zJkBq43Kn02qihxw4QAniUWmFzs2cCQ +Nb9n/nsI2JwWLQ1tiGMEExECACMCGyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAUCSc21uAIZ +AQAKCRCVgAYyALUFD8cyAJwLpzh9DUd549OfJKjyGEmqN76YHgCdGBcW4xRT5u/3dCmmEtPZ +GbzmX1qJARwEEAECAAYFAkwQnYoACgkQhPIAKGFjy542IQgAqQcqPG6dGy5OlPYoecGSL/ph +Ijxc1QQzh5LU0oMHtQpNx/ORHYis3IELKkn7bpHkdl1u0VGz3UM9FGILpcEfiHNZopAJM6G4 +mkkIP49noQlhwr+Sefr929z7WscPMCpKRJSAFlzfHp8Ac9SAKLDcJzOeMvSNXSrjtvxgIEtv +rX+vpQLe39GyLyTo43wgTPSL5FhIjM42tGwFjP3eHUTg5qh+AfKkD6PS/cTSocZ5vlluyVd0 +cqv5cCdSTM3fi/Us8wXRCx+ANuM0K/1rdYfmLsTeBxeUEG42419sFxotrmICDwl+GxI0gKE4 +8jbvXKtN+sD1Y7lRoFbE3WbWNLEkZIkBHAQQAQIABgUCTBGDLgAKCRCTlYHp7GezlUjqB/42 +fHAaXYdqlUe240pVtgrMByOlMXob0ZYJAanXBXxaBeqIvb8d6DWKRtfuGneh/Fn4xom2fDQX +vg4SEOXs6CCcSdQTKe3kgxapN5C5HsTM5h8BDBJ9qvqEiMuDi+db4G3ylwLMLVrkx1w2GIt8 +Jos7Xdtajd1wbeaqt/15QqB3OQnxySUhxOgfreyRtomLyBa27gcX2sFCT6ADOaASXGP6I5At +SUfF+3B+ze8x8et4adYBTKI0HqR2ro6xoLEFMmqZgaV41whntMVxwzJGxKVGRhLDJpM53x4M +daxReQ6fQqozFNrVOKogsTvdd7ALRxJNj4V5HV0JloNLA65MSOgDtCRCZW4gQWxleCA8YmVu +LmFsZXhAc3ByaW5nc291cmNlLmNvbT6IYAQTEQIAIAUCSc21sQIbIwYLCQgHAwIEFQIIAwQW +AgMBAh4BAheAAAoJEJWABjIAtQUP/IAAn3TFJIrS/8jPH72nMFsx3ZDwQG8ZAJ44N+ctuKzk +Do59el3ysFBJaOOM1YkBHAQQAQIABgUCTBCd1AAKCRCE8gAoYWPLnrd1B/4qdDO+vdcmbXk9 +Ds0wlLzcgDExD8J0KlOior/8zcVvVkmKQyw03o/wtd8pmAMcYm2Y3k4mR2ptYUEQ26x3jJkK +xJDD7+pqyFngy/ywNEgrDXOw0jWOB9Z4lTh7vsv/q+EcCHKGyAIUJX+CImk0YD1riyLtJz3h +7iU4IKYxAYzlEsZnDKR1nNVfRDjTCfPP0xs3oLGOscIyH5uigc+gF8OfusH+NMsDcbSzJ8/u +Fbq76kz1LsqIOF5pPiCghzf2Fm/WT/cn1+0rRzdbheqNZnU3vlPqwCOqmAb5VxEw6qaxOkE1 +zYGRimi7Rh1PL5F0DjUnv2EJU37u30hruG10lzK7iQEcBBABAgAGBQJMEYMuAAoJEJOVgens +Z7OVGKMIAK/8IjBEGjbc0Unm6OWdt0BXSaXmgcH/h6mb3XzIrz+ynpSSbbrHdWe07sbDXBvQ +Vnd16VThfGkdCftapmuZQRfWyeeLuFE+g6H9GHOlZ+ntiazRcr0AEXZi/jI+u1z3hevl2by6 +6weFho8GhasAwz/68AmfBn6OgJmb4nEWtXNN+4ScP7PKQjCmCEiLHpAGO3Mh7im0V15eTEYM +tfCIhkcHcvML8bSb9MFgfQv2J0cASn1h43L9AorZXO6hkd+36HZUMx23BdwuQb80V56IGspk +9j6+BX9B6sJXqJOPmbZt/q8ObZujaZfnyPmNo7ZxL76bF90mDH37Kq35bbZRiBW5BA0ESc21 +JBAQAIpbY0z1F5bCo6+B9JNiZqknQx78cp2j23e7ROi08bW8N4+FiT7UYplJd1ftn5ist7KJ +r01pnRS0nPuy3d/gGCbNZ3pJU9qAm0N1udb6MCZK+rUAur07SqUunBA2s6Utvh0j/ImfCdxt +Qs6wD3mgAL2Vio01j1Z5iLDHji+rrmKjVZ6cPpFOkTsaHuzuovvLYYQvEKOMNwRYxEcMEGXO +S9HPr/Od8S+Otm4NOL+9G2ejaCLuoEVnmAJiROu5ZrD1v4X/5LGV8K2g910kwWpKbQeSZpcR +Tcgst10USLgfFEGEA9cApVtbgQ7tBc5Nz6BI61vhMDqD5sXe32I4f4YAUcdIDpeVxpo9h6aJ +kxQQt9c0ucz4bh1VfBZTHd3AeRwnV4KWjDCHPt2Jhx4SVfHfhfkkPt1jabah2XjvCjcvwJwA +oOLuLl7/95hpHckd0QJxo1bb4/He4SfdQ36btIJWOeS3+drQPAqO9bIMUqHM9L2n36asR2pm +4YxGpWaFEZwOBDxDOE6WJfY4WPqDhzY1t+NJLy2D3KIo49UzojhG0WKAclZn1eBCnCod5l8h +aInnK8OawZqKGgQV5rMFV2xxQLBY4GrQur5SkdXjZoHo3XtUgM7UOfYlZsqJBtybNazUAOPS +2+Mn8NI/AxpbX6OlBjV8Xw5FuLpUreneBHUYWPMzAAMGD/0aKu6RTydK0hS1wFUBkR2UFNKx +ra45e+KKCqFU82YI12BrByMMaVAQK7Jhv5KiV+la/EjW6dY595QmzV0p9Q/TgZFAwwyYkGAj +wEXre1ah9PKmLsGwBMQwbqbtoDX4QPoGlIX+lvceEZ5oNSKeTgi082RMUNM0AmBzRV+HgTXV +JB54nW2xoIHFQwaP51xUHdkcHcTufqUqYUw4WfhQQLW5NVelxSz1YtUO5gSUhlYPyJpVtfSL +z3cI4Jze4KVERm4F1flpDSKKQvrFXpvWkR/lguGj3STg6z0CqF0n+bX0FygNoE7zqD4F9Aco +5tg2GmMY3e370L7MkPr57w8Wb0PKsxQ1OKAdEaDh9hBZlGMBx0MPMKL5GWxq4pqpUs8vZ7Ac +f30sjhHpgymRxarqaKIvifN+H4ktWp0g2WgBAD/jOG8m2lTPU/pEDJ4+uYA0KKtfL+JQCi37 +2KNIkMPb5aOLtaYcddbVJIxIFvW15lHY/mLlsO2tEbgP3erxx1ccdZ4YQZF/9phMyuwTFL1T +1QwO01tVRCucQKOaDlCyvwDr+Dpn/TsDtKHqxIvwZbh2whHgxud+/Arb9OA/amyiTqZlhjuJ +OCdMQTg0e9aL6nUBhMFhFfdXnSjnPzO6G4aYa6fwGafW8jFFi2T9C9Rn7WdSqVPKSzUwGpi+ ++Rf9qV0hNohJBBgRAgAJBQJJzbUkAhsMAAoJEJWABjIAtQUPFCQAnivlYrAwDQmEd+f/uB5/ +lIz0gULNAJ9JLm4YT0qbyn6Zx7SR/NY+n6J4+g== +=d8qo +-----END PGP PUBLIC KEY BLOCK----- diff --git a/felix/src/main/resources/org/springframework/roo/felix/pgp/jtyrrell.asc b/felix/src/main/resources/org/springframework/roo/felix/pgp/jtyrrell.asc new file mode 100644 index 000000000..2e02beecf --- /dev/null +++ b/felix/src/main/resources/org/springframework/roo/felix/pgp/jtyrrell.asc @@ -0,0 +1,38 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG/MacGPG2 v2.0.14 (Darwin) + +mQENBExsewwBCACtKKh9k4jeqPwL7nMTZtBU3/4aVqNog3gM2k2ITpbbBtjP1P5/ +uQ/6z648n+C66aX0C6JbweJs0hiiKHOwmzscAEjfbQoAvHHYcPs5ExMXCrFzoHb3 +6jmAmWPqA/CnPIwF4sJj9pDUhmsShFeKgRNrYGQdm58lKVaYyVXCV89A+nvlaM+e +9aaJu8J47DzFrWQct6YFr2K7lJ3HjC9P8uF51gzZjR/3PKK3KAo380w5ikhrpiy1 +eVkLJCNTU/vKjLlFGR2irN1NGPdfFKJTWXjV82LumovCcCHHHsykmxQD4s0fsK2b +GKqF3dTa0B/tPw/YKTQkvDBw2+rwHEAANPAVABEBAAG0IkphbWVzIFR5cnJlbGwg +PGphbWVzQHR5cnJlbGxzLmNvbT6JATgEEwECACIFAkxsewwCGwMGCwkIBwMCBhUI +AgkKCwQWAgMBAh4BAheAAAoJEPxcmfaBCJdot0EIAJ6gP/roC+jfKbOpgNcHXSjF +fgcjfsYj8ZyXiulIFDpTVSXAZmM8+D6WoZ1n/zE4Nl4Lgi3Y0SaBppm3m4Uusswa +wBpAIr65muqwT8C+SCnRrLCTM4fwKx6ZRs11omYishaxWJnFmKqnFaxVP0xSivSl +9IeJnnWgQwUrULcruL3EcazXQaL3GOqyluA0m9552AyxnppPhQBsneCoBwb27ca3 +lEbN2bgC/8ThUs0j+hw4oA6zFaTQ/OEKg0bM1KkpW9yCJnJTuRB892DYr5Tla7A6 +GGoAWlJy5u128cFeXqP+75VLzAnFjSLZrI19gG/8AtUDzWmUQxFHZ1kKJeWOPyu0 +I0phbWVzIFR5cnJlbGwgPGp0eXJyZWxsQHZtd2FyZS5jb20+iQE4BBMBAgAiBQJN +obtEAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRD8XJn2gQiXaAEcB/9B +Ig5JiHGWTwLBzh/r1XOWiG67pO6qkmPNf1A2z/aa9tZ+B+2C4z/M4yxZk0PL0FKh +XoG2KYQEnICu41K/a5rbh1Z/ibizUAFV0sWAe+czL5rTeh1Env+S8Gfmp35eHioM +G8zyoa/XAxTHLferZUUKNEPkp6vn4qtPtgq+sW0iv+UOU0rUevGvTfbGVXwiNTpS +B8NJnBUgMX+J1j4Ait6wYm2t/cstztTA/JA8ytMSQEz6Nk68V5FbjnEsLPslL0fx +C285FNw5P6XZD90mjUjdmcPhEiBQcO4cI5cPmaoy3aMez2oWxhql6mgaoVnH3tzG +FCCr+VHdExHu9yshmwScuQENBExsewwBCADOKWcJTN51yI06GQk/8nEyB7ie+50J +2kPh6PnKCy5byI7ZEUUflQGfk6XoYUHK6MF5v/srIn/FVfq+wTwj4IuAK2GWqjrT +UEyPYvuIIuXT7CDHahdZLg5gcwpj3afx9hnQC1itIpmGZAMDegL42eYbwNNj+bO0 +4Sn0O8IRyriYlWrY9lRk/GSlrSptpiWiEuoi9YYjBCn/HoqHuIf1Y1DRPFdX6wOY +7xJ7frqs337+RWTflxHPYYSgx8mDaehOshUMBrq5egyXiCuIPIrimsXVnP/Umtwb +49yKHgVn5v+nU6fbsk3kTZBrlGNF+w1pyHSdhTCY92iEdG7oRas6J0FnABEBAAGJ +AR8EGAECAAkFAkxsewwCGwwACgkQ/FyZ9oEIl2ht9gf+NAbTupqDbekmAREa6wQ5 +oYbTo6IG5Ny27wG4pDP8mRHHb8IE+Tii6NBVuj3KX4WITA77YracXFbbJMDh/S/0 +zrvGCAo+1Mu/afPy7MpAZq2N7TCiLlsV7wEZx8x+ZMK04G0UEKQbtZgVLhNz0BCj +xtMoqNwtJcWkiz77mRREM0bFtis8FYh5nHvg495tDoHmwgQ/tQF+1RfVScWKkb4r +fCwdU0xeq/Xf7sPAaPLlbyp63y3AWr/rk498dD0yO6qhDnk1NDRU6fTvYL1P49mT +LaZcT8Vt0USqgwibL2Z5IMsuwZ2VY0m55qCQJ0FfNNJjHC4elBNR2/ipLuCvVg6e +aA== +=MKqo +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/felix/src/main/resources/org/springframework/roo/felix/pgp/roobuild.asc b/felix/src/main/resources/org/springframework/roo/felix/pgp/roobuild.asc new file mode 100644 index 000000000..84e51ae2b --- /dev/null +++ b/felix/src/main/resources/org/springframework/roo/felix/pgp/roobuild.asc @@ -0,0 +1,40 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.5 (GNU/Linux) + +mQGiBE1B2I8RBACThVNaMUyo2oUzbQRE3uoyCwvv5ugdTStUBm75HJCttCxfzyne +g2HOmwNtUdan03rgLcYpRd5mvtGc8hwC0+Y5QLjRd8yo4MkwKGJmAsytc1atNnnf +r5krf6f3lsbVhrBVA0VuD+KdwWftKLAk+C/Yoh7ibbzgDioaE57+i3snBwCg5tel +pAUipbTwDA3XTgvAMoB90gsD/1vRgiQCFq6wZZeO2knQSp2h7dTeO/qr+XwzYzqi +PAcnP+bTB95hiOv1nLZ3qiFrQR9WskiTsQxNS0qxkRRWf39ZOxh8n+CzOIhYyG6J +aRJd/M13dMEyDGxWL9mC94CASUbIiPMNOmc1HE0jvHMRg4jUdnpTcn33kri9CwSW +gXUcBACDm4/gl2f8UFfYY9RnuVouggKhR/UKN+eGmGFHxfP0qNyXlC3MfZ9ezEXY +IctBJGJzDoceZn2bgcjeWXiUyj/rK3M0el5enDB5cZcGIqaVIeh6Q8F9wJK2xjpT +CdsRrnm4BJtjF8anchasmOYcFy9ICLkHP1mQgPmtBY6l7SguvrQrU3ByaW5nIFJv +byBCdWlsZCBTZXJ2ZXIgPHMyLXJvb0B2bXdhcmUuY29tPohgBBMRAgAgBQJNQdiP +AhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQvyrzD1h7eWw41ACgym1mZ29C +xLJfnbiGu5JZ+w7o8WgAn1Zzik9Os4TGIp/znybPMpm1c0VVuQQNBE1B2ZgQEACV +WTO2RJ1S5e7n6S415lfFSoSBL7wSIPfxClGK3Gc9c0PQcO8o18WgsR1e0scyO+WG +pTfyNb5QTZEii5gDZaKyHt/u1+/3l/Q9hwneJNhEfYXiQnfB2KWrAeqLk8uJh2Xu +mH+mBf0m/rYVkrMb8LGM8Fboe99HecQ+mQKo7gs30dtQCLNtamedt6n89YOhwY2s +Dv3aKKJIXIVPrQ2lIB6nWU8AcyXbA4QaIYDGFSZxfCUaeDqIaTCFEdLaeRwwfHxC +Uodido4Zsi5LYwdHlPisdRi38oKBsI/W9Qssis5oYuFHMg/8DsSzoeI8GzPesfRm +P5tAHsbjXL4TSvVJYTzg0Lu46zwYdnX7M7CxuZNgRFz62hW2/7kjwxTnr/Ci66n9 +epJbi+WysX1axW+H4dNif5HdpPtt3FuHS21dD7twYwS/XnDAHyXOA2tC5/FxzrjY +hh/0ULkm75NI17ijBSIDD76MuyZ3MSpYBWQ0U0RJB/Iiq04pUiTHNYn2kvc7nAcr +UpvOHLT9WV6HaexamnkVb7dssYGKU1/VhCMjqO57cCBQEtXOQVKvQiumzVITxFli +A8jn/eOnFKUEHcObTL0aliFRa+Q9XR+yqCOm2KLJh+1h1YWud8fboLXkqWImwDyr +Vskfw9KrYo9Jmwk5WHOyDOpyQbCFyKev8WsyGfLCQwADBQ//f5mP/5UxDShCj8lY +njUPAplxLWartLFwQDSaoM+76ErlU5LKs4fX8EIToKsoinv/n/Fss+9oiYKpUcyT +Pvv0dNkMpFZl8hTPDwUZFO2wnF9uay+MkbUrYd/T9iEx62HLUS0J7V3EuL1v5rmg +jgIg/3LbV+yERGepwiEGov3NW5w7L6kKCFDlPK6uvzcKr722OAzvsSnB3/9Z+XmL +eEYCpWSP4okvk66BRi5ji46sXazbrhTXDcZnWtFUm4P7L8u9HI2ihNn0Die3yTwk +5k68QB4JBeJhzCpQY/yOUoueGpmHtU3U7Q+fbGBziO22GgzT/lcrr2X/loAR9mA5 +84PmTrdAYxR06H7VB7PrBQ6F9Zncpgl1nt84ZhNtSAlk+fwNqMDwsulxjXT2utTw +XICQn5ikvrxsx9wNnn/NJSybFWmKMDqskENZGVLqags4tKR02J5nasIO0+dVGOEE +/s+kcwNaTzhaMY5KUKNkdIUvti8ForyCYRp2VIql7ERb+DcL/adt8FCghBzhGIzV +XpNSe7OUSOsa3K6ZKBQv+GuLf5YnDgu8IotKXz829+8OPb1Y361LrNKl2tznNaBL +gj0PB8Uhc7S5dZ6AdQUVA8YACpFkjXAdLJs7vPq30AgviSy48OazPyn0DG4M5Ij8 +5G4exovLIlCR0oQ6Q0+LozyiDwGISQQYEQIACQUCTUHZmAIbDAAKCRC/KvMPWHt5 +bNliAKCgO6GYnCysXXOXFBIddgT/IF3CBwCgv0qai9RrpBtS1GDiQpzDgKpRza8= +=WANx +-----END PGP PUBLIC KEY BLOCK----- diff --git a/felix/src/main/resources/org/springframework/roo/felix/pgp/schmidts.asc b/felix/src/main/resources/org/springframework/roo/felix/pgp/schmidts.asc new file mode 100644 index 000000000..96b3a3f5a --- /dev/null +++ b/felix/src/main/resources/org/springframework/roo/felix/pgp/schmidts.asc @@ -0,0 +1,37 @@ +Public Key Server -- Get ``0x84f200286163cb9e '' + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.0 + +mQENBEwPL7wBCAC2tjrWPkanrwKuCRMke3ueZ4kxFBURVns3w38K8WnTHbr0kzSaMHr+beaS +pddXTLlA3BZzGkH5/c7troIxFgE/3wHh9CQJ53wSuf5UQa8Rd8jJ2oNF8UAPrISsLpr0fHht +xeNcpxmEzFCB/qZNFFOwGotDbqBZSZJgxmkDBr7joDj8nqo1YKiiErqRAv37orGC3JG6+Ci3 +ZUFO3r+ZYWjK7Cbnpmpx3MAZzUQB6XNbO405qDmATfaLch9IT3E3x6IJcsKvL5HBbWO4ZYaT +KjI5LupwUWX/fWHHMsrpgJXNZdBnXW1NUje7QYgAUlmY0cm6do1Fp2Bx5DAis7a7x4//ABEB +AAG0JFN0ZWZhbiBTY2htaWR0IDxzY2htaWR0c0B2bXdhcmUuY29tPohGBBMRAgAGBQJMEJsR +AAoJEJWABjIAtQUPu00An21KAzaisQ1b7/2pWB2YZuU/jcOeAJ0f+PZSt2nXAbVkY5yeHJho +Z97Q34kBHAQQAQIABgUCTBGDwwAKCRCTlYHp7GezlZLMB/9y+yjG7R3MaJ/32dZcClrBGpP4 +hsTgCdi4HAWJq3/nshn3W99+BvR4vEEJFuYVtbpzCpmDYXrs1bKRU3P4yps2A/QaiPiIZwsI +jBv1/au6VLPjiwVwM1qdSRf+WzoPaPgOiIDQYKCKyd5zkI8+cpO2rUa8ugUOEDs3veEbcRSS +KAbOYUuO5kXH9sVIaBV0NUvJjSoj8nL8/GDk2VPi6vejJiVCXUcYM4YKAvGB0FIPpVVL+RKA +npeV35owjO008Opjjqydc3Y7voJIBkUdek8w6MkME0rBAx3zuKUgF23AJfAwSQsHP814ovyl +F+sZDRxeDwZeEiN1ThTocSac3pkiiQE4BBMBAgAiBQJMDy+8AhsDBgsJCAcDAgYVCAIJCgsE +FgIDAQIeAQIXgAAKCRCE8gAoYWPLnuCBCACRUorCgd/ZsrcprlxAlQ3brxr8EXchqtv/KBw2 +Jr+zmD14qZb2jTVDlA8QXLvSpqlVPKcwYIqcYdos7ez7PWBZZXwfsRhtfhh2Jo4ajXSU6OCO +NmXSzrc0AETxwWBfuU8mNTm9EsYImJZkapvffWkmHqfiUJO8xwL0FgMEf+/nNZzMpEqUk7/Z +5PmZWv4s9/qPZxIh8G84cfyP7zwRDJQ+8FQBAcynitXYWu6gggp7+elqxWqcFrXGVsV0kQD/ +cKINrXcRK1JSKebGjiOIhlIUBgMRrtOm5OFkX+aLsZwLo6ZvtANs7X3PX1gi01LCMXFNS22O +nfvGJbr11+jOByJTuQENBEwPL7wBCADJp6wnaDMjbnEsG1GCRX0qaU8SqYKVhu0/QjkEc2FO +Qf0R2wVKpdycZBUHXwQIAwnT5hrwjNpVwYl0vpQidj18boskotrZuX/ouo05+WEzJdx05kfH +JNhBLvFZHvb8vc+Yq+cSNYSiyYrxqCzhLrKaP86J3RBl6PbFNvv0VZv5STaI8PGEZg5mKTMN +sw/HpYEWIN0DtG2wuf2PYs1mWQOtSB/ewLqF9wz5LF5IN1nMfnJ2nOHOb0H22xkA2WU/GzUQ +Kj9pbvuBCmp0+wXapvM+YIgJTRNNxxaxQpGZZWLf1uFo1Wb4FoHaaaIYJ/AnckjtzcFirOBH +TqX+H+Q3B6mzABEBAAGJAR8EGAECAAkFAkwPL7wCGwwACgkQhPIAKGFjy5583wf+MC9D3PrJ +8fn/3r7pRC655Hr4uPzOjF2OW70b0uLgjZwRXEszC/lb1N5q2N3BZGKiYK151xl+cdcvhyXR +s1UdhQ35NUdfcFpb0GkJuf7xMjgT23Ie2cN2ujaS/mCVux/nzQ5pDIMOXsiJ86gkBBhIclxd +WRAsFjgaAyDqZe/KLlyIh6B1nBsHwhWjcdO48/EQSFgUFj9ySXNCjFK+a86woHn/fh9tVS4C +kCnw8hGX1fOA0lcG7DtE5mNqCIMQl5jg4Oy1aaHGW1UO9EdHA/z8Dfzgt8vcDFNRg5ObeET2 +Rk5ijxDJEhiQBIs5ye4WjCgKaFYtASw3btrwJIEEIUGRsQ== +=arfx +-----END PGP PUBLIC KEY BLOCK----- + diff --git a/felix/src/main/resources/org/springframework/roo/felix/pgp/stewarta.asc b/felix/src/main/resources/org/springframework/roo/felix/pgp/stewarta.asc new file mode 100644 index 000000000..623f0b0d5 --- /dev/null +++ b/felix/src/main/resources/org/springframework/roo/felix/pgp/stewarta.asc @@ -0,0 +1,48 @@ +Public Key Server -- Get ``0x939581e9ec67b395 '' + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.0 + +mQENBEwPhgoBCADUqvUMJu1hcOMkSm4xE2RTQ3WdqycUB7qLN8qFiVFBSnY9J87BMhoi8k8P +4Dux39sqgJALK2nqd7w2WSjiWpB8oJNafGuKOSNRXHp9cWdDW8/2DG2RWmjDBiuMUroIYNYZ +lc+xz8LV0B/BPI2SP4mGBLYIfFD6SFBmqUWQ+UOs1BAdk5DtHL1aQxWvdrKD7OvPkwHYZIYG +oaVOjcKRCAbvRjEuXIXcGThn8fgEBBNUsxBo5emAjFT3D+awTTx5lF36KiaDtSyuY48RPNhD +VhFC1/ST2shKlW2eV+aj+vM0kel0DwuFDYT3CFAliy5mpeSB1jTBfHU4oQUk2i2DlYdjABEB +AAG0IkFsYW4gU3Rld2FydCA8c3Rld2FydGFAdm13YXJlLmNvbT6IRgQQEQIABgUCTBGDcwAK +CRCVgAYyALUFD3qQAJ9tnyWCEx0d0ioHQgUXCcdi0ogHgQCeJYBPham7AJQM1E6zZld2xpKs +EmWJATgEEwECACIFAkwRgWsCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJOVgens +Z7OV8DgIAM0jk9usbmthODG+8B0cGyRKxExOJ7UFCwTCr9RGVd2spemDuqusLKf3xY4y35bj +VBtWfPGhh1EQe0NIJbcO9JJnwhoULznDET6jprI0PV3fRTjtmROE5UhR2vkJx7J1FL58q7Py +S/G/nOiE1S/jQyRCFNQuaR3HQF1UixIa0IyVkcMGC/XE8IJbNnpEKFA2j1OLMBo+3Z2L+pvd +AN0o66lxEvobuqZE2Kz7FmLlQSqZEuvqPyGadBKiD1fEc8AZRfcIp1hohyF0d7Tcz4jn6iXw +/pTD+zkKKSFXP4AuQvJc0TB3bUZff+2NMg/QjFHzRiqPQfE9dWFxKKSUXi0lv7u0JUFsYW4g +U3Rld2FydCA8YWxhbmtzdGV3YXJ0QGdtYWlsLmNvbT6IRgQQEQIABgUCTBGEcwAKCRCVgAYy +ALUFD/90AJ42GoXOSalBAGmEAcSE8wx9PKN5UACfX0vfEEXGfBgo6i8i/YDVFoeFQOCJATgE +EwECACIFAkwRhCgCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJOVgensZ7OVSA8H +/i+KtUMluJFDVk/EVOSc8rSCRLXLpsrS65IqHJv1GWWK1WRT4soejuw1p3KL5FP4WMma80aF +L1Yno45WWo5WuLLe/mDHeaD7vaI/kwNr08tU2/y39gkVIt3ZhcSr0qj8N6EEnTzyZYEko/ny +cYGdEa7QwtO2ynnaWRptowhl3V8qF5gv8d/PbfVmoJac/rUvliOJSlRJDUe3ah8AYMTt3416 +3461SG43Fl/T0DKeIR9xqIqkLSkC8kU9CG7L1ly5SEvNmUh+QCrcxLjPUrspSQrd6lHESksh +xc+qTscHG+XGfgptVTGhuKXed3K/bUj7lLiUa9kzV0Y/An4VxMm6xpO0RUFsYW4gU3Rld2Fy +dCAoU2VuaW9yIE1lbWJlciBvZiBUZWNobmljYWwgU3RhZmYpIDxzdGV3YXJ0YUB2bXdhcmUu +Y29tPohGBBARAgAGBQJMEYNzAAoJEJWABjIAtQUPbc0An2Cds//YtDj8qOGNFBsBkOSAQDai +AJ4/JMM6tWSuT6Qx7lDFlit5wvaeLIkBOAQTAQIAIgUCTA+GCgIbAwYLCQgHAwIGFQgCCQoL +BBYCAwECHgECF4AACgkQk5WB6exns5UYLAgAmtHiplJ2HosK0ikJ0+fPDp+iZVVeGXEQrWl7 +TYVyNugryB11uK8u3xh64prFWRwB57Esp33JmK7IV8UEsSnlPKc3YizrMMPg6uDc4CmeVp6Q +8Usm6GZFNjwMpj/b6AHLy0aRb4X7Ic5ukd/+XgNBvcMo4KySbS6RYY+w8VHH3/H8ftYFQjF0 +OSsGoFLmIuJcOVj6/Sp9zqSu2tkIy2PXXJ43vbZNduHw1acs0pTCa5gD2fgVgDZ0NjuxZ2XX +2SGKoMirGyfB9pzWY3xJa33NHt0ok2gShjgNbhE+vOvFdAHFOEMboZGKQFwkFgvdUvEnI0yH +Hyphjr6umrabfwYsDLkBDQRMD4YKAQgA4AEo1s5KHWUEhQJ5OkTRYFriQCG8mgZPgkLcPta6 +bmKprD+/mztngZSr5Wwyvn3uGtpjzxMF99PWMxFmdYDMk2fJ+nrvLSR+qAW9HZd1ST3eeszA +iZpdFM7XlU9iOBSIteNcAdAkxZ+qQmgfr6LlJ3v7kiqUcB26pGZvGCEDUIhES4mjHZrDZN9+ +8oqqwCJ/ltuHfxOSFHAMyxKQVMi1PeSK6MqZmNfmJsZrgEwjgB7QCHPjIx0IteinTjCS/PBv +5kZCeFbt+CoglYJcXnUwzZFxaeYhCNk5F6iWkWDpWlBtZQnX7PJ0HBonex6FJ155Ga4VzuGu +bAeSC0wk0A6KgwARAQABiQEfBBgBAgAJBQJMD4YKAhsMAAoJEJOVgensZ7OVF3gIAJCiyo0Z +wYrzczTEDQw15VLzJTWEx73rZAzQk567xaezktQcei3roNttTbORFEF1PqoxHUIVqZA+AXXN +CcFLpmbz4g0YZMmU0dT6yhuHAoVbIkEjmvJEy33dm/WNQ4werIY+pa8NXPe5npv1ugziltzX +fq1JPP3dIvQgfMjWmIXZyDafIVQacW/o63LVlEwVtr2+3eUu+Ai172r6Qxb2UpqDRVklR7rS +BaDHcKU5S6PoIn3xkv9alK6VFQlDhn9+zJQGUbS0nIZ8brsx72k4uJNQjGB9aP23EoMqy03W +umYcY+5hYxQvEloz3AvrtC1BCPQjb9ga22mx/m0+bALYutI= +=sgEw +-----END PGP PUBLIC KEY BLOCK----- + diff --git a/file-monitor-polling-roo/pom.xml b/file-monitor-polling-roo/pom.xml new file mode 100644 index 000000000..0c1f1325e --- /dev/null +++ b/file-monitor-polling-roo/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.file.monitor.polling.roo + bundle + Spring Roo - File Monitor - Polling (Roo Launcher) + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.monitor.polling + + + \ No newline at end of file diff --git a/file-monitor-polling-roo/src/main/java/org/springframework/roo/file/monitor/polling/roo/PollingFileMonitorComponent.java b/file-monitor-polling-roo/src/main/java/org/springframework/roo/file/monitor/polling/roo/PollingFileMonitorComponent.java new file mode 100644 index 000000000..790f69f52 --- /dev/null +++ b/file-monitor-polling-roo/src/main/java/org/springframework/roo/file/monitor/polling/roo/PollingFileMonitorComponent.java @@ -0,0 +1,32 @@ +package org.springframework.roo.file.monitor.polling.roo; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.file.monitor.event.FileEventListener; +import org.springframework.roo.file.monitor.polling.PollingFileMonitorService; + +/** + * Extends {@link PollingFileMonitorService} by making it available as an OSGi + * component that automatically monitors the environment's + * {@link FileEventListener} components. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +@Reference(name = "fileEventListener", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = FileEventListener.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class PollingFileMonitorComponent extends PollingFileMonitorService { + + protected void bindFileEventListener(final FileEventListener listener) { + add(listener); + } + + protected void unbindFileEventListener(final FileEventListener listener) { + remove(listener); + } +} diff --git a/file-monitor-polling/pom.xml b/file-monitor-polling/pom.xml new file mode 100644 index 000000000..934a1b55d --- /dev/null +++ b/file-monitor-polling/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.file.monitor.polling + bundle + Spring Roo - File Monitor - Polling + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.file.monitor + + + \ No newline at end of file diff --git a/file-monitor-polling/src/main/java/org/springframework/roo/file/monitor/polling/PollingFileMonitorService.java b/file-monitor-polling/src/main/java/org/springframework/roo/file/monitor/polling/PollingFileMonitorService.java new file mode 100644 index 000000000..3d8adc481 --- /dev/null +++ b/file-monitor-polling/src/main/java/org/springframework/roo/file/monitor/polling/PollingFileMonitorService.java @@ -0,0 +1,681 @@ +package org.springframework.roo.file.monitor.polling; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.WeakHashMap; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.DirectoryMonitoringRequest; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.MonitoringRequest; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.file.monitor.event.FileEvent; +import org.springframework.roo.file.monitor.event.FileEventListener; +import org.springframework.roo.file.monitor.event.FileOperation; +import org.springframework.roo.support.util.FileUtils; + +/** + * A simple polling-based {@link FileMonitorService}. + *

    + * This implementation iterates over each of the {@link MonitoringRequest} + * instances, building an active file index at the time of execution. It then + * compares this active file index with the last time it was executed for that + * particular {@link MonitoringRequest}. Events are then fired, and only when + * the event firing process has completed is the next {@link MonitoringRequest} + * examined. + *

    + * This implementation does not recognize {@link FileOperation#RENAMED} events. + * This implementation will ignore any monitored files with a filename starting + * with a period (ie hidden files). + *

    + * In the case of {@link FileOperation#DELETED} events, this implementation will + * present in the {@link FileEvent} times equal to the last time a deleted file + * was modified. The time does NOT represent the deletion time nor the time the + * deletion was first detected. + * + * @author Ben Alex + * @since 1.0 + */ +public class PollingFileMonitorService implements NotifiableFileMonitorService { + + private final Set allFiles = new HashSet(); + private final Map> changeMap = new HashMap>(); + private final Set fileEventListeners = new HashSet(); + private final Object lock = new Object(); + private final Set notifyChanged = new HashSet(); + private final Set notifyCreated = new HashSet(); + private final Set notifyDeleted = new HashSet(); + private final Map> priorExecution = new WeakHashMap>(); + private final Set requests = new LinkedHashSet(); + + public final void add(final FileEventListener e) { + synchronized (lock) { + fileEventListeners.add(e); + } + } + + public boolean add(final MonitoringRequest request) { + synchronized (lock) { + Validate.notNull(request, "MonitoringRequest required"); + + // Ensure existing monitoring requests don't overlap with this new + // request; + // amend existing requests or ignore new request as appropriate + if (request instanceof DirectoryMonitoringRequest) { + final DirectoryMonitoringRequest dmr = (DirectoryMonitoringRequest) request; + if (dmr.isWatchSubtree()) { + for (final MonitoringRequest existing : requests) { + if (existing instanceof DirectoryMonitoringRequest) { + final DirectoryMonitoringRequest existingDmr = (DirectoryMonitoringRequest) existing; + if (existingDmr.isWatchSubtree()) { + // We have a new request and an existing + // request, both for directories, and both which + // monitor sub-trees + String existingDmrPath; + String newDmrPath; + try { + existingDmrPath = existingDmr.getFile() + .getCanonicalPath(); + newDmrPath = dmr.getFile() + .getCanonicalPath(); + } + catch (final IOException ioe) { + throw new IllegalStateException( + "Unable to resolve canonical name", + ioe); + } + // If the new request is a sub-directory of the + // existing request, ignore the new request as + // it's unnecessary + if (newDmrPath.startsWith(existingDmrPath)) { + return false; + } + // If the existing request is a sub-directory of + // the new request, remove the existing request + // as this new request + // will incorporate it + if (existingDmrPath.startsWith(newDmrPath)) { + remove(existing); + } + } + } + } + } + } + + return requests.add(request); + } + } + + /** + * Adds one or more entries into the Map. The key of the Map is the File + * object, and the value is the {@link File#lastModified()} time. + *

    + * Specifically: + *

      + *
    • If invoked with a File that is actually a File, only the file is + * added.
    • + *
    • If invoked with a File that is actually a Directory, all files and + * directories are added.
    • + *
    • If invoked with a File that is actually a Directory, subdirectories + * will be added only if "includeSubtree" is true.
    • + *
    + */ + private void computeEntries(final Map map, + final File currentFile, final boolean includeSubtree) { + Validate.notNull(map, "Map required"); + Validate.notNull(currentFile, "Current file is required"); + + if (!currentFile.exists() || currentFile.getName().length() > 1 + && currentFile.getName().startsWith(".") + || currentFile.getName().equals("log.roo") + || currentFile.isDirectory() + && isExcludedDirectory(currentFile.getPath())) { + return; + } + + map.put(currentFile, currentFile.lastModified()); + + try { + allFiles.add(currentFile.getCanonicalPath()); + } + catch (final IOException ignored) { + } + + if (currentFile.isDirectory()) { + final File[] files = currentFile.listFiles(); + if (files == null || files.length == 0) { + return; + } + for (final File file : files) { + if (file.isFile() || includeSubtree) { + computeEntries(map, file, includeSubtree); + } + } + } + } + + public SortedSet findMatchingAntPath(final String antPath) { + Validate.notBlank(antPath, "Ant path required"); + final SortedSet result = new TreeSet(); + // Now we need to compute the starting directory by reference to the + // first * in the Ant Path + int index = antPath.indexOf("*"); + // Conditionals are based on an index of 0 (not -1) to ensure the + // detected character is not the only character in the string + Validate.isTrue( + index > 0, + "'%s' is not an Ant Path as it fails to include an * character", + antPath); + String newPath = antPath.substring(0, index); + index = newPath.lastIndexOf(File.separatorChar); + Validate.isTrue(index > 0, + "'%s' fails to include any '%s' directory separator", antPath, + File.separatorChar); + newPath = newPath.substring(0, index); + final File somePath = new File(newPath); + if (!somePath.exists()) { + // Path at the start of the Ant expression doesn't exist, so there's + // no way we'll find anything via a search + return result; + } + Validate.isTrue( + somePath.isDirectory(), + "Ant path '%s' appears under file system path '%s' but this is not a directory that can be searched", + antPath, somePath); + recursiveAntMatch(antPath, somePath, result); + return result; + } + + public Collection getDirtyFiles(final String requestingClass) { + synchronized (lock) { + final Collection changesSinceLastRequest = changeMap + .get(requestingClass); + if (changesSinceLastRequest == null) { + changeMap.put(requestingClass, new LinkedHashSet()); + return new LinkedHashSet(allFiles); + } + final Collection copyOfChangesSinceLastRequest = new LinkedHashSet( + changesSinceLastRequest); + changesSinceLastRequest.clear(); + return copyOfChangesSinceLastRequest; + } + } + + private List getFileCreationEvents( + final MonitoringRequest request, final Map priorFiles) { + final List createEvents = new ArrayList(); + for (final Iterator iter = notifyCreated.iterator(); iter + .hasNext();) { + final String filePath = iter.next(); + if (isWithin(request, filePath)) { + iter.remove(); // We've processed it + // Skip this file if it doesn't exist + final File thisFile = new File(filePath); + if (thisFile.exists()) { + // Record the notification + createEvents.add(new FileEvent(new FileDetails(thisFile, + thisFile.lastModified()), FileOperation.CREATED, + null)); + // Update the prior execution map so it isn't notified again + // next round + priorFiles.put(thisFile, thisFile.lastModified()); + } + } + } + return createEvents; + } + + private List getFileDeletionEvents( + final MonitoringRequest request, final Map priorFiles) { + final List deleteEvents = new ArrayList(); + for (final Iterator iter = notifyDeleted.iterator(); iter + .hasNext();) { + final String filePath = iter.next(); + if (isWithin(request, filePath)) { + iter.remove(); // We've processed it + // Skip this file if it suddenly exists again (it shouldn't be + // in the notify deleted in this case!) + final File thisFile = new File(filePath); + if (!thisFile.exists()) { + // Record the notification + deleteEvents.add(new FileEvent(new FileDetails(thisFile, + null), FileOperation.DELETED, null)); + // Update the prior execution map so it isn't notified again + // next round + priorFiles.remove(thisFile); + } + } + } + return deleteEvents; + } + + private List getFileUpdateEvents( + final MonitoringRequest request, final Map priorFiles) { + final List updateEvents = new ArrayList(); + for (final Iterator iter = notifyChanged.iterator(); iter + .hasNext();) { + final String filePath = iter.next(); + if (isWithin(request, filePath)) { + iter.remove(); // We've processed it + // Skip this file if it doesn't exist + final File thisFile = new File(filePath); + if (thisFile.exists()) { + // Record the notification + updateEvents.add(new FileEvent(new FileDetails(thisFile, + thisFile.lastModified()), FileOperation.UPDATED, + null)); + // Update the prior execution map so it isn't notified again + // next round + priorFiles.put(thisFile, thisFile.lastModified()); + // Also remove it from the created list, if it's in there + if (notifyCreated.contains(filePath)) { + notifyCreated.remove(filePath); + } + } + } + } + return updateEvents; + } + + public List getMonitored() { + synchronized (lock) { + final List monitored = new ArrayList(); + if (requests.isEmpty()) { + return monitored; + } + + for (final MonitoringRequest request : requests) { + if (priorExecution.containsKey(request)) { + final Map priorFiles = priorExecution + .get(request); + for (final Entry entry : priorFiles.entrySet()) { + monitored.add(new FileDetails(entry.getKey(), entry + .getValue())); + } + } + } + + return monitored; + } + } + + public boolean isDirty() { + synchronized (lock) { + return !notifyChanged.isEmpty() || !notifyCreated.isEmpty() + || !notifyDeleted.isEmpty(); + } + } + + private boolean isExcludedDirectory(final String path) { + final boolean hasSrc = path.contains(File.separator + "src"); + return !hasSrc + && (path.contains(File.separator + "target") || path + .contains(File.separator + "bin")) || hasSrc + && path.contains(File.separator + "maven"); + } + + /** + * Decides whether we want to store this notification. This only happens if + * a monitoring request has indicated it is interested in this request. See + * ROO-794 for details. + * + * @param fileCanonicalPath to potentially keep + * @return true if the notification is able to be kept + */ + private boolean isNotificationUnderKnownMonitoringRequest( + final String fileCanonicalPath) { + synchronized (lock) { + for (final MonitoringRequest request : requests) { + if (isWithin(request, fileCanonicalPath)) { + return true; + } + } + } + return false; + } + + private boolean isWithin(final MonitoringRequest request, + final String filePath) { + String requestCanonicalPath; + try { + requestCanonicalPath = request.getFile().getCanonicalPath(); + } + catch (final IOException e) { + return false; + } + if (request instanceof DirectoryMonitoringRequest) { + final DirectoryMonitoringRequest dmr = (DirectoryMonitoringRequest) request; + if (dmr.isWatchSubtree()) { + if (!filePath.startsWith(requestCanonicalPath)) { + // Not within this directory or as ub-directory + return false; + } + } + else { + if (!FileUtils.matchesAntPath(requestCanonicalPath + + File.separator + "*", filePath)) { + return false; // Not within this directory + } + } + } + else { + if (!requestCanonicalPath.equals(filePath)) { + return false; // Not a file + } + } + return true; + } + + private boolean noRequestsOrChanges() { + return requests.isEmpty() || !isDirty(); + } + + public void notifyChanged(final String fileCanonicalPath) { + synchronized (lock) { + updateChanges(fileCanonicalPath, false); + if (isNotificationUnderKnownMonitoringRequest(fileCanonicalPath)) { + notifyChanged.add(fileCanonicalPath); + } + } + } + + public void notifyCreated(final String fileCanonicalPath) { + synchronized (lock) { + updateChanges(fileCanonicalPath, false); + if (isNotificationUnderKnownMonitoringRequest(fileCanonicalPath)) { + notifyCreated.add(fileCanonicalPath); + } + } + } + + public void notifyDeleted(final String fileCanonicalPath) { + synchronized (lock) { + updateChanges(fileCanonicalPath, true); + if (isNotificationUnderKnownMonitoringRequest(fileCanonicalPath)) { + notifyDeleted.add(fileCanonicalPath); + } + } + } + + /** + * Publish the events, if needed. + *

    + * This method assumes the caller has already acquired a synchronisation + * lock. + * + * @param eventsToPublish to publish (not null, but can be empty) + */ + private void publish(final List eventsToPublish) { + if (eventsToPublish.isEmpty()) { + return; + } + if (fileEventListeners.isEmpty() || eventsToPublish.isEmpty()) { + return; + } + for (final FileEvent event : eventsToPublish) { + updateChanges(event.getFileDetails().getCanonicalPath(), + event.getOperation() == FileOperation.DELETED); + for (final FileEventListener l : fileEventListeners) { + l.onFileEvent(event); + } + } + } + + private int publishRequestedFileEvents() { + int eventsPublished = 0; + for (final MonitoringRequest request : requests) { + final List eventsToPublish = new ArrayList(); + + // See when each file was last checked + Map priorFiles = priorExecution.get(request); + if (priorFiles == null) { + priorFiles = new HashMap(); + priorExecution.put(request, priorFiles); + } + + // Handle files apparently updated, created, or deleted since the + // last execution + eventsToPublish.addAll(getFileUpdateEvents(request, priorFiles)); + eventsToPublish.addAll(getFileCreationEvents(request, priorFiles)); + eventsToPublish.addAll(getFileDeletionEvents(request, priorFiles)); + + publish(eventsToPublish); + eventsPublished += eventsToPublish.size(); + } + return eventsPublished; + } + + /** + * Locates all files under the specified current directory which patch the + * given Ant Path. + * + * @param antPath to match (required) + * @param currentDirectory an existing directory to search from (required) + * @param result to append located files into (required) + */ + private void recursiveAntMatch(final String antPath, + final File currentDirectory, final SortedSet result) { + Validate.notNull(currentDirectory, "Current directory required"); + Validate.isTrue( + currentDirectory.exists() && currentDirectory.isDirectory(), + "Path '%s' does not exist or is not a directory", + currentDirectory); + Validate.notBlank(antPath, "Ant path required"); + Validate.notNull(result, "Result required"); + + final File[] listFiles = currentDirectory.listFiles(); + if (listFiles == null || listFiles.length == 0) { + return; + } + for (final File f : listFiles) { + try { + if (FileUtils.matchesAntPath(antPath, f.getCanonicalPath())) { + result.add(new FileDetails(f, f.lastModified())); + } + } + catch (final IOException ignored) { + } + + if (f.isDirectory()) { + recursiveAntMatch(antPath, f, result); + } + } + } + + public final void remove(final FileEventListener e) { + synchronized (lock) { + fileEventListeners.remove(e); + } + } + + public boolean remove(final MonitoringRequest request) { + synchronized (lock) { + Validate.notNull(request, "MonitoringRequest required"); + + // Advise of the cessation to monitoring + if (priorExecution.containsKey(request)) { + final List eventsToPublish = new ArrayList(); + + final Map priorFiles = priorExecution.get(request); + for (final Entry entry : priorFiles.entrySet()) { + final File thisFile = entry.getKey(); + final Long lastModified = entry.getValue(); + eventsToPublish.add(new FileEvent(new FileDetails(thisFile, + lastModified), FileOperation.MONITORING_FINISH, + null)); + } + publish(eventsToPublish); + } + + priorExecution.remove(request); + + return requests.remove(request); + } + } + + public int scanAll() { + synchronized (lock) { + if (requests.isEmpty()) { + return 0; + } + + int changes = 0; + + for (final MonitoringRequest request : requests) { + boolean includeSubtree = false; + if (request instanceof DirectoryMonitoringRequest) { + includeSubtree = ((DirectoryMonitoringRequest) request) + .isWatchSubtree(); + } + + if (!request.getFile().exists()) { + continue; + } + + // Build contents of the monitored location + final Map currentExecution = new HashMap(); + computeEntries(currentExecution, request.getFile(), + includeSubtree); + + final List eventsToPublish = new ArrayList(); + + if (priorExecution.containsKey(request)) { + // Need to perform a comparison, as we have data from a + // previous execution + final Map priorFiles = priorExecution + .get(request); + + // Locate created and modified files + for (final Entry entry : currentExecution + .entrySet()) { + final File thisFile = entry.getKey(); + final Long currentTimestamp = entry.getValue(); + if (!priorFiles.containsKey(thisFile)) { + // This file did not exist last execution, so it + // must be new + eventsToPublish.add(new FileEvent(new FileDetails( + thisFile, currentTimestamp), + FileOperation.CREATED, null)); + try { + // If this file was already going to be + // notified, there is no need to do it twice + notifyCreated.remove(thisFile + .getCanonicalPath()); + } + catch (final IOException ignored) { + } + continue; + } + + final Long previousTimestamp = priorFiles.get(thisFile); + if (!currentTimestamp.equals(previousTimestamp)) { + // Modified + eventsToPublish.add(new FileEvent(new FileDetails( + thisFile, currentTimestamp), + FileOperation.UPDATED, null)); + try { + // If this file was already going to be + // notified, there is no need to do it twice + notifyChanged.remove(thisFile + .getCanonicalPath()); + } + catch (final IOException ignored) { + } + } + } + + // Now locate deleted files + priorFiles.keySet().removeAll(currentExecution.keySet()); + for (final Entry entry : priorFiles.entrySet()) { + final File deletedFile = entry.getKey(); + eventsToPublish.add(new FileEvent(new FileDetails( + deletedFile, entry.getValue()), + FileOperation.DELETED, null)); + try { + // If this file was already going to be notified, + // there is no need to do it twice + notifyDeleted + .remove(deletedFile.getCanonicalPath()); + } + catch (final IOException ignored) { + } + } + } + else { + // No data from previous execution, so it's a + // newly-monitored location + for (final Entry entry : currentExecution + .entrySet()) { + eventsToPublish.add(new FileEvent(new FileDetails(entry + .getKey(), entry.getValue()), + FileOperation.MONITORING_START, null)); + } + } + + // Record the monitored location's contents, ready for next + // execution + priorExecution.put(request, currentExecution); + + // We can discard the created and deleted notifications, as they + // would have been correctly discovered in the above loop + notifyCreated.clear(); + notifyDeleted.clear(); + + // Explicitly handle any undiscovered update notifications, as + // this indicates an identical millisecond update occurred + for (final String canonicalPath : notifyChanged) { + final File file = new File(canonicalPath); + eventsToPublish.add(new FileEvent(new FileDetails(file, + file.lastModified()), FileOperation.UPDATED, null)); + } + notifyChanged.clear(); + publish(eventsToPublish); + + changes += eventsToPublish.size(); + } + + return changes; + } + } + + public int scanNotified() { + synchronized (lock) { + if (noRequestsOrChanges()) { + return 0; + } + return publishRequestedFileEvents(); + } + } + + private void updateChanges(final String fileCanonicalPath, + final boolean remove) { + for (final String requestingClass : changeMap.keySet()) { + if (remove) { + changeMap.get(requestingClass).remove(fileCanonicalPath); + } + else { + changeMap.get(requestingClass).add(fileCanonicalPath); + } + } + if (remove) { + allFiles.remove(fileCanonicalPath); + } + else { + allFiles.add(fileCanonicalPath); + } + } +} diff --git a/file-monitor/pom.xml b/file-monitor/pom.xml new file mode 100644 index 000000000..a9f2ddde2 --- /dev/null +++ b/file-monitor/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.file.monitor + bundle + Spring Roo - File Monitor + + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/DirectoryMonitoringRequest.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/DirectoryMonitoringRequest.java new file mode 100644 index 000000000..185119e4d --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/DirectoryMonitoringRequest.java @@ -0,0 +1,66 @@ +package org.springframework.roo.file.monitor; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.file.monitor.event.FileOperation; + +/** + * A request to monitor a particular directory. + * + * @author Ben Alex + * @since 1.0 + */ +public class DirectoryMonitoringRequest extends MonitoringRequest { + + private final boolean watchSubtree; + + /** + * Constructor that accepts a Collection of operations + * + * @param directory the directory to monitor; must be an existing directory + * @param watchSubtree whether to also monitor the sub-directories of the + * given directory + * @param notifyOn the operations to notify upon (can't be empty) + */ + public DirectoryMonitoringRequest(final File directory, + final boolean watchSubtree, final Collection notifyOn) { + super(directory, notifyOn); + Validate.isTrue(directory.isDirectory(), + "File '%s' must be a directory", directory); + this.watchSubtree = watchSubtree; + } + + /** + * Constructor that accepts an array of operations + * + * @param directory the directory to monitor; must be an existing directory + * @param watchSubtree whether to also monitor the sub-directories of the + * given directory + * @param notifyOn the operations to notify upon (can't be empty) + */ + public DirectoryMonitoringRequest(final File file, + final boolean watchSubtree, final FileOperation... notifyOn) { + this(file, watchSubtree, Arrays.asList(notifyOn)); + } + + /** + * @return whether all files and folders under this directory should also be + * monitored (to an unlimited depth). + */ + public boolean isWatchSubtree() { + return watchSubtree; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("directory", getFile()); + builder.append("watchSubtree", watchSubtree); + builder.append("notifyOn", getNotifyOn()); + return builder.toString(); + } +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/FileMonitorService.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/FileMonitorService.java new file mode 100644 index 000000000..b87d5bba4 --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/FileMonitorService.java @@ -0,0 +1,98 @@ +package org.springframework.roo.file.monitor; + +import java.util.Collection; +import java.util.List; +import java.util.SortedSet; + +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.file.monitor.event.FileEventListener; + +/** + * Provides a mechanism to monitor disk locations and publish events when those + * disk locations change. + *

    + * Implementations are required to monitor the locations expressed via the + * methods on this interface. The mechanism used for monitoring is an + * implementation choice. The order in which notifications must be published is + * unspecified. + *

    + * This API is provided as an interim measure until JSR 203 (to be included in + * Java Standard Edition, version 7 AKA "Dolphin") is widely available. Several + * useful prior works in the area of file monitoring includes JNotify and PNotify. + * + * @author Ben Alex + * @since 1.0 + */ +public interface FileMonitorService { + + /** + * @param request a monitoring request + * @return true if the monitor did not already contain the specified request + */ + boolean add(MonitoringRequest request); + + /** + * Locates all {@link FileDetails} which match the presented Ant path. + * + * @param antPath the Ant path to evaluate, as per the canonical file path + * format (required) + * @return all matching identifiers (may be empty, but never null) + */ + public SortedSet findMatchingAntPath(String antPath); + + /** + * Provides a list of canonical paths which represent changes to the file + * system since the requesting class last requested the change set. The + * returned change set is relative the requesting class in order to provide + * a list of changes to a number of callers, instead of a point of time + * snapshot which would change depending on the order invocation. This + * method should provide a more robust implementation of {@link #isDirty()} + * as the change set isn't cleared when a scan is performed and greater + * insight is given into what has changed instead of just indicating if the + * filesystem is dirty. + * + * @param requestingClass the invoking class (required) + * @return file system changes that occurred since the last invocation by + * the requesting class (may be empty, but never null) + */ + Collection getDirtyFiles(String requestingClass); + + /** + * Indicates the files currently being monitored, which is potentially + * useful for newly-registered {@link FileEventListener} instances that may + * have missed previous events. + * + * @return every file currently being monitored (never null, but may be + * empty if there are no files being monitored) + */ + List getMonitored(); + + /** + * Indicates on a best-efforts basis whether there are known changes to the + * disk which would be reported should {@link #scanAll()} be invoked. This + * method is not required to return a guaranteed outcome of what will happen + * should {@link #scanAll()} be invoked, but callers may rely on this method + * to assist with optimisations where applicable. + * + * @return true if there are known changes to be notified during the next + * {@link #scanAll} + */ + boolean isDirty(); + + /** + * @param request a monitoring request + * @return true if this set contained the specified element + */ + boolean remove(MonitoringRequest request); + + /** + * Execute a scan of all monitored locations. + * + * @return the number of changes detected during this invocation (can be 0 + * or above) + */ + int scanAll(); +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/FileMonitoringRequest.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/FileMonitoringRequest.java new file mode 100644 index 000000000..85fd310ff --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/FileMonitoringRequest.java @@ -0,0 +1,31 @@ +package org.springframework.roo.file.monitor; + +import java.io.File; +import java.util.Collection; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.file.monitor.event.FileOperation; + +/** + * A request to monitor a particular file. + * + * @author Ben Alex + * @since 1.0 + */ +public class FileMonitoringRequest extends MonitoringRequest { + + public FileMonitoringRequest(final File file, + final Collection notifyOn) { + super(file, notifyOn); + Validate.isTrue(file.isFile(), "File '%s' must be a file", file); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("resource", getFile()); + builder.append("notifyOn", getNotifyOn()); + return builder.toString(); + } +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/MonitoringRequest.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/MonitoringRequest.java new file mode 100644 index 000000000..27458cc38 --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/MonitoringRequest.java @@ -0,0 +1,99 @@ +package org.springframework.roo.file.monitor; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.event.FileOperation; + +/** + * Represents a request to monitor a particular file system location. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class MonitoringRequest { + + /** + * Factory method for monitoring the following operations upon the given + * file or directory: + *

      + *
    • {@link FileOperation#CREATED}
    • + *
    • {@link FileOperation#RENAMED}
    • + *
    • {@link FileOperation#UPDATED}
    • + *
    • {@link FileOperation#DELETED}
    • + *
    + * + * @param resource the resource to monitor; null means the + * current directory + * @return a non-null monitoring request + */ + public static MonitoringRequest getInitialMonitoringRequest(String resource) { + if (resource == null) { + resource = "."; + } + final MonitoringRequestEditor mre = new MonitoringRequestEditor(); + mre.setAsText(resource + ",CRUD"); + return mre.getValue(); + } + + /** + * Factory method for monitoring the following operations upon the given + * directory and its sub-tree: + *
      + *
    • {@link FileOperation#CREATED}
    • + *
    • {@link FileOperation#RENAMED}
    • + *
    • {@link FileOperation#UPDATED}
    • + *
    • {@link FileOperation#DELETED}
    • + *
    + * + * @param directory the directory to monitor; null means the + * current directory + * @return a non-null monitoring request + */ + public static MonitoringRequest getInitialSubTreeMonitoringRequest( + String directory) { + if (directory == null) { + directory = "."; + } + final MonitoringRequestEditor mre = new MonitoringRequestEditor(); + mre.setAsText(directory + ",CRUD,**"); + return mre.getValue(); + } + + private final Collection notifyOn; + private final File resource; + + /** + * Constructor + * + * @param resource the file to monitor (required) + * @param notifyOn the file operations to notify upon (can't be empty) + */ + protected MonitoringRequest(final File resource, + final Collection notifyOn) { + Validate.notNull(resource, "Resource to monitor is required"); + Validate.notEmpty(notifyOn, + "At least one FileOperation to monitor must be specified"); + this.notifyOn = new HashSet(notifyOn); + this.resource = resource; + } + + /** + * @return the file to be monitored (never null) + */ + public File getFile() { + return resource; + } + + /** + * Returns the operations to be monitored + * + * @return an unmodifiable collection containing one or more elements + */ + public Collection getNotifyOn() { + return Collections.unmodifiableCollection(notifyOn); + } +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/MonitoringRequestEditor.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/MonitoringRequestEditor.java new file mode 100644 index 000000000..2054f5c6d --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/MonitoringRequestEditor.java @@ -0,0 +1,139 @@ +package org.springframework.roo.file.monitor; + +import static org.springframework.roo.file.monitor.event.FileOperation.CREATED; +import static org.springframework.roo.file.monitor.event.FileOperation.DELETED; +import static org.springframework.roo.file.monitor.event.FileOperation.RENAMED; +import static org.springframework.roo.file.monitor.event.FileOperation.UPDATED; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.event.FileOperation; + +/** + * A {@link PropertyEditor} for {@link MonitoringRequest}s. + *

    + * The syntax expected by the editor is as follows: + *

    + * fullyQualifiedName + "," + fileOperationCodes + {"," + "**"} + * + *

    + * Where: + *

      + *
    • fullyQualifiedName is a {@link File}-resolvable name (required)
    • + *
    • fileOperationCodes is one or more of characters "C" (for create), "R" (for rename), + * "U" (for update) and "D" (for delete), as per {@link FileOperation} (required)
    • + *
    • literal "**" indicates to watch the subtree, which is only valid if the + * fullyQualifiedName is an existing directory (optional, but can only be used + * for a directory)
    • + *
    + * + * @author Ben Alex + * @since 1.0 + */ +public class MonitoringRequestEditor extends PropertyEditorSupport { + + private static final FileOperation[] MONITORED_OPERATIONS = { CREATED, + RENAMED, UPDATED, DELETED }; + private static final String SUBTREE_WILDCARD = "**"; + + /** + * @return this object in accordance with the string specification given in + * the JavaDocs (or null if the object null) + */ + @Override + public String getAsText() { + final MonitoringRequest req = getValue(); + if (req == null) { + return null; + } + final StringBuilder text = new StringBuilder(); + try { + text.append(req.getFile().getCanonicalPath()); + } + catch (final IOException ioe) { + throw new IllegalStateException( + "Failure retrieving path for request '" + req + "'", ioe); + } + text.append(","); + for (final FileOperation fileOperation : MONITORED_OPERATIONS) { + if (req.getNotifyOn().contains(fileOperation)) { + text.append(fileOperation.name().charAt(0)); + } + } + if (req instanceof DirectoryMonitoringRequest) { + final DirectoryMonitoringRequest dmr = (DirectoryMonitoringRequest) req; + if (dmr.isWatchSubtree()) { + text.append(",").append(SUBTREE_WILDCARD); + } + } + return text.toString(); + } + + @Override + public MonitoringRequest getValue() { + return (MonitoringRequest) super.getValue(); + } + + private Collection parseFileOperations( + final String fileOperationCodes) { + final Set fileOperations = new HashSet(); + for (final FileOperation fileOperation : MONITORED_OPERATIONS) { + if (fileOperationCodes.contains(fileOperation.name() + .substring(0, 1))) { + fileOperations.add(fileOperation); + } + } + return fileOperations; + } + + @Override + public void setAsText(final String text) throws IllegalArgumentException { + if (StringUtils.isBlank(text)) { + setValue(null); + return; + } + + final String[] segments = StringUtils.split(text, ","); + Validate.isTrue(segments.length == 2 || segments.length == 3, + "Text '%s' is invalid for a MonitoringRequest", text); + final File file = new File(segments[0]); + Validate.isTrue(file.exists(), "File '%s' does not exist", file); + + final Collection fileOperations = parseFileOperations(segments[1]); + Validate.notEmpty( + fileOperations, + "One or more valid operation codes ('CRUD') required for file '%s'", + file); + + if (file.isFile()) { + Validate.isTrue(segments.length == 2, + "Can only have two values for file '%s'", file); + setValue(new FileMonitoringRequest(file, fileOperations)); + } + else { + setValueToDirectoryMonitoringRequest(segments, file, fileOperations); + } + } + + private void setValueToDirectoryMonitoringRequest(final String[] segments, + final File file, final Collection fileOperations) { + if (segments.length == 3) { + Validate.isTrue( + SUBTREE_WILDCARD.equals(segments[2]), + "The third value for directory '%s' can only be '%s' (or completely remove the third parameter if you do not want to watch the subtree)", + file, SUBTREE_WILDCARD); + setValue(new DirectoryMonitoringRequest(file, true, fileOperations)); + } + else { + setValue(new DirectoryMonitoringRequest(file, false, fileOperations)); + } + } +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/NotifiableFileMonitorService.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/NotifiableFileMonitorService.java new file mode 100644 index 000000000..76442e709 --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/NotifiableFileMonitorService.java @@ -0,0 +1,68 @@ +package org.springframework.roo.file.monitor; + +import org.springframework.roo.file.monitor.event.FileEventListener; + +/** + * A {@link FileMonitorService} that permits callers to explicitly indicate they + * have changed a specific file. These files are guaranteed to be included in a + * change notification when the next {@link FileMonitorService#scanAll()} or + * {@link #scanNotified()} is called. + *

    + * This interface works around the practical problem that many file systems only + * provide precision to a whole second for file update operations. This + * precludes polling-based implementations (which rely on last update time) from + * identifying changes. The downside is this interface must be used by any type + * that can rapidly modify the file system (ie make more than one change per + * second). Failure to do so will mean some files can be updated in the same + * whole second but not be detected as updated. + *

    + * This interface also exists so there are lightweight methods available for + * explicitly recording disk changes and then publishing those changes without + * requiring a full scan. + * + * @author Ben Alex + * @since 1.0 + */ +public interface NotifiableFileMonitorService extends FileMonitorService { + + /** + * Indicates the canonical path specified should be treated as if it had + * changed. The last update time will become equal to actual disk timestamp. + *

    + * The implementation must only present the indicated file once in a given + * {@link FileMonitorService#scanAll()} or {@link #scanNotified()} + * invocation, even if this method has been repeatedly called and/or the + * file was detected as changed using normal last updated timestamps. + *

    + * No attempt is made to verify whether the presented path is subject to + * monitoring or not. It is expected that {@link FileEventListener}s will + * ignore files they are not interested in. + * + * @param fileCanonicalPath required (not null) + */ + void notifyChanged(String fileCanonicalPath); + + void notifyCreated(String fileCanonicalPath); + + /** + * Notifies this service that the given file system resource is about to be + * deleted + * + * @param fileCanonicalPath + */ + void notifyDeleted(String fileCanonicalPath); + + /** + * Similar to {@link #scanAll()} except will only notify those files + * explicitly advised via notification methods on + * {@link NotifiableFileMonitorService}. This is designed to allow faster + * operation where a full disk scan (as would be provided by + * {@link #scanAll()} is unnecessary. + *

    + * Note that executing this method will result in change notifications + * + * @return the number of changes detected during this invocation (can be 0 + * or above) + */ + int scanNotified(); +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileDetails.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileDetails.java new file mode 100644 index 000000000..1feb7f2ee --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileDetails.java @@ -0,0 +1,189 @@ +package org.springframework.roo.file.monitor.event; + +import java.io.File; +import java.util.Date; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.support.util.FileUtils; + +/** + * The details of a file that once existed on the disk. + *

    + * Instances of this class are usually included within a {@link FileEvent} + * object. + * + * @author Ben Alex + * @since 1.0 + */ +public class FileDetails implements Comparable { + + /** + * Returns the canonical path of the given {@link File}. + * + * @param file the file for which to find the canonical path (required) + * @return the canonical path + * @deprecated use {@link FileUtils#getCanonicalPath(File)} instead + */ + @Deprecated + public static String getCanonicalPath(final File file) { + return FileUtils.getCanonicalPath(file); + } + + /** + * Indicates whether the given canonical path matches the given Ant-style + * pattern + * + * @param antPattern the pattern to check against (can't be blank) + * @param canonicalPath the path to check (can't be blank) + * @return see above + * @deprecated use {@link FileUtils#matchesAntPath(String, String)} instead + */ + @Deprecated + public static boolean matchesAntPath(final String antPattern, + final String canonicalPath) { + return FileUtils.matchesAntPath(antPattern, canonicalPath); + } + + private final File file; + private final Long lastModified; + + /** + * Constructor + * + * @param file the file for which these are the details (required) + * @param lastModified the system clock in milliseconds when this file was + * last modified (can be null) + */ + public FileDetails(final File file, final Long lastModified) { + Validate.notNull(file, "File required"); + this.file = file; + this.lastModified = lastModified; + } + + public int compareTo(final FileDetails o) { + if (o == null) { + throw new NullPointerException(); + } + // N.B. this is in reverse order to how we'd normally compare + int result = o.getFile().compareTo(file); + if (result == 0) { + result = ObjectUtils.compare(o.getLastModified(), lastModified); + } + return result; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof FileDetails && compareTo((FileDetails) obj) == 0; + } + + /** + * Each {@link FileDetails} is known by its canonical file name, which is + * also the format used for Ant path matching etc. This method provides the + * canonical file name without forcing the user to deal with the exceptions + * that would arise from using {@link File} directly. + * + * @return the canonical path. + */ + public String getCanonicalPath() { + return FileUtils.getCanonicalPath(file); + } + + /** + * @return the file that is subject of this status object (indicates the new + * name in the case of a {@link FileOperation#RENAMED}). + */ + public File getFile() { + return file; + } + + /** + * The {@link FileMonitorService} is required to advise of last modification + * times. This method provides access to the modification time according to + * {@link FileMonitorService}, which may be out of date due to the polling + * mechanisms often used by implementations. Instead you should generally + * use {@link #getFile()#lastModified} for the most accurate disk-derived + * representation of the last modification time. + * + * @return the time the file was last modified, or in the case of a delete, + * it is implementation-specific (may return null) + */ + public Long getLastModified() { + return lastModified; + } + + /** + * Returns the portion of the child identifier that is relative to the + * parent {@link FileDetails} instance. Note that this instance must be the + * parent. + *

    + * If an empty string is returned from this method, it denotes the child was + * actually the same identifier as the parent. + * + * @param childCanonicalPath the confirmed child of this instance (required; + * use canonical path) + * @return the relative path within the parent instance (never null) + */ + public String getRelativeSegment(final String childCanonicalPath) { + Validate.notNull(childCanonicalPath, "Child identifier is required"); + Validate.isTrue(isParentOf(childCanonicalPath), + "Identifier '%s' is not a child of '%s'", childCanonicalPath, + this); + return childCanonicalPath.substring(getCanonicalPath().length()); + } + + @Override + public int hashCode() { + return 7 * file.hashCode() * ObjectUtils.hashCode(lastModified); + } + + /** + * Indicates whether the presented canonical path is a child of the current + * {@link FileDetails} instance. Put differently, returning true indicates + * the current instance is a parent directory of the presented + * possibleChildCanonicalPath. + *

    + * This method will return true if the presented child is a child of the + * current instance, or if the presented child is identical to the current + * instance. + * + * @param possibleChildCanonicalPath to evaluate (required) + * @return true if the presented possible child is indeed a child of the + * current instance + */ + public boolean isParentOf(final String possibleChildCanonicalPath) { + Validate.notBlank(possibleChildCanonicalPath, + "Possible child to evaluate is required"); + return FileUtils.ensureTrailingSeparator(possibleChildCanonicalPath) + .startsWith( + FileUtils.ensureTrailingSeparator(getCanonicalPath())); + } + + /** + * Indicates whether this file's canonical path matches the given Ant-style + * pattern. + *

    + * The presented path must be in Ant syntax. It should include a full prefix + * that is consistent with the {@link #getCanonicalPath()} method. + * + * @param antPattern the pattern to check this file against (cannot be + * blank) + * @return whether the path matches or not + */ + public boolean matchesAntPath(final String antPattern) { + return FileUtils.matchesAntPath(antPattern, getCanonicalPath()); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("file", file); + builder.append("exists", file.exists()); + builder.append("lastModified", lastModified == null ? "Unavailable" + : new Date(lastModified).toString()); + return builder.toString(); + } +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileEvent.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileEvent.java new file mode 100644 index 000000000..9abe3d943 --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileEvent.java @@ -0,0 +1,73 @@ +package org.springframework.roo.file.monitor.event; + +import java.io.File; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Represents a file notification message. + *

    + * There are three types of file event notifications: + *

      + *
    • An event with {@link FileOperation#MONITORING_START} when a file is first + * detected on the disk and will be monitored.
    • + *
    • An event with {@link FileOperation#MONITORING_FINISH} when a file that + * has been monitored is no longer going to be monitored.
    • + *
    • An event with any other {@link FileOperation} code when the file is + * created, updated, deleted or (if available) renamed.
    • + *
    + * + * @author Ben Alex + * @since 1.0 + */ +public class FileEvent { + private final FileDetails fileDetails; + private final FileOperation operation; + private final File previousName; + + public FileEvent(final FileDetails fileDetails, + final FileOperation operation, final File previousName) { + Validate.notNull(fileDetails, "File details required"); + Validate.notNull(operation, "File operation required"); + this.fileDetails = fileDetails; + this.operation = operation; + this.previousName = previousName; + } + + /** + * @return the file that is subject of this event (never null). + */ + public FileDetails getFileDetails() { + return fileDetails; + } + + /** + * @return the file operation being performed (never null). + */ + public FileOperation getOperation() { + return operation; + } + + /** + * If supported by the implementation, indicates the old name of the + * resource. Implementations are not required to support rename + * notifications. + * + * @return the old name of the file being {@link FileOperation#RENAMED} + * (will be null if not a rename notification). + */ + public File getPreviousName() { + return previousName; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("fileDetails", fileDetails); + builder.append("operation", operation); + builder.append("previousName", previousName); + return builder.toString(); + } + +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileEventListener.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileEventListener.java new file mode 100644 index 000000000..31e12c55b --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileEventListener.java @@ -0,0 +1,19 @@ +package org.springframework.roo.file.monitor.event; + +/** + * Implemented by classes that wish to be notified of file system changes. + * + * @author Ben Alex + * @since 1.0 + */ +public interface FileEventListener { + + /** + * Invoked by a + * {@link org.springframework.roo.file.monitor.polling.PollingFileMonitorService} + * to report a new status. + * + * @param fileEvent the file event (never null) + */ + void onFileEvent(FileEvent fileEvent); +} diff --git a/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileOperation.java b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileOperation.java new file mode 100644 index 000000000..7446fc6a6 --- /dev/null +++ b/file-monitor/src/main/java/org/springframework/roo/file/monitor/event/FileOperation.java @@ -0,0 +1,47 @@ +package org.springframework.roo.file.monitor.event; + +/** + * Represents the type of operations possible on a file or directory. + * + * @author Ben Alex + * @since 1.0 + */ +public enum FileOperation { + /** + * Represents a file or directory creation event. + */ + CREATED, + + /** + * Represents a file or directory deletion event. Guaranteed to be fired + * before any {@link #MONITORING_FINISH}. + */ + DELETED, + + /** + * Represents a file that will no longer be monitored. This usually follows + * a removal request, or a deletion. Once fired, a {@link #MONITORING_START} + * will be fired before any other {@link FileOperation} status codes for + * that same file (for example, if the file is re-monitored or re-created). + */ + MONITORING_FINISH, + + /** + * Represents a file that has been initially detected on the file system and + * will be monitored. Guaranteed to be fired before any other + * {@link FileOperation} status code. + */ + MONITORING_START, + + /** + * Represents a file or directory rename event; note this may not be + * available on certain implementations (in which case a DELETED and CREATED + * event would be issued instead). + */ + RENAMED, + + /** + * Represents a file or directory modification event. + */ + UPDATED, +} diff --git a/file-monitor/src/test/java/org/springframework/roo/file/monitor/MonitoringRequestEditorTest.java b/file-monitor/src/test/java/org/springframework/roo/file/monitor/MonitoringRequestEditorTest.java new file mode 100644 index 000000000..d7f039703 --- /dev/null +++ b/file-monitor/src/test/java/org/springframework/roo/file/monitor/MonitoringRequestEditorTest.java @@ -0,0 +1,203 @@ +package org.springframework.roo.file.monitor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.file.monitor.event.FileOperation.CREATED; +import static org.springframework.roo.file.monitor.event.FileOperation.DELETED; +import static org.springframework.roo.file.monitor.event.FileOperation.RENAMED; +import static org.springframework.roo.file.monitor.event.FileOperation.UPDATED; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.roo.file.monitor.event.FileOperation; + +/** + * Unit test of {@link MonitoringRequestEditor} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MonitoringRequestEditorTest { + + private static final File TEMP_DIR = new File( + System.getProperty("java.io.tmpdir")); + + private MonitoringRequestEditor editor; + // Fixture + private File testDirectory; + private File testFile; + + /** + * Asserts that the editor converts the given {@link MonitoringRequest} to + * the given text + * + * @param mockMonitoringRequest + * @param expectedText + * @throws Exception + */ + private void assertAsText(final MonitoringRequest mockMonitoringRequest, + final String expectedText) throws Exception { + // Set up + final File mockFile = mock(File.class); + when(mockMonitoringRequest.getFile()).thenReturn(mockFile); + when(mockFile.getCanonicalPath()).thenReturn("/path/to/file"); + final FileOperation[] operations = { CREATED, DELETED }; + when(mockMonitoringRequest.getNotifyOn()).thenReturn( + Arrays.asList(operations)); + editor.setValue(mockMonitoringRequest); + + // Invoke + final String text = editor.getAsText(); + + // Check + assertEquals(expectedText, text); + } + + /** + * Asserts that passing the given text to the + * {@link MonitoringRequestEditor} results in a null + * {@link MonitoringRequest}. + * + * @param text the text to pass (can be blank) + */ + private void assertCreatesNullMonitoringRequest(final String text) { + // Set up + editor.setAsText(text); + + // Invoke + final MonitoringRequest monitoringRequest = editor.getValue(); + + // Check + assertNull(monitoringRequest); + } + + /** + * Asserts that passing the given text to + * {@link MonitoringRequestEditor#setAsText(String)} results in a + * {@link MonitoringRequest} with the given values + * + * @param text the text to parse as a {@link MonitoringRequest} + * @param expectedFile the file we expect to be monitored + * @param expectedFileOperations the operations about which we expect to be + * notified + * @return the generated {@link MonitoringRequest} for any further + * assertions + */ + private MonitoringRequest assertMonitoringRequest(final String text, + final File expectedFile, + final FileOperation... expectedFileOperations) { + // Set up + editor.setAsText(text); + + // Invoke + final MonitoringRequest monitoringRequest = editor.getValue(); + + // Check + assertEquals(expectedFile, monitoringRequest.getFile()); + final Collection notifyOn = monitoringRequest + .getNotifyOn(); + assertEquals(expectedFileOperations.length, notifyOn.size()); + assertTrue("Expected " + Arrays.toString(expectedFileOperations) + + " but was " + notifyOn, + notifyOn.containsAll(Arrays.asList(expectedFileOperations))); + return monitoringRequest; + } + + @Before + public void setUp() throws Exception { + editor = new MonitoringRequestEditor(); + testDirectory = new File(TEMP_DIR, getClass().getSimpleName()); + testDirectory.mkdir(); + testFile = File.createTempFile(getClass().getSimpleName(), null); + } + + @After + public void tearDown() { + testDirectory.delete(); + testFile.delete(); + } + + @Test + public void testGetAsTextWhenMonitoringDirectoryAndSubTree() + throws Exception { + // Set up + final DirectoryMonitoringRequest mockMonitoringRequest = mock(DirectoryMonitoringRequest.class); + when(mockMonitoringRequest.isWatchSubtree()).thenReturn(true); + assertAsText(mockMonitoringRequest, "/path/to/file,CD,**"); + } + + @Test + public void testGetAsTextWhenMonitoringDirectoryOnly() throws Exception { + // Set up + final DirectoryMonitoringRequest mockMonitoringRequest = mock(DirectoryMonitoringRequest.class); + when(mockMonitoringRequest.isWatchSubtree()).thenReturn(false); + assertAsText(mockMonitoringRequest, "/path/to/file,CD"); + } + + @Test + public void testGetAsTextWhenMonitoringFile() throws Exception { + assertAsText(mock(MonitoringRequest.class), "/path/to/file,CD"); + } + + @Test + public void testGetAsTextWhenNoValueSet() { + assertNull(editor.getAsText()); + } + + @Test + public void testMonitorDirectoryAndSubtreeForDelete() { + final MonitoringRequest monitoringRequest = assertMonitoringRequest( + testDirectory.getAbsolutePath() + ",D,**", testDirectory, + DELETED); + final DirectoryMonitoringRequest directoryMonitoringRequest = (DirectoryMonitoringRequest) monitoringRequest; + assertTrue(directoryMonitoringRequest.isWatchSubtree()); + } + + @Test + public void testMonitorDirectoryButNotSubtreeForRename() { + final MonitoringRequest monitoringRequest = assertMonitoringRequest( + testDirectory.getAbsolutePath() + ",R", testDirectory, RENAMED); + final DirectoryMonitoringRequest directoryMonitoringRequest = (DirectoryMonitoringRequest) monitoringRequest; + assertFalse(directoryMonitoringRequest.isWatchSubtree()); + } + + @Test + public void testMonitorFileForRenameUpdateOrDelete() { + assertMonitoringRequest(testFile.getAbsolutePath() + ",RUD", testFile, + RENAMED, UPDATED, DELETED); + } + + @Test(expected = IllegalArgumentException.class) + public void testMonitoringSubTreeOfFileIsInvalid() { + editor.setAsText(testFile.getAbsolutePath() + ",C,**"); + } + + @Test + public void testSettingEmptyAsTextCreatesNullValue() { + assertCreatesNullMonitoringRequest(""); + } + + @Test + public void testSettingNullAsTextCreatesNullValue() { + assertCreatesNullMonitoringRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testSettingTextWithNoCommaIsInvalid() { + editor.setAsText("foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void testSettingTextWithNoOperationCodesIsInvalid() { + editor.setAsText("foo,"); + } +} diff --git a/file-monitor/src/test/java/org/springframework/roo/file/monitor/MonitoringRequestTest.java b/file-monitor/src/test/java/org/springframework/roo/file/monitor/MonitoringRequestTest.java new file mode 100644 index 000000000..7519dae84 --- /dev/null +++ b/file-monitor/src/test/java/org/springframework/roo/file/monitor/MonitoringRequestTest.java @@ -0,0 +1,63 @@ +package org.springframework.roo.file.monitor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.roo.file.monitor.event.FileOperation.CREATED; +import static org.springframework.roo.file.monitor.event.FileOperation.DELETED; +import static org.springframework.roo.file.monitor.event.FileOperation.RENAMED; +import static org.springframework.roo.file.monitor.event.FileOperation.UPDATED; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.springframework.roo.file.monitor.event.FileOperation; + +/** + * Unit test of {@link MonitoringRequest} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MonitoringRequestTest { + + private static final FileOperation[] CRUD_OPERATIONS = { CREATED, RENAMED, + UPDATED, DELETED }; + + /** + * Asserts that the given {@link MonitoringRequest} is for + * {@link #CRUD_OPERATIONS} on the current directory. + * + * @param monitoringRequest the request to check (required) + * @param expectedToWatchSubTree whether we expect the sub-tree to be + * monitored as well + */ + private void assertMonitorsCurrentDirectory( + final MonitoringRequest monitoringRequest, + final boolean expectedToWatchSubTree) { + assertEquals(DirectoryMonitoringRequest.class, + monitoringRequest.getClass()); + assertEquals(expectedToWatchSubTree, + ((DirectoryMonitoringRequest) monitoringRequest) + .isWatchSubtree()); + final Collection notifyOn = monitoringRequest + .getNotifyOn(); + assertEquals(CRUD_OPERATIONS.length, notifyOn.size()); + assertTrue(notifyOn.containsAll(Arrays.asList(CRUD_OPERATIONS))); + assertEquals(new File("."), monitoringRequest.getFile()); + } + + @Test + public void testGetMonitoringRequestForCurrentDirectoryAndSubTree() { + assertMonitorsCurrentDirectory( + MonitoringRequest.getInitialSubTreeMonitoringRequest(null), + true); + } + + @Test + public void testGetMonitoringRequestForCurrentDirectoryOnly() { + assertMonitorsCurrentDirectory( + MonitoringRequest.getInitialMonitoringRequest(null), false); + } +} diff --git a/file-monitor/src/test/java/org/springframework/roo/file/monitor/event/FileDetailsTest.java b/file-monitor/src/test/java/org/springframework/roo/file/monitor/event/FileDetailsTest.java new file mode 100644 index 000000000..a4e3e832b --- /dev/null +++ b/file-monitor/src/test/java/org/springframework/roo/file/monitor/event/FileDetailsTest.java @@ -0,0 +1,32 @@ +package org.springframework.roo.file.monitor.event; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.io.File; + +import org.junit.Test; + +/** + * Unit test of {@link FileDetails} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class FileDetailsTest { + + @Test + public void testInstancesWithSameFileAndNullTimestamp() { + // Set up + final File mockFile = mock(File.class); + final FileDetails fileDetails1 = new FileDetails(mockFile, null); + final FileDetails fileDetails2 = new FileDetails(mockFile, null); + + // Invoke and check + assertTrue(fileDetails1.equals(fileDetails2) + && fileDetails2.equals(fileDetails1)); + assertEquals(fileDetails1.hashCode(), fileDetails2.hashCode()); + assertEquals(0, fileDetails1.compareTo(fileDetails2)); + } +} diff --git a/file-undo/pom.xml b/file-undo/pom.xml new file mode 100644 index 000000000..475f2b89a --- /dev/null +++ b/file-undo/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.file.undo + bundle + Spring Roo - File Undo + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/CreateDirectory.java b/file-undo/src/main/java/org/springframework/roo/file/undo/CreateDirectory.java new file mode 100644 index 000000000..5732f6465 --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/CreateDirectory.java @@ -0,0 +1,72 @@ +package org.springframework.roo.file.undo; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * {@link UndoableOperation} to create a directory, including any parents. + *

    + * Note that the created instance will internally track the uppermost directory + * it created, and remove that directory during any undo operation. + * + * @author Ben Alex + * @since 1.0 + */ +public class CreateDirectory implements UndoableOperation { + + private static final Logger LOGGER = HandlerUtils + .getLogger(CreateDirectory.class); + + private final File actual; + private File deleteFrom; + private final FilenameResolver filenameResolver; + + public CreateDirectory(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File actual) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(actual, "Actual file required"); + Validate.notNull(filenameResolver, "Filename resolver required"); + Validate.isTrue(!actual.exists(), "Actual file '%s' cannot exist", + actual); + this.filenameResolver = filenameResolver; + this.actual = actual; + + // Figure out the first directory we should delete from + deleteFrom = actual; + while (true) { + final File parent = deleteFrom.getParentFile(); + if (!parent.exists()) { + deleteFrom = parent; + } + else { + break; + } + } + + Validate.validState(this.actual.mkdirs(), + "Could not create directory '%s'", actual); + undoManager.add(this); + LOGGER.fine("Created " + filenameResolver.getMeaningfulName(actual)); + } + + public void reset() { + } + + public boolean undo() { + boolean success = true; + try { + FileUtils.deleteDirectory(deleteFrom); + } + catch (IOException e) { + success = false; + } + LOGGER.fine((success ? "Undo create " : "Undo failed ") + + filenameResolver.getMeaningfulName(actual)); + return success; + } +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/CreateFile.java b/file-undo/src/main/java/org/springframework/roo/file/undo/CreateFile.java new file mode 100644 index 000000000..65339c24b --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/CreateFile.java @@ -0,0 +1,52 @@ +package org.springframework.roo.file.undo; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * {@link UndoableOperation} to create a file. + * + * @author Ben Alex + * @since 1.0 + */ +public class CreateFile implements UndoableOperation { + + private static final Logger LOGGER = HandlerUtils + .getLogger(CreateFile.class); + + private final File actual; + private final FilenameResolver filenameResolver; + + public CreateFile(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File actual) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(actual, "Actual file required"); + Validate.notNull(filenameResolver, "Filename resolver required"); + Validate.isTrue(!actual.exists(), "Actual file '%s' cannot exist", + actual); + this.filenameResolver = filenameResolver; + this.actual = actual; + try { + this.actual.createNewFile(); + } + catch (final IOException ioe) { + throw new IllegalStateException("Unable to create file '" + + this.actual + "'", ioe); + } + undoManager.add(this); + } + + public void reset() { + } + + public boolean undo() { + final boolean success = actual.delete(); + LOGGER.fine((success ? "Undo create " : "Undo failed ") + + filenameResolver.getMeaningfulName(actual)); + return success; + } +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/DefaultFilenameResolver.java b/file-undo/src/main/java/org/springframework/roo/file/undo/DefaultFilenameResolver.java new file mode 100644 index 000000000..c055de18e --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/DefaultFilenameResolver.java @@ -0,0 +1,26 @@ +package org.springframework.roo.file.undo; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.lang3.Validate; + +/** + * Default {@link FilenameResolver} that simply returns canonical file paths. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultFilenameResolver implements FilenameResolver { + + public String getMeaningfulName(final File file) { + Validate.notNull(file, "File required"); + try { + return file.getCanonicalPath(); + } + catch (final IOException ioe) { + throw new IllegalStateException("Could not resolve filename for '" + + file + "'", ioe); + } + } +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/DefaultUndoManager.java b/file-undo/src/main/java/org/springframework/roo/file/undo/DefaultUndoManager.java new file mode 100644 index 000000000..b31858cec --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/DefaultUndoManager.java @@ -0,0 +1,103 @@ +package org.springframework.roo.file.undo; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.undo.UndoEvent.UndoOperation; + +/** + * Default implementation of the {@link UndoManager} interface. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class DefaultUndoManager implements UndoManager { + + private final Set listeners = new HashSet(); + private final Stack stack = new Stack(); + private boolean undoEnabled = true; + + protected void activate(final ComponentContext context) { + } + + public void add(final UndoableOperation undoableOperation) { + Validate.notNull(undoableOperation, "Undoable operation required"); + stack.push(undoableOperation); + } + + public void addUndoListener(final UndoListener undoListener) { + listeners.add(undoListener); + } + + public void flush() { + notifyListeners(UndoOperation.FLUSH); + } + + private void notifyListeners(final UndoOperation operation) { + for (final UndoListener listener : listeners) { + listener.onUndoEvent(new UndoEvent(operation)); + } + } + + public void removeUndoListener(final UndoListener undoListener) { + listeners.remove(undoListener); + } + + public void reset() { + while (!stack.empty()) { + final UndoableOperation op = stack.pop(); + try { + op.reset(); + } + catch (final Throwable t) { + throw new IllegalStateException( + "UndoableOperation '" + + op + + "' threw an exception, in violation of the interface contract"); + } + } + notifyListeners(UndoOperation.RESET); + } + + public void setUndoEnabled(final boolean undoEnabled) { + this.undoEnabled = undoEnabled; + } + + public boolean undo() { + boolean undoMode = true; + if (!undoEnabled) { + // Force the undo stack to simply reset (but not perform any undos) + undoMode = false; + } + while (!stack.empty()) { + final UndoableOperation op = stack.pop(); + try { + if (undoMode) { + if (!op.undo()) { + // Undo failed, so switch to reset mode going forward + undoMode = false; + } + } + else { + // In reset mode + op.reset(); + } + } + catch (final Throwable t) { + throw new IllegalStateException( + "UndoableOperation '" + + op + + "' threw an exception, in violation of the interface contract"); + } + } + notifyListeners(UndoOperation.UNDO); + return undoMode; + } +} \ No newline at end of file diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/DeleteDirectory.java b/file-undo/src/main/java/org/springframework/roo/file/undo/DeleteDirectory.java new file mode 100644 index 000000000..e7431d7c6 --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/DeleteDirectory.java @@ -0,0 +1,126 @@ +package org.springframework.roo.file.undo; + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.FileUtils; + +/** + * {@link UndoableOperation} to delete a directory. + * + * @author Ben Alex + * @since 1.0 + */ +public class DeleteDirectory implements UndoableOperation { + + private static final Logger LOGGER = HandlerUtils + .getLogger(DeleteDirectory.class); + private static final File TEMP_DIRECTORY = new File( + System.getProperty("java.io.tmpdir")); + + private final File actual; + private final File backup; + private final FilenameResolver filenameResolver; + + /** + * Constructor that doesn't allow a reason to be given + * + * @param undoManager (required) + * @param filenameResolver (required) + * @param directory the directory to delete; must be an existing directory + * (not a file) + * @deprecated use the constructor that allows a reason to be provided + */ + @Deprecated + public DeleteDirectory(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File directory) { + this(undoManager, filenameResolver, directory, null); + } + + /** + * Constructor that allows a reason to be given + * + * @param undoManager (required) + * @param filenameResolver (required) + * @param directory the directory to delete; must be an existing directory + * (not a file) + * @param reason the reason for the directory's deletion; can be blank + * @since 1.2.0 + */ + public DeleteDirectory(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File directory, + final String reason) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(directory, "Actual file required"); + Validate.notNull(filenameResolver, "Filename resolver required"); + Validate.isTrue(directory.exists(), "File '%s' must exist", directory); + Validate.isTrue(directory.isDirectory(), + "Path '%s' must be a directory (not a file)", directory); + Validate.isTrue(TEMP_DIRECTORY.isDirectory(), + "Temporary directory '%s' is not a directory", TEMP_DIRECTORY); + actual = directory; + backup = new File(TEMP_DIRECTORY, "tmp_" + new Date().getTime() + + "_dir"); + this.filenameResolver = filenameResolver; + if (!FileUtils.copyRecursively(directory, backup, true)) { + throw new IllegalStateException( + "Unable to create a complete backup of directory '" + + directory + "'"); + } + try { + org.apache.commons.io.FileUtils.deleteDirectory(directory); + } + catch (IOException e) { + throw new IllegalStateException( + "Unable to completely delete directory '" + directory + "'"); + } + undoManager.add(this); + String deletionMessage = "Deleted " + + filenameResolver.getMeaningfulName(directory); + if (StringUtils.isNotBlank(reason)) { + deletionMessage += " - " + reason.trim(); + } + LOGGER.fine(deletionMessage); + } + + public void reset() { + // Fix for ROO-1555 + boolean success = true; + try { + org.apache.commons.io.FileUtils.deleteDirectory(backup); + } + catch (IOException e) { + success = false; + } + + try { + if (success) { + LOGGER.finest("Reset manage " + + filenameResolver.getMeaningfulName(backup)); + } + else { + backup.deleteOnExit(); + LOGGER.fine("Reset failed " + + filenameResolver.getMeaningfulName(backup)); + } + } + catch (final Throwable ignore) { + backup.deleteOnExit(); + LOGGER.fine("Reset failed " + + filenameResolver.getMeaningfulName(backup)); + } + } + + public boolean undo() { + final boolean success = FileUtils + .copyRecursively(backup, actual, false); + LOGGER.fine((success ? "Undo delete " : "Undo failed ") + + filenameResolver.getMeaningfulName(actual)); + return success; + } +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/DeleteFile.java b/file-undo/src/main/java/org/springframework/roo/file/undo/DeleteFile.java new file mode 100644 index 000000000..8062b23e1 --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/DeleteFile.java @@ -0,0 +1,116 @@ +package org.springframework.roo.file.undo; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * {@link UndoableOperation} to delete a file. + * + * @author Ben Alex + * @since 1.0 + */ +public class DeleteFile implements UndoableOperation { + + private static final Logger LOGGER = HandlerUtils + .getLogger(DeleteFile.class); + + private final File actual; + private final File backup; + private final FilenameResolver filenameResolver; + + /** + * Constructor that doesn't allow a reason to be given + * + * @param undoManager cannot be null + * @param filenameResolver cannot be null + * @param actual the file to delete; must be an existing file (not a + * directory) + * @deprecated use the constructor that allows a reason to be given + */ + @Deprecated + public DeleteFile(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File actual) { + this(undoManager, filenameResolver, actual, null); + } + + /** + * Constructor that allows a reason to be given + * + * @param undoManager cannot be null + * @param filenameResolver cannot be null + * @param actual the file to delete; must be an existing file (not a + * directory) + * @param reason the reason for the file's deletion (can be blank) + * @since 1.2.0 + */ + public DeleteFile(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File actual, + final String reason) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(actual, "File required"); + Validate.notNull(filenameResolver, "Filename resolver required"); + Validate.isTrue(actual.exists(), "File '%s' must exist", actual); + Validate.isTrue(actual.isFile(), + "Path '%s' must be a file (not a directory)", actual); + + try { + backup = File.createTempFile("DeleteFile", "tmp"); + FileUtils.copyFile(actual, backup); + } + catch (final IOException ioe) { + throw new IllegalStateException("Unable to make a backup of file '" + + actual + "'", ioe); + } + this.actual = actual; + this.actual.delete(); + this.filenameResolver = filenameResolver; + undoManager.add(this); + String deletionMessage = "Deleted " + + filenameResolver.getMeaningfulName(actual); + if (StringUtils.isNotBlank(reason)) { + deletionMessage += " - " + reason.trim(); + } + LOGGER.fine(deletionMessage); + } + + public void reset() { + // Fix for ROO-1555 + try { + if (backup.delete()) { + LOGGER.finest("Reset manage " + + filenameResolver.getMeaningfulName(backup)); + } + else { + backup.deleteOnExit(); + LOGGER.fine("Reset failed " + + filenameResolver.getMeaningfulName(backup)); + } + } + catch (final Throwable e) { + backup.deleteOnExit(); + LOGGER.fine("Reset failed " + + filenameResolver.getMeaningfulName(backup)); + } + } + + public boolean undo() { + try { + FileUtils.copyFile(backup, actual); + LOGGER.fine("Undo delete " + + filenameResolver.getMeaningfulName(actual)); + return true; + } + catch (final IOException ioe) { + LOGGER.fine("Undo failed " + + filenameResolver.getMeaningfulName(actual)); + return false; + } + } + +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/FilenameResolver.java b/file-undo/src/main/java/org/springframework/roo/file/undo/FilenameResolver.java new file mode 100644 index 000000000..e90fe5c26 --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/FilenameResolver.java @@ -0,0 +1,25 @@ +package org.springframework.roo.file.undo; + +import java.io.File; + +/** + * Interface to support {@link UndoableOperation} implementations rendering log + * messages with filename conventions applicable to the caller. + *

    + * This interface is primarily intended to allow more meaningful paths to be + * displayed than those available directly via {@link File}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface FilenameResolver { + /** + * Resolves the presented {@link File} into a meaningful name for display + * purposes. + * + * @param file to resolve (required) + * @return a string-based representation of the file name (never null or + * empty) + */ + String getMeaningfulName(File file); +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/UndoEvent.java b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoEvent.java new file mode 100644 index 000000000..55cbc8eec --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoEvent.java @@ -0,0 +1,42 @@ +package org.springframework.roo.file.undo; + +import org.apache.commons.lang3.Validate; + +/** + * An event delivered to an {@link UndoListener}. + * + * @author Ben Alex + * @since 1.1.1 + */ +public class UndoEvent { + + public enum UndoOperation { + FLUSH, RESET, UNDO + } + + private final UndoOperation operation; + + public UndoEvent(final UndoOperation operation) { + Validate.notNull(operation, "Operation required"); + this.operation = operation; + } + + public UndoOperation getOperation() { + return operation; + } + + public boolean isFlushing() { + return operation == UndoOperation.FLUSH; + } + + public boolean isResetting() { + return operation == UndoOperation.RESET; + } + + /** + * @return true if undoing, false if committing + */ + public boolean isUndoing() { + return operation == UndoOperation.UNDO; + } +} \ No newline at end of file diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/UndoListener.java b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoListener.java new file mode 100644 index 000000000..25c77e240 --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoListener.java @@ -0,0 +1,17 @@ +package org.springframework.roo.file.undo; + +/** + * Indicates an implementation of receiving {@link UndoEvent}s. + *

    + * Register via {@link UndoManager#addUndoListener(UndoListener)}. + * + * @author Ben Alex + * @since 1.1.1 + */ +public interface UndoListener { + + /** + * @param event the new event which took place (required) + */ + void onUndoEvent(UndoEvent event); +} \ No newline at end of file diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/UndoManager.java b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoManager.java new file mode 100644 index 000000000..606c433ef --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoManager.java @@ -0,0 +1,74 @@ +package org.springframework.roo.file.undo; + +import java.util.Stack; + +/** + * Provides the ability to undo changes to a file system. + *

    + * Note that the complexities of file system I/O are significant and Java does + * not provide full access to a file lock API that would simplify coding of + * implementations. Therefore undoing changes to the file system is a + * best-effort basis only, and designs should not rely on robust, guaranteed, + * fail-safe undo semantics. + * + * @author Ben Alex + * @since 1.0 + */ +public interface UndoManager { + + /** + * Registers an undoable operation in the {@link Stack}. + * + * @param undoableOperation to register in a {@link Stack} (required) + */ + void add(UndoableOperation undoableOperation); + + /** + * @param undoListener registers a new undo listener (required) + */ + void addUndoListener(UndoListener undoListener); + + /** + * Indicates a caller wishes the {@link UndoManager} or "flush" its + * contents. The exact meaning of a flush is implementation dependent. It is + * guaranteed to not change the undo stack, but simply notify + * {@link UndoListener}s. + */ + void flush(); + + /** + * @param undoListener removes a previously-registered undo listener + * (required) + */ + void removeUndoListener(UndoListener undoListener); + + /** + * Resets the undo {@link Stack}, and guarantees to clear the {@link Stack}. + *

    + * Executing this command guarantees the {@link Stack} will be empty upon + * return, with every element reset. + */ + void reset(); + + /** + * @param undoEnabled enables or disables the undo feature, which is useful + * for debugging (defaults to true) + */ + void setUndoEnabled(boolean undoEnabled); + + /** + * Replays the undo {@link Stack}, and guarantees to clear the {@link Stack} + * . + *

    + * The first operation that returns false to + * {@link UndoableOperation#undo()} will cause the remainder of the + * {@link Stack} to stop undoing. + *

    + * Executing this command guarantees the {@link Stack} will be empty upon + * return, with every element either undone or reset. + * + * @return true if all {@link UndoableOperation}s in the {@link Stack} were + * successfully undone + */ + boolean undo(); +} \ No newline at end of file diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/UndoableOperation.java b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoableOperation.java new file mode 100644 index 000000000..860a1de1c --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/UndoableOperation.java @@ -0,0 +1,32 @@ +package org.springframework.roo.file.undo; + +import java.util.logging.Logger; + +/** + * An operation than can be undone by {@link UndoManager}. + *

    + * An {@link UndoableOperation} is NOT permitted to throw any exception at any + * time. It should log any error conditions to the {@link Logger} only. + * + * @author Ben Alex + * @since 1.0 + */ +public interface UndoableOperation { + + /** + * Release any temporary resources consumed by the {@link UndoableOperation} + * . + *

    + * No exceptions may be thrown. + */ + void reset(); + + /** + * Attempt to undo the changes, and release any resources consumed. + *

    + * No exceptions may be thrown. + * + * @return whether the undo was successful or not + */ + boolean undo(); +} diff --git a/file-undo/src/main/java/org/springframework/roo/file/undo/UpdateFile.java b/file-undo/src/main/java/org/springframework/roo/file/undo/UpdateFile.java new file mode 100644 index 000000000..edf67af51 --- /dev/null +++ b/file-undo/src/main/java/org/springframework/roo/file/undo/UpdateFile.java @@ -0,0 +1,88 @@ +package org.springframework.roo.file.undo; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * {@link UndoableOperation} to update a file. + * + * @author Ben Alex + * @since 1.0 + */ +public class UpdateFile implements UndoableOperation { + + private static final Logger LOGGER = HandlerUtils + .getLogger(UpdateFile.class); + + private final File actual; + private final File backup; + private final FilenameResolver filenameResolver; + + /** + * Constructor + * + * @param undoManager cannot be null + * @param filenameResolver cannot be null + * @param actual the file to be updated; must be an existing file (not a + * directory) + */ + public UpdateFile(final UndoManager undoManager, + final FilenameResolver filenameResolver, final File actual) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(actual, "File required"); + Validate.isTrue(actual.exists(), "File '%s' must exist", actual); + Validate.isTrue(actual.isFile(), + "Path '%s' must be a file (not a directory)", actual); + Validate.notNull(filenameResolver, "Filename resolver required"); + this.filenameResolver = filenameResolver; + try { + backup = File.createTempFile("UpdateFile", "tmp"); + FileUtils.copyFile(actual, backup); + } + catch (final IOException ioe) { + throw new IllegalStateException("Unable to make a backup of file '" + + actual + "'", ioe); + } + this.actual = actual; + undoManager.add(this); + } + + public void reset() { + // Fix for ROO-1555 + try { + if (backup.delete()) { + LOGGER.finest("Reset manage " + + filenameResolver.getMeaningfulName(backup)); + } + else { + backup.deleteOnExit(); + LOGGER.fine("Reset failed " + + filenameResolver.getMeaningfulName(backup)); + } + } + catch (final Throwable e) { + backup.deleteOnExit(); + LOGGER.fine("Reset failed " + + filenameResolver.getMeaningfulName(backup)); + } + } + + public boolean undo() { + try { + FileUtils.copyFile(backup, actual); + LOGGER.fine("Undo manage " + + filenameResolver.getMeaningfulName(actual)); + return true; + } + catch (final IOException ioe) { + LOGGER.fine("Undo failed " + + filenameResolver.getMeaningfulName(actual)); + return false; + } + } +} diff --git a/metadata/pom.xml b/metadata/pom.xml new file mode 100644 index 000000000..47da14249 --- /dev/null +++ b/metadata/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.metadata + bundle + Spring Roo - Metadata + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/metadata/src/main/java/org/springframework/roo/metadata/AbstractHashCodeTrackingMetadataNotifier.java b/metadata/src/main/java/org/springframework/roo/metadata/AbstractHashCodeTrackingMetadataNotifier.java new file mode 100644 index 000000000..9019a7e04 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/AbstractHashCodeTrackingMetadataNotifier.java @@ -0,0 +1,123 @@ +package org.springframework.roo.metadata; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; + +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Allows a {@link MetadataProvider} or other class to track hash codes of + * {@link MetadataItem}s and only invoke + * {@link MetadataDependencyRegistry#notifyDownstream(String)} if there has been + * an actual change since the last notification. + *

    + * IMPORTANT: Before subclassing this class, ensure the {@link MetadataItem}s + * that you will be presenting are all of the same type AND they provide a + * reliable {@link Object#hashCode()} method. Failure to observe this + * requirement will result in erroneous notifications. + * + * @author Ben Alex + * @since 1.1 + */ +@Component(componentAbstract = true) +public abstract class AbstractHashCodeTrackingMetadataNotifier { + + protected final static Logger LOGGER = HandlerUtils.getLogger(AbstractHashCodeTrackingMetadataNotifier.class); + + // ------------ OSGi component attributes ---------------- + public BundleContext context; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + private final Map hashes = new HashMap(); + + protected MetadataDependencyRegistry metadataDependencyRegistry; + protected MetadataService metadataService; + + /** + * Notifies downstream dependencies of a change if and only if the passed + * metadata item has a different hash code than the existing metadata item. + * This is aimed at reducing needless notifications if nothing has actually + * changed since the last notification. + * + * @param metadataItem the potentially-updated metadata item (required; must + * be a metadata item of the same class as all other items + * presented to this class) + */ + protected void notifyIfRequired(final MetadataItem metadataItem) { + + final String instanceId = MetadataIdentificationUtils + .getMetadataInstance(metadataItem.getId()); + final Integer existing = hashes.get(instanceId); + final int newHash = metadataItem.hashCode(); + if (existing != null && newHash == existing) { + // No need to notify + return; + } + // To get this far, we need to notify and replace/add the metadata + // item's hash for future reference + hashes.put(instanceId, newHash); + + // Eagerly insert into the cache to so any recursive gets for this + // metadata item will be returned successfully + getMetadataService().put(metadataItem); + + getMetadataDependencyRegistry().notifyDownstream(metadataItem.getId()); + } + + public MetadataDependencyRegistry getMetadataDependencyRegistry(){ + if(metadataDependencyRegistry == null){ + // Get all Services implement MetadataDependencyRegistry interface + try { + ServiceReference[] references = context.getAllServiceReferences(MetadataDependencyRegistry.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataDependencyRegistry) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataDependencyRegistry on AbstractHashCodeTrackingNotifier."); + return null; + } + }else{ + return metadataDependencyRegistry; + } + + } + + public MetadataService getMetadataService(){ + if(metadataService == null){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on AbstractHashCodeTrackingNotifier."); + return null; + } + }else{ + return metadataService; + } + + } + +} \ No newline at end of file diff --git a/metadata/src/main/java/org/springframework/roo/metadata/AbstractMetadataItem.java b/metadata/src/main/java/org/springframework/roo/metadata/AbstractMetadataItem.java new file mode 100644 index 000000000..0dbda2759 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/AbstractMetadataItem.java @@ -0,0 +1,48 @@ +package org.springframework.roo.metadata; + +import org.apache.commons.lang3.Validate; + +/** + * Abstract implementation of {@link MetadataItem}. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractMetadataItem implements MetadataItem { + + /** + * Private to reinforce contractual immutability and formatting requirements + */ + private final String id; + + /** + * Defaults to true; protected to simplify superclass direct field + * modification + */ + protected boolean valid = true; + + /** + * Constructs an {@link AbstractMetadataItem} with the specified identifier, + * defaulting the {@link #isValid()} to true. + * + * @param id the metadata identification string for a particular instance + * (must return true when presented to + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)} + * ) + */ + protected AbstractMetadataItem(final String id) { + Validate.isTrue( + MetadataIdentificationUtils.isIdentifyingInstance(id), + "Metadata identification string '%s' does not identify a metadata instance", + id); + this.id = id; + } + + public final String getId() { + return id; + } + + public final boolean isValid() { + return valid; + } +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/DefaultMetadataLogger.java b/metadata/src/main/java/org/springframework/roo/metadata/DefaultMetadataLogger.java new file mode 100644 index 000000000..7d949bc39 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/DefaultMetadataLogger.java @@ -0,0 +1,172 @@ +package org.springframework.roo.metadata; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.Stack; +import java.util.TreeSet; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.metadata.internal.StandardMetadataTimingStatistic; + +/** + * Default implementation of {@link MetadataLogger}. + * + * @author Ben Alex + * @since 1.1.2 + */ +@Service +@Component +public class DefaultMetadataLogger implements MetadataLogger { + + private static class TimerEntry { + long clockStartedOrResumed; // nanos + long duration; // nanos + String responsibleClass; + } + + private long eventNumber = 0; + private final Stack eventStack = new Stack(); + private FileWriter fileLog; + /** + * key: responsible class, value: number of times a timing record was + * created for the responsible class + */ + private final Map invocations = new HashMap(); + private final Class mutex = DefaultMetadataLogger.class; + private final Stack timerStack = new Stack(); + /** key: responsible class, value: nanos occupied */ + private final Map timings = new HashMap(); + + private int traceLevel = 0; + + public DefaultMetadataLogger() { + if (System.getProperty("roo.metadata.trace") != null) { + traceLevel = 2; + } + } + + public SortedSet getTimings() { + final SortedSet result = new TreeSet(); + synchronized (mutex) { + for (final String key : timings.keySet()) { + result.add(new StandardMetadataTimingStatistic(key, timings + .get(key), invocations.get(key))); + } + } + return result; + } + + public int getTraceLevel() { + return traceLevel; + } + + public void log(final String message) { + Validate.notBlank(message, "Message to log required"); + Validate.isTrue(eventStack.size() > 0, + "Event stack is empty, so no logging should have been requested at this time"); + final StringBuilder sb = new StringBuilder("00000000"); + // Get the current event ID off the stack + final Long eventIdentifier = eventStack.get(eventStack.size() - 1); + // Figure out the indentation level + final int indentationLevel = eventStack.size(); + final String hex = Long.toHexString(eventIdentifier); + sb.replace(8 - hex.length(), 8, hex); + for (int i = 0; i < indentationLevel; i++) { + sb.append(" "); + } + sb.append(message); + logToFile(sb.toString()); + } + + private void logToFile(final String line) { + if (fileLog == null) { + try { + // Overwrite existing (don't append) + fileLog = new FileWriter("metadata.log", false); + } + catch (final IOException ignore) { + } + if (fileLog == null) { + // Still failing, so give up + return; + } + } + try { + fileLog.write(line + "\n"); // Unix line endings only from Roo + fileLog.flush(); // So tail -f will show it's working + } + catch (final IOException ignoreIt) { + } + } + + public void setTraceLevel(final int trace) { + traceLevel = trace; + } + + public void startEvent() { + eventNumber++; + eventStack.push(eventNumber); + } + + public void startTimer(final String responsibleClass) { + Validate.notBlank(responsibleClass, "Responsible class required"); + final long now = System.nanoTime(); + if (timerStack.size() > 0) { + // There is an existing timer on the stack, so we need to stop the + // clock for it + final TimerEntry timerEntry = timerStack.get(timerStack.size() - 1); + // Add the duration it ran to any existing duration + timerEntry.duration = timerEntry.duration + now + - timerEntry.clockStartedOrResumed; + timerEntry.clockStartedOrResumed = now; + } + // Start a new timer + final TimerEntry timerEntry = new TimerEntry(); + timerEntry.responsibleClass = responsibleClass; + timerEntry.clockStartedOrResumed = now; + timerStack.push(timerEntry); + } + + public void stopEvent() { + Validate.isTrue( + eventStack.size() > 0, + "Event stack is empty, indicating a mismatched number of timer start/stop calls"); + eventStack.pop(); + } + + public void stopTimer() { + Validate.isTrue( + timerStack.size() > 0, + "Timer stack is empty, indicating a mismatched number of timer start/stop calls"); + final long now = System.nanoTime(); + final TimerEntry timerEntry = timerStack.pop(); + timerEntry.duration = timerEntry.duration + now + - timerEntry.clockStartedOrResumed; + final String responsibleClass = timerEntry.responsibleClass; + + // Update the timings summary + synchronized (mutex) { + Long existingSummary = timings.get(responsibleClass); + if (existingSummary == null) { + existingSummary = timerEntry.duration; + } + else { + existingSummary = existingSummary + timerEntry.duration; + } + timings.put(responsibleClass, existingSummary); + + // Update the invocation count + Long existingInvocations = invocations.get(responsibleClass); + if (existingInvocations == null) { + existingInvocations = 0L; + } + existingInvocations++; + invocations.put(responsibleClass, existingInvocations); + } + } +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/DefaultMetadataService.java b/metadata/src/main/java/org/springframework/roo/metadata/DefaultMetadataService.java new file mode 100644 index 000000000..1a687c804 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/DefaultMetadataService.java @@ -0,0 +1,372 @@ +package org.springframework.roo.metadata; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.metadata.internal.AbstractMetadataCache; + +/** + * Default implementation of {@link MetadataService}. + *

    + * This implementation is not thread safe. It should only be accessed by a + * single thread at a time. This is enforced by the process manager semantics, + * so we avoid the cost of re-synchronization here. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +@Reference(name = "metadataProvider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = MetadataProvider.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class DefaultMetadataService extends AbstractMetadataCache implements + MetadataService { + + @Reference private MetadataDependencyRegistry metadataDependencyRegistry; + @Reference private MetadataLogger metadataLogger; + + // Request control + // List to assist output "stacks"which show the order of requests + private final List activeRequests = new ArrayList(); + private int cacheEvictions = 0; + private int cacheHits = 0; + private int cacheMisses = 0; + private int cachePuts = 0; + // List to help us verify correct operation through logs (predictable + // ordering) + private final List keysToRetry = new ArrayList(); + // Mutex + private final Object lock = new Object(); + private final Map providerMap = new HashMap(); + private final Set providers = new HashSet(); + private int recursiveGets = 0; + private int validGets = 0; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.addNotificationListener(this); + } + + protected void bindMetadataProvider(final MetadataProvider mp) { + synchronized (lock) { + Validate.notNull(mp, "Metadata provider required"); + final String mid = mp.getProvidesType(); + Validate.isTrue( + MetadataIdentificationUtils.isIdentifyingClass(mid), + "Metadata provider '%s' violated interface contract by returning '%s'", + mp, mid); + Validate.isTrue( + !providerMap.containsKey(mid), + "Metadata provider '%s' already is providing metadata for '%s'", + providerMap.get(mid), mid); + providers.add(mp); + providerMap.put(mid, mp); + } + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.removeNotificationListener(this); + } + + @Override + public void evict(final String metadataIdentificationString) { + synchronized (lock) { + // Clear my own cache (which also verifies the argument is valid at + // the same time) + super.evict(metadataIdentificationString); + + // Finally, evict downstream dependencies (ie metadata that + // previously depended on this now-evicted metadata) + for (final String downstream : metadataDependencyRegistry + .getDownstream(metadataIdentificationString)) { + // We only need to evict if it is an instance, as only an + // instance will ever go into the cache + if (MetadataIdentificationUtils + .isIdentifyingInstance(downstream)) { + evict(downstream); + } + } + } + } + + @Override + public void evictAll() { + synchronized (lock) { + // Clear my own cache + super.evictAll(); + + // Clear the caches of any metadata providers which support the + // interface + for (final MetadataProvider p : providers) { + if (p instanceof MetadataCache) { + ((MetadataCache) p).evictAll(); + } + } + } + } + + public MetadataItem evictAndGet(final String metadataIdentificationString) { + return getInternal(metadataIdentificationString, true, false); + } + + public MetadataItem get(final String metadataIdentificationString) { + return get(metadataIdentificationString, false); + } + + public MetadataItem get(final String metadataIdentificationString, + final boolean evictCache) { + return getInternal(metadataIdentificationString, evictCache, true); + } + + private MetadataItem getInternal(final String metadataIdentificationString, + final boolean evictCache, final boolean cacheRetrievalAllowed) { + Validate.isTrue( + MetadataIdentificationUtils + .isIdentifyingInstance(metadataIdentificationString), + "Metadata identification string '%s' does not identify a metadata instance", + metadataIdentificationString); + + synchronized (lock) { + validGets++; + + try { + metadataLogger.startEvent(); + + // Do some cache eviction if the caller requested it + if (evictCache) { + evict(metadataIdentificationString); + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Evicting " + + metadataIdentificationString); + } + cacheEvictions++; + } + + // We can use the cache even for a recursive get (unless of + // course the caller has prevented it) + if (cacheRetrievalAllowed) { + // Try the cache first + final MetadataItem result = getFromCache(metadataIdentificationString); + if (result != null) { + cacheHits++; + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Cache hit " + + metadataIdentificationString); + } + return result; + } + } + + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Cache miss " + + metadataIdentificationString); + } + cacheMisses++; + + // Determine if this MID was already requested earlier. We need + // to stop these infinite requests from occurring. + if (activeRequests.contains(metadataIdentificationString)) { + recursiveGets++; + if (!keysToRetry.contains(metadataIdentificationString)) { + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Blocked recursive request for " + + metadataIdentificationString); + } + keysToRetry.add(metadataIdentificationString); + } + return null; + } + + // Get the destination + final String mdClassId = MetadataIdentificationUtils + .getMetadataClassId(metadataIdentificationString); + final MetadataProvider p = providerMap.get(mdClassId); + Validate.notNull( + p, + "No metadata provider is currently registered to provide metadata for identifier '%s' (class '%s')", + metadataIdentificationString, mdClassId); + + // Infinite loop management + activeRequests.add(metadataIdentificationString); + + // Obtain the item + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Get " + metadataIdentificationString + + " from " + p.getClass().getName()); + } + MetadataItem result = null; + try { + metadataLogger.startTimer(p.getClass().getName()); + result = p.get(metadataIdentificationString); + } + finally { + metadataLogger.stopTimer(); + } + + // If the item isn't available, evict it from the cache (unless + // we did so at the start of the method already) + if (result == null && !evictCache) { + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Evicting unavailable item " + + metadataIdentificationString); + } + evict(metadataIdentificationString); + cacheEvictions++; + } + + // Put into the cache, provided it isn't null + if (result != null) { + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Caching " + + metadataIdentificationString); + } + super.put(result); + cachePuts++; + } + + activeRequests.remove(metadataIdentificationString); + + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Returning " + + metadataIdentificationString); + } + + return result; + } + catch (final Exception e) { + activeRequests.remove(metadataIdentificationString); + throw new IllegalStateException(e); + } + finally { + // We use another try..finally block as we want to ensure + // exceptions don't prevent our metadataLogger.stopEvent() + try { + // Have we processed all requests? If so, handle any retries + // we recorded + if (activeRequests.isEmpty()) { + final List thisRetry = new ArrayList(); + thisRetry.addAll(keysToRetry); + keysToRetry.clear(); + if (metadataLogger.getTraceLevel() > 0 + && thisRetry.size() > 0) { + metadataLogger.log(thisRetry.size() + + " keys to retry: " + thisRetry); + } + for (final String retryMid : thisRetry) { + // Important: we should not evict any prior version + // from the cache (an interim version is + // acceptable). + // We discard the result of the get; this is purely + // to facilitate updating metadata stored in memory + // and on-disk + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log("Retrying " + retryMid); + } + if (ObjectUtils.equals(retryMid, metadataIdentificationString)) { + // Avoid infinite recursion loop + continue; + } + getInternal(retryMid, false, false); + } + if (metadataLogger.getTraceLevel() > 0 + && thisRetry.size() > 0) { + metadataLogger.log("Retry group completed " + + metadataIdentificationString); + } + } + } + finally { + metadataLogger.stopEvent(); + } + } + } + } + + public void notify(final String upstreamDependency, + final String downstreamDependency) { + Validate.isTrue( + MetadataIdentificationUtils.isValid(upstreamDependency), + "Upstream dependency is an invalid metadata identification string ('%s')", + upstreamDependency); + Validate.isTrue( + MetadataIdentificationUtils.isValid(downstreamDependency), + "Downstream dependency is an invalid metadata identification string ('%s')", + downstreamDependency); + + synchronized (lock) { + // Get the destination + final String mdClassId = MetadataIdentificationUtils + .getMetadataClassId(downstreamDependency); + final MetadataProvider p = providerMap.get(mdClassId); + + if (p == null) { + // No known provider that can consume this notification, so just + // return as per the interface contract + return; + } + + if (p instanceof MetadataNotificationListener) { + // The provider can directly handle this notification, so we + // just need to delegate directly to it. + // We rely on the provider to evict items from the cache if + // applicable. + ((MetadataNotificationListener) p).notify(upstreamDependency, + downstreamDependency); + } + else { + // As per interface contract, we just ensure we evict the item + // and recreate it + // However, we only do this if the destination is an instance - + // if it's a class, "get" is not a meaningful operation. + if (MetadataIdentificationUtils + .isIdentifyingInstance(downstreamDependency)) { + get(downstreamDependency, true); + } + // As per interface contract, we now notify any listeners this + // downstream instance has probably now changed + metadataDependencyRegistry + .notifyDownstream(downstreamDependency); + } + } + } + + @Override + public void put(final MetadataItem metadataItem) { + super.put(metadataItem); + cachePuts++; + } + + @Override + public final String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("validGets", validGets); + builder.append("recursiveGets", recursiveGets); + builder.append("cachePuts", cachePuts); + builder.append("cacheHits", cacheHits); + builder.append("cacheMisses", cacheMisses); + builder.append("cacheEvictions", cacheEvictions); + builder.append("cacheCurrentSize", getCacheSize()); + builder.append("cacheMaximumSize", getMaxCapacity()); + return builder.toString().replaceFirst("@[0-9a-f]+", ":"); + } + + protected void unbindMetadataProvider(final MetadataProvider mp) { + synchronized (lock) { + final String mid = mp.getProvidesType(); + providers.remove(mp); + providerMap.remove(mid); + } + } +} \ No newline at end of file diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataCache.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataCache.java new file mode 100644 index 000000000..ef378e23b --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataCache.java @@ -0,0 +1,56 @@ +package org.springframework.roo.metadata; + +/** + * Indicates a cache is maintained by the implementation. + *

    + * A cache is a "best effort" cache and does not need to guarantee to include + * every item of metadata. Implementations should take care to ensure excessive + * memory consumption does not occur as a result of their operation. It is + * recommended that least recently used metadata instances are automatically + * removed should consumption exceed an implementation-defined threshold. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MetadataCache { + + /** + * Evicts the specified metadata instance from the cache. + *

    + * The identification string must return true if presented to + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)}. + *

    + * If the metadata instance does not presently exist in the cache, this is + * considered a non-fatal event and the method will simply return. + * + * @param metadataIdentificationString to evict (mandatory and must refer to + * a specific item instance) + */ + void evict(String metadataIdentificationString); + + /** + * Evicts every item from the cache. + *

    + * This method can be used during reload/refresh-style operations or where + * there is a requirement to guarantee cache consistency. + */ + void evictAll(); + + /** + * Eagerly inserts an item into the cache. ONLY SPRING ROO INFRASTRUCTURE + * SHOULD INVOKE THIS METHOD. Do not invoke this method from add-ons, as the + * caching semantics are likely to be modified in the future and you should + * not rely on this method remaining. + * + * @param metadataItem an instance-identifying metadata item to insert + * (required) + */ + void put(MetadataItem metadataItem); + + /** + * Modifies the metadata cache maximum capacity. + * + * @param maxCapacity the new maximum capacity + */ + void setMaxCapacity(int maxCapacity); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataDependencyRegistry.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataDependencyRegistry.java new file mode 100644 index 000000000..2b763c0cf --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataDependencyRegistry.java @@ -0,0 +1,183 @@ +package org.springframework.roo.metadata; + +import java.util.Set; + +/** + * Registers the dependencies between different metadata identification strings. + *

    + * Metadata is free to depend on other metadata, including both metadata classes + * and metadata instances being depended on or depending on other metadata + * classes and metadata instances. In other words, metadata dependency graphs + * are not limited to only metadata instances. + *

    + * For performance and memory efficiency reasons, only metadata identification + * strings are stored by the {@link MetadataDependencyRegistry}. In particular, + * {@link MetadataItem} instances are definitely not stored internally - which + * is especially applicable given they are immutable. + *

    + * From a logical perspective, an item of metadata can "depend on" some other + * piece of metadata. For example, an item of metadata representing the members + * appearing within a Java source file would "depend on" the metadata that + * represents the physical source file on disk. An item of metadata can also be + * "depended on" by other pieces of metadata. If the relationship just + * exemplified was declared, the metadata representing the physical source file + * on disk would be "depended on" by the Java member metadata. We use the terms + * "upstream" and "downstream" to refer to these dependencies. In our example we + * would say that Java member metadata is downstream of the physical source file + * on disk metadata. We would also say that the physical source file on disk is + * upstream of the Java member metadata. + *

    + * In terms of notifications, the {@link MetadataDependencyRegistry} maintains a + * reference to the {@link MetadataService}, and delivers notifications to the + * {@link MetadataService} via its extension of the + * {@link MetadataNotificationListener} interface. Note that a + * {@link MetadataService} may subsequently deliver notifications to + * {@link MetadataProvider}s using the semantics defined in the contract for + * {@link MetadataService}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MetadataDependencyRegistry { + + /** + * Registers an additional instance to receive + * {@link MetadataNotificationListener} events. Note that these events are + * guaranteed to be delivered after the {@link MetadataService} has received + * them. + *

    + * Attempting to register a {@link MetadataService} using this method will + * result in an exception. + * + * @param listener to also receive all notifications (required) + */ + void addNotificationListener(MetadataNotificationListener listener); + + /** + * Removes all upstream dependencies that were previously registered for the + * specified downstream dependency. This is useful if rebuilding the + * downstream dependency metadata, and just want to clear all old dependency + * information from the registry. + *

    + * The dependency must return true if presented to + * {@link MetadataIdentificationUtils#isValid(String)}. + *

    + * If the dependency was never registered in the first place, this method + * simply returns. + * + * @param downstreamDependency the downstream dependency (required) + */ + void deregisterDependencies(String downstreamDependency); + + /** + * Removes a dependency between two items of metadata. + *

    + * Both arguments must return true if presented to + * {@link MetadataIdentificationUtils#isValid(String)}. + *

    + * If the dependency was never registered in the first place, this method + * simply returns. + * + * @param upstreamDependency the upstream dependency (required) + * @param downstreamDependency the downstream dependency (required) + */ + void deregisterDependency(String upstreamDependency, + String downstreamDependency); + + /** + * Obtains the list of the immediate downstream dependencies of the + * indicated metadata item. + *

    + * The upstream dependency must return true if presented to + * {@link MetadataIdentificationUtils#isValid(String)}. However, the + * upstream dependency need not have been registered with this registry in + * advance (in which case the method will not return any downstream + * dependencies, as none are known). + * + * @param upstreamDependency to find the immediate downstream items for + * (required) + * @return an immutable set of dependencies (never null, but the set may be + * empty) + */ + Set getDownstream(String upstreamDependency); + + /** + * Obtains a list of the immediate upstream dependencies of the indicated + * metadata item. + *

    + * The downstream dependency must return true if presented to + * {@link MetadataIdentificationUtils#isValid(String)}. + * + * @param downstreamDependency to find the immediate upstream items for + * (required) + * @return an immutable set of dependencies (never null, but the set may be + * empty) + */ + Set getUpstream(String downstreamDependency); + + /** + * Indicates whether the indicated downstream dependency is legally + * permitted to depend on the indicated upstream dependency. Specifically, + * the {@link MetadataDependencyRegistry} is required to verify the upstream + * dependency (and its dependencies) do not already depend on the downstream + * dependency (or any other dependency that itself depends on the downstream + * dependency). Good metadata design should prevent metadata from ever + * invoking this method and receiving a false response, but we like to be + * robust in ensuring the {@link MetadataDependencyRegistry} never can + * represent a circular dependency graph. + *

    + * Both arguments must return true if presented to + * {@link MetadataIdentificationUtils#isValid(String)}. + * + * @param upstreamDependency the upstream dependency (required; eg metadata + * representing a disk file) + * @param downstreamDependency the downstream dependency (required; eg + * metadata representing a Java type) + * @return true if the dependency relationship is legal + */ + boolean isValidDependency(String upstreamDependency, + String downstreamDependency); + + /** + * Causes the immediate downstream dependencies of the indicated metadata + * item to be notified the upstream metadata item is publishing a + * notification. + *

    + * The upstream dependency must return true if presented to + * {@link MetadataIdentificationUtils#isValid(String)}. However, the + * upstream dependency need not have been registered with this registry in + * advance (in which case the method will not notify any downstream + * dependencies, as none are known). + *

    + * Notifications are delivered to the {@link MetadataService} initially, + * followed by all {@link MetadataNotificationListener}s registered against + * the instance. + * + * @param upstreamDependency that is generating the notification (required). + */ + void notifyDownstream(String upstreamDependency); + + /** + * Registers a dependency between two items of metadata. + *

    + * The two items of metadata will be presented to + * {@link #isValidDependency(String, String)}. If this method returns false, + * an exception will be generated. + * + * @param upstreamDependency the upstream dependency (required; eg metadata + * representing a disk file) + * @param downstreamDependency the downstream dependency (required; eg + * metadata representing a Java type) + */ + void registerDependency(String upstreamDependency, + String downstreamDependency); + + /** + * De-register an additional instance to receive + * {@link MetadataNotificationListener} events. If the listener was never + * registered in the first place, the method simply returns. + * + * @param listener to no longer receive notifications (required) + */ + void removeNotificationListener(MetadataNotificationListener listener); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataIdentificationUtils.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataIdentificationUtils.java new file mode 100644 index 000000000..91251acf3 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataIdentificationUtils.java @@ -0,0 +1,215 @@ +package org.springframework.roo.metadata; + +import org.apache.commons.lang3.StringUtils; + +/** + * Utility methods relating to metadata identification strings. + *

    + * We use identification strings for metadata in order to reduce memory + * consumption and garbage collection overhead. Strings also have the advantage + * of being immutable and easy to display as text. + *

    + * Metadata identification strings can identify either: + *

      + *
    • a class of {@link MetadataItem} (in which case + * {@link #isIdentifyingClass(String)} returns true), or
    • + *
    • a specific instance of such a class (in which case + * {@link #isIdentifyingInstance(String)} returns true)
    • + *
    + * + * @author Ben Alex + * @since 1.0 + */ +public final class MetadataIdentificationUtils { + + /* + * This delimiter was chosen because it never appears in a Java type name, + * is uncommon to use in file system paths, and looks OK to a human in a + * metadata id string. The first instance of this character in a given MID + * separates the metadata class name from the name of the project type to + * which the metadata applies. + */ + static final String INSTANCE_DELIMITER = "#"; + + // All MIDs start with these characters + private static final char[] MID_PREFIX_CHARACTERS = { 'M', 'I', 'D', ':' }; + + static final String MID_PREFIX = String.valueOf(MID_PREFIX_CHARACTERS); + + private static final int MID_PREFIX_LENGTH = MID_PREFIX_CHARACTERS.length; + + /** + * Returns the class-level ID for the given type of metadata + * + * @param metadataClass the metadata class for which to create an ID (can be + * null) + * @return a non-blank metadata ID, or null if a + * null class was given + * @since 1.2.0 + */ + public static String create(final Class metadataClass) { + if (metadataClass == null) { + return null; + } + return create(metadataClass.getName()); + } + + /** + * Creates a class-specific metadata id for the given fully qualified class + * name. + *

    + * You can acquire a fully qualified class name using + * {@link Class#getName()}, although it's more typesafe to call + * {@link #create(Class)}. + * + * @param fullyQualifiedClassName to create (can be null or empty) + * @return the metadata identification string (may be null if the input was + * invalid) + */ + public static String create(final String fullyQualifiedClassName) { + if (StringUtils.isBlank(fullyQualifiedClassName) + || fullyQualifiedClassName.contains(INSTANCE_DELIMITER)) { + return null; + } + return MID_PREFIX + fullyQualifiedClassName; + } + + /** + * Creates an instance-specific metadata identification string for the + * presented class/key pair. + * + * @param fullyQualifiedMetadataClass + * @param instanceIdentificationKey + * @return null if either of the given strings is blank or the + * metadata class name is not well-formed + */ + public static String create(final String fullyQualifiedMetadataClass, + final String instanceIdentificationKey) { + if (StringUtils.isBlank(instanceIdentificationKey) + || StringUtils.isBlank(fullyQualifiedMetadataClass) + || fullyQualifiedMetadataClass.contains(INSTANCE_DELIMITER)) { + return null; + } + final StringBuilder mid = new StringBuilder(); + mid.append(MID_PREFIX); + mid.append(fullyQualifiedMetadataClass); + mid.append(INSTANCE_DELIMITER); + mid.append(instanceIdentificationKey); + return mid.toString(); + } + + /** + * Indicates the class of metadata a particular string represents. The class + * will be returned even if the metadata identification string represents a + * specific instance. + * + * @param metadataId to evaluate (can be null or empty) + * @return the class only, or null if the identification string is invalid + * in some way + */ + public static String getMetadataClass(final String metadataId) { + if (!isValid(metadataId) + || metadataId.equals(MID_PREFIX + INSTANCE_DELIMITER)) { + return null; + } + final int delimiterIndex = metadataId.indexOf(INSTANCE_DELIMITER); + if (delimiterIndex == -1) { + // No specific metadata instance was identified, so return + // everything except "MID:" + return metadataId.substring(MID_PREFIX_LENGTH); + } + // A particular instance was identified, so we only return the instance + // name part + return metadataId.substring(MID_PREFIX_LENGTH, delimiterIndex); + } + + /** + * Returns the ID of the given metadata's class. + * + * @param metadataId the metadata ID for which to return the class ID (can + * be blank) + * @return null if a blank ID is given, otherwise a valid + * class-level ID + * @since 1.2.0 + */ + public static String getMetadataClassId(final String metadataId) { + return create(getMetadataClass(metadataId)); + } + + /** + * Returns the instance key from the given metadata instance ID. + * + * @param metadataId the MID to evaluate (can be blank) + * @return the instance ID only, or null if the identification + * string is invalid in some way + */ + public static String getMetadataInstance(final String metadataId) { + if (isIdentifyingInstance(metadataId)) { + return metadataId + .substring(metadataId.indexOf(INSTANCE_DELIMITER) + 1); + } + return null; + } + + /** + * Indicates whether the argument appears to represent a particular metadata + * identification class. This method returns false if a particular instance + * is identified. + * + * @param metadataIdentificationString to evaluate (can be null or empty) + * @return true if the string is identifying a class of {@link MetadataItem} + */ + public static boolean isIdentifyingClass( + final String metadataIdentificationString) { + return isValid(metadataIdentificationString) + && !metadataIdentificationString.contains(INSTANCE_DELIMITER); + } + + /** + * Indicates whether the argument appears to represent a specific metadata + * instance. + * + * @param metadataIdentificationString to evaluate (can be null or empty) + * @return true if the string is identifying a specific instance of a + * {@link MetadataItem} + */ + public static boolean isIdentifyingInstance( + final String metadataIdentificationString) { + return isValid(metadataIdentificationString) + && metadataIdentificationString.contains(INSTANCE_DELIMITER) + && !metadataIdentificationString.endsWith(INSTANCE_DELIMITER); + } + + /** + * Indicates whether the argument is a well-formed metadata identification + * string. This does not guarantee that it is valid, i.e. that the + * identified metadata actually exists or could ever exist. + * + * @param metadataIdentificationString to evaluate (can be null or empty) + * @return true if the string appears to be a valid metadata + * identification string + */ + public static boolean isValid(final String metadataIdentificationString) { + /* + * According to the first comment on ROO-1932, the algorithm below is an + * optimisation over simply checking for null and calling + * String#startsWith(). + */ + if (metadataIdentificationString == null + || metadataIdentificationString.length() <= MID_PREFIX_LENGTH) { + return false; + } + for (int i = 0; i < MID_PREFIX_LENGTH; i++) { + if (metadataIdentificationString.charAt(i) != MID_PREFIX_CHARACTERS[i]) { + return false; + } + } + return true; + } + + /** + * Constructor is private to prevent instantiation + */ + private MetadataIdentificationUtils() { + } +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataItem.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataItem.java new file mode 100644 index 000000000..e87dff0fb --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataItem.java @@ -0,0 +1,37 @@ +package org.springframework.roo.metadata; + +/** + * A piece of information about the user's project, typically obtained via the + * {@link MetadataService}. + *

    + * Implementations should be immutable. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MetadataItem { + + /** + * Returns the unique ID of this piece of metadata within the user's + * project. + * + * @return a non-blank ID that satisfies + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)} + * ) + */ + String getId(); + + /** + * Indicates whether this piece of metadata was successfully produced. + *

    + * TODO Ben has suggested deprecating this method and having + * {@link MetadataProvider}s simply return null if they can't + * produce the metadata for some reason (some already do this). Callers + * already have to check for null anyway, so requiring them to call this + * method as well imposes an extra step that's easily missed. + * + * @return false if for example some metadata on which it + * depends was unavailable. + */ + boolean isValid(); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataLogger.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataLogger.java new file mode 100644 index 000000000..aa77582f1 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataLogger.java @@ -0,0 +1,99 @@ +package org.springframework.roo.metadata; + +import java.util.SortedSet; + +/** + * Simplifies the creation of nested event logs and metadata timing statistics. + *

    + * This interface is intended for concurrent use between + * {@link MetadataDependencyRegistry} and {@link MetadataService}. + *

    + * Metadata timing is the simplest operation. Before a metadata provider is + * invoked, the {@link MetadataLogger} identifies the metadata provider as the + * "responsible class" and commences timing via {@link #startTimer(String)}. The + * metadata provider is free to then perform its work, which may include calling + * for more metadata. If more metadata is requested, new + * {@link #startTimer(String)} invocations will take place. As each metadata + * provider completes its work, the {@link #stopTimer()} method is called. This + * aggregates timing information and makes it available via + * {@link #getTimings()}. + *

    + * Metadata logging is similar. Before logging can take place, + * {@link #startEvent()} should be invoked. The definition of an "event" varies, + * but generally a series of related logging calls should be the same "event". + * Once an event has been started, calls to {@link #log(String)} will log + * messages and associate them with the active event. After an event concludes, + * the {@link #stopEvent()} method must be called. Nesting is automatically + * handled by an implementation, with successive start event calls being + * indented in the logging output, and the logging output correctly reporting + * the correlating event ID. Callers should not invoke {@link #log(String)} + * unless a suitable {@link #getTraceLevel()} is desired by the user. + *

    + * It is important to always call the "stop" method once for every "start" + * method that was called. Failure to observe this requirement will lead to + * exceptions as the stack is managed and an invalid state detected. + *

    + * Implementations are free to store metadata logging output in any file they + * wish. This file should be created on the first call to {@link #log(String)}. + * + * @author Ben Alex + * @since 1.1.2 + */ +public interface MetadataLogger { + + /** + * @return a snapshot of timing statistics that have been collated so far + * (never null, but may be empty) + */ + SortedSet getTimings(); + + /** + * @return the currently active trace level (0 = none, 1 = major events, 2 = + * all events) + */ + int getTraceLevel(); + + /** + * Logs a message against the given event identifier. + * + * @param message to log (required) + */ + void log(String message); + + /** + * Enable low-level tracing of event delivery information. Defaults to level + * 0 (none). + * + * @param trace the level (0 = none, 1 = major events, 2 = all events) + */ + void setTraceLevel(int trace); + + /** + * Increments the current stack level. The current stack level determines + * the indentation of logged messages. It is required that for every + * increment, a corresponding {@link #decrementLevel()} is invoked. + */ + void startEvent(); + + /** + * Starts the timer counting against the responsible class. The timer must + * eventually be {@link #stopTimer()}, but timings will cease being counted + * against the responsible class when a new {@link #startTimer(String)} is + * invoked. + * + * @param responsibleClass the class responsible for this timing (required) + */ + void startTimer(String responsibleClass); + + /** + * Decrements the current stack level. + */ + void stopEvent(); + + /** + * Stops the most recently started timer. This is mandatory and must be in + * the reverse order timers were started. When a timer stops is also when we + * update its timings. + */ + void stopTimer(); +} \ No newline at end of file diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataNotificationListener.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataNotificationListener.java new file mode 100644 index 000000000..790bb9abd --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataNotificationListener.java @@ -0,0 +1,34 @@ +package org.springframework.roo.metadata; + +/** + * Indicates an implementation is able to process metadata notifications. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MetadataNotificationListener { + + /** + * Invoked to notify an implementation that a particular source metadata + * identification has requested to notify a particular destination metadata + * identification of an event. + *

    + * Both the source and destination metadata identifications can be either a + * {@link MetadataIdentificationUtils#isIdentifyingClass(String)} or + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)}. + * However, both the source and metadata identifications must return true + * when presented to {@link MetadataIdentificationUtils#isValid(String)}. + *

    + * Where possible exceptions should not be thrown when processing metadata, + * except for genuinely fatal operations. Simple failures to obtain metadata + * information can be safely ignored and indicated via + * {@link MetadataItem#isValid()}, with an expectation that metadata + * depending on such metadata will call this method. + * + * @param upstreamDependency the upstream source of the event (mandatory and + * must be valid) + * @param downstreamDependency the downstream destination of the event (may + * be null if no particular downstream is targeted) + */ + void notify(String upstreamDependency, String downstreamDependency); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataProvider.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataProvider.java new file mode 100644 index 000000000..72526dd49 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataProvider.java @@ -0,0 +1,42 @@ +package org.springframework.roo.metadata; + +/** + * Indicates a service that guarantees to authoritatively provide + * {@link MetadataItem}s for a particular class of metadata identification + * strings. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MetadataProvider { + + /** + * Creates the requested {@link MetadataItem} if possible, returning null if + * the item cannot be created or found. + *

    + * This method will throw an exception if the caller has provided an invalid + * input argument. This would be the case if the input argument is null, + * empty, does not return true from + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)}, or the + * requested metadata identifier is not of the same class as indicated by + * {@link #getProvidesType()}). + * + * @param metadataIdentificationString to acquire (required and must be + * supported by this provider) + * @return the metadata, or null if the identification was valid but the + * metadata is unavailable + */ + MetadataItem get(String metadataIdentificationString); + + /** + * Returns the class-level id of the type of metadata being provided. + *

    + * The value returned by this method must remain identical for the entire + * lifecycle of a particular {@link MetadataProvider} instance. It cannot + * change once it has been returned. + * + * @return a value that satisfies + * {@link MetadataIdentificationUtils#isIdentifyingClass(String)}. + */ + String getProvidesType(); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataService.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataService.java new file mode 100644 index 000000000..8e01149e0 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataService.java @@ -0,0 +1,99 @@ +package org.springframework.roo.metadata; + +/** + * Indicates a service which is aware of all {@link MetadataProvider}s in the + * system and can provide access to their respective capabilities. + *

    + * A {@link MetadataService} provides a convenient way for any object that + * requires metadata information to consult a single source that can provide + * that metadata. An implementation can act as the single source because it can + * identify which {@link MetadataProvider} is applicable to a given metadata + * identification string and delegate to that provider. + *

    + * An instance of {@link MetadataService} becomes aware of candidate + * {@link MetadataProvider} instances by way of "registration". An + * implementation is required to use OSGi declarative services to detect the + * presence of {@link MetadataProvider} instances. + *

    + * As indicated by the {@link MetadataService} interface extending + * {@link MetadataCache}, all implementations must provide caching support. + *

    + * Also as indicated by {@link MetadataService} extending + * {@link MetadataNotificationListener}, an implementation is required to + * receive notification events and pass these notification events through to the + * relevant {@link MetadataProvider}. If there is no registered + * {@link MetadataProvider}, the notification method should simply return + * without error. If passing through to a {@link MetadataProvider} that + * implements {@link MetadataNotificationListener}, any cache modification + * responsibilities are that of the delegate. If the {@link MetadataProvider} + * does not implement {@link MetadataNotificationListener}, the fallback + * behaviour of the {@link MetadataService} implementation is to invoke + * {@link #get(String, boolean)}, passing a true value to the evict from cache + * boolean parameter. It must also call + * {@link MetadataDependencyRegistry#notifyDownstream(String)} in case any other + * metadata was monitoring the metadata. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MetadataService extends MetadataNotificationListener, + MetadataCache { + + /** + * Returns the {@link MetadataItem} with the given ID, generating it from + * scratch and caching the result. For performance reasons it's preferable + * to call {@link #get(String)} if possible, to take advantage of the cache. + * + * @param metadataIdentificationString the ID of the {@link MetadataItem} to + * acquire; must identify a metadata instance, i.e. return + * true when passed to + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)} + * @return the metadata, or null if the ID was valid but the + * metadata is not currently available + * @throws an exception if the given type of metadata is not supported + */ + MetadataItem evictAndGet(String metadataIdentificationString); + + /** + * Returns the {@link MetadataItem} with the given ID, from the cache if + * possible. + * + * @param metadataIdentificationString the ID of the {@link MetadataItem} to + * acquire; must identify a metadata instance, i.e. return + * true when passed to + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)} + * @return the metadata, or null if the ID was valid but the + * metadata is not currently available + * @throws an exception if the given type of metadata is not supported + */ + MetadataItem get(String metadataIdentificationString); + + /** + * Creates the requested {@link MetadataItem} if possible, returning null if + * the item cannot be created or found. Implementations will delegate + * creation events to the respective registered {@link MetadataProvider}, + * and may at their option retrieve a cached instance. + *

    + * This method will throw an exception if the caller has provided an invalid + * input argument. This would be the case if the input argument is null, + * empty, does not return true from + * {@link MetadataIdentificationUtils#isIdentifyingInstance(String)}, or the + * requested metadata identifier is not of the same class as indicated by + * getProvidesType()). + *

    + * An exception will also be thrown if the identification string is related + * to a provider that is not registered. + * + * @param metadataIdentificationString to acquire (required and must be + * supported by this provider) + * @param evictCache forces eviction of the instance from any caches before + * attempting retrieval + * @return the metadata, or null if the identification was valid and a + * provider was found, but the metadata is unavailable + * @deprecated use {@link #evictAndGet(String)} instead of calling + * {@link #get(String, true)} or {@link #get(String)} instead of + * calling {@link #get(String, false)} + */ + @Deprecated + MetadataItem get(String metadataIdentificationString, boolean evictCache); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/MetadataTimingStatistic.java b/metadata/src/main/java/org/springframework/roo/metadata/MetadataTimingStatistic.java new file mode 100644 index 000000000..6aa2e9598 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/MetadataTimingStatistic.java @@ -0,0 +1,29 @@ +package org.springframework.roo.metadata; + +/** + * Represents an immutable representation of a single timing statistic from + * {@link MetadataDependencyRegistry}. + * + * @author Ben Alex + */ +public interface MetadataTimingStatistic extends + Comparable { + + /** + * @return the number of invocations associated with this {@link #getName()} + * . + */ + long getInvocations(); + + /** + * @return an identifier to differentiate this timing statistic from another + * (never null or empty) + */ + String getName(); + + /** + * @return the number of nanoseconds associated with this {@link #getName()} + * . + */ + long getTime(); +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/internal/AbstractMetadataCache.java b/metadata/src/main/java/org/springframework/roo/metadata/internal/AbstractMetadataCache.java new file mode 100644 index 000000000..11bd5f843 --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/internal/AbstractMetadataCache.java @@ -0,0 +1,85 @@ +package org.springframework.roo.metadata.internal; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.metadata.MetadataCache; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; + +/** + * Basic {@link MetadataCache} that stores elements on a least recently used + * (LRU) basis. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractMetadataCache implements MetadataCache { + + private static final float hashTableLoadFactor = 0.75f; + + private LinkedHashMap map; + private int maxCapacity = 100000; + + protected AbstractMetadataCache() { + init(); + } + + public void evict(final String metadataIdentificationString) { + Validate.isTrue(MetadataIdentificationUtils + .isIdentifyingInstance(metadataIdentificationString), + "Only metadata instances can be cached (not '%s')", + metadataIdentificationString); + map.remove(metadataIdentificationString); + } + + public void evictAll() { + init(); + } + + protected int getCacheSize() { + return map.size(); + } + + protected MetadataItem getFromCache( + final String metadataIdentificationString) { + Validate.isTrue(MetadataIdentificationUtils + .isIdentifyingInstance(metadataIdentificationString), + "Only metadata instances can be cached (not '%s')", + metadataIdentificationString); + return map.get(metadataIdentificationString); + } + + public int getMaxCapacity() { + return maxCapacity; + } + + private void init() { + final int hashTableCapacity = (int) Math.ceil(maxCapacity + / hashTableLoadFactor) + 1; + map = new LinkedHashMap(hashTableCapacity, + hashTableLoadFactor, true) { + private static final long serialVersionUID = 1; + + @Override + protected boolean removeEldestEntry( + final Map.Entry eldest) { + return size() > maxCapacity; + } + }; + } + + public void put(final MetadataItem metadataItem) { + Validate.notNull(metadataItem, "A metadata item is required"); + map.put(metadataItem.getId(), metadataItem); + } + + public void setMaxCapacity(int maxCapacity) { + if (maxCapacity < 100) { + maxCapacity = 100; + } + this.maxCapacity = maxCapacity; + init(); + } +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/internal/DefaultMetadataDependencyRegistry.java b/metadata/src/main/java/org/springframework/roo/metadata/internal/DefaultMetadataDependencyRegistry.java new file mode 100644 index 000000000..507cdeb1e --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/internal/DefaultMetadataDependencyRegistry.java @@ -0,0 +1,310 @@ +package org.springframework.roo.metadata.internal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataLogger; +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.metadata.MetadataService; + +/** + * Default implementation of {@link MetadataDependencyRegistry}. + *

    + * This implementation is not thread safe. It should only be accessed by a + * single thread at a time. This is enforced by the process manager semantics, + * so we avoid the cost of re-synchronization here. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class DefaultMetadataDependencyRegistry implements + MetadataDependencyRegistry { + /** key: downstream dependency; value: list */ + private final Map> downstreamKeyed = new HashMap>(); + private final Set listeners = new HashSet(); + @Reference private MetadataLogger metadataLogger; + private MetadataService metadataService; + /** key: upstream dependency; value: list */ + private final Map> upstreamKeyed = new HashMap>(); + + public void addNotificationListener( + final MetadataNotificationListener listener) { + Validate.notNull(listener, "Metadata notification listener required"); + + if (listener instanceof MetadataService) { + Validate.isTrue(metadataService == null, + "Cannot register more than one MetadataListener"); + metadataService = (MetadataService) listener; + return; + } + + listeners.add(listener); + } + + private void buildSetOfAllUpstreamDependencies(final Set results, + final String downstreamDependency) { + final Set upstreams = downstreamKeyed.get(downstreamDependency); + if (upstreams == null) { + return; + } + + for (final String upstream : upstreams) { + results.add(upstream); + buildSetOfAllUpstreamDependencies(results, upstream); + } + } + + public void deregisterDependencies(final String downstreamDependency) { + Validate.isTrue( + MetadataIdentificationUtils.isValid(downstreamDependency), + "Downstream dependency is an invalid metadata identification string ('%s')", + downstreamDependency); + + // Acquire the keys to delete + final Set upstream = downstreamKeyed.get(downstreamDependency); + if (upstream == null) { + return; + } + + final Set upstreamToDelete = new HashSet(upstream); + + // Delete them normally + for (final String deleteUpstream : upstreamToDelete) { + deregisterDependency(deleteUpstream, downstreamDependency); + } + } + + public void deregisterDependency(final String upstreamDependency, + final String downstreamDependency) { + Validate.isTrue( + MetadataIdentificationUtils.isValid(upstreamDependency), + "Upstream dependency is an invalid metadata identification string ('%s')", + upstreamDependency); + Validate.isTrue( + MetadataIdentificationUtils.isValid(downstreamDependency), + "Downstream dependency is an invalid metadata identification string ('%s')", + downstreamDependency); + + // Maintain the upstream-keyed map, if it even exists + final Set downstream = upstreamKeyed.get(upstreamDependency); + if (downstream != null) { + downstream.remove(downstreamDependency); + } + + // Maintain the downstream-keyed map, if it even exists + final Set upstream = downstreamKeyed.get(downstreamDependency); + if (upstream != null) { + upstream.remove(upstreamDependency); + } + } + + public Set getDownstream(final String upstreamDependency) { + Validate.isTrue( + MetadataIdentificationUtils.isValid(upstreamDependency), + "Upstream dependency is an invalid metadata identification string ('%s')", + upstreamDependency); + + final Set downstream = upstreamKeyed.get(upstreamDependency); + if (downstream == null) { + return new HashSet(); + } + + return Collections.unmodifiableSet(new CopyOnWriteArraySet( + downstream)); + } + + public Set getUpstream(final String downstreamDependency) { + Validate.isTrue( + MetadataIdentificationUtils.isValid(downstreamDependency), + "Downstream dependency is an invalid metadata identification string ('%s')", + downstreamDependency); + + final Set upstream = downstreamKeyed.get(downstreamDependency); + if (upstream == null) { + return new HashSet(); + } + + return Collections.unmodifiableSet(upstream); + } + + public boolean isValidDependency(final String upstreamDependency, + final String downstreamDependency) { + Validate.isTrue( + MetadataIdentificationUtils.isValid(upstreamDependency), + "Upstream dependency is an invalid metadata identification string ('%s')", + upstreamDependency); + Validate.isTrue( + MetadataIdentificationUtils.isValid(downstreamDependency), + "Downstream dependency is an invalid metadata identification string ('%s')", + downstreamDependency); + Validate.isTrue( + !upstreamDependency.equals(downstreamDependency), + "Upstream dependency cannot be the same as the downstream dependency ('%s')", + downstreamDependency); + + // The simplest possible outcome is the relationship already exists, so + // quickly return in that case + Set downstream = upstreamKeyed.get(upstreamDependency); + if (downstream != null && downstream.contains(downstreamDependency)) { + return true; + } + // Don't need the variable anymore, as we don't care about the other + // downstream dependencies + downstream = null; + + // Need to walk the upstream dependency's parent dependency graph, + // verifying no presence of the proposed downstream dependency + + // Need to build a set representing every eventual upstream dependency + // of the indicated upstream dependency + final Set allUpstreams = new HashSet(); + buildSetOfAllUpstreamDependencies(allUpstreams, upstreamDependency); + + // The dependency is valid if none of the upstreams depend on the + // proposed downstream + return !allUpstreams.contains(downstreamDependency); + } + + public void notifyDownstream(final String upstreamDependency) { + try { + metadataLogger.startEvent(); + + if (metadataService != null) { + // First dispatch the fine-grained, instance-specific + // dependencies. + Set notifiedDownstreams = new HashSet(); + for (final String downstream : getDownstream(upstreamDependency)) { + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log(upstreamDependency + " -> " + + downstream); + } + // No need to ensure upstreamDependency is different from + // downstream, as that's taken care of in the + // isValidDependency() method + try { + final String responsibleClass = MetadataIdentificationUtils + .getMetadataClass(downstream); + metadataLogger.startTimer(responsibleClass); + metadataService.notify(upstreamDependency, downstream); + } + finally { + metadataLogger.stopTimer(); + } + notifiedDownstreams.add(downstream); + } + + // Next dispatch the coarse-grained, class-specific + // dependencies. + // We only do it if the upstream is not class specific, as + // otherwise we'd have handled class-specific dispatch in + // previous loop + if (!MetadataIdentificationUtils + .isIdentifyingClass(upstreamDependency)) { + final String asClass = MetadataIdentificationUtils + .getMetadataClassId(upstreamDependency); + for (final String downstream : getDownstream(asClass)) { + // We don't notify a downstream if it had a direct + // instance-specific dependency and was already notified + // in previous loop + // We also don't notify if upstream is the same as + // downstream, as it doesn't make sense to notify + // yourself of an event + // (such a condition is only possible if an instance + // registered to receive class-specific notifications + // and that instance + // caused an event to fire) + if (!notifiedDownstreams.contains(downstream) + && !upstreamDependency.equals(downstream)) { + if (metadataLogger.getTraceLevel() > 0) { + metadataLogger.log(upstreamDependency + " -> " + + downstream + " [via class]"); + } + try { + final String responsibleClass = MetadataIdentificationUtils + .getMetadataClass(downstream); + metadataLogger.startTimer(responsibleClass); + metadataService.notify(upstreamDependency, + downstream); + } + finally { + metadataLogger.stopTimer(); + } + } + } + } + + notifiedDownstreams = null; + } + + // Finally dispatch the general-purpose additional listeners + for (final MetadataNotificationListener listener : listeners) { + if (metadataLogger.getTraceLevel() > 1) { + metadataLogger.log(upstreamDependency + " -> " + + upstreamDependency + " [" + + listener.getClass().getSimpleName() + "]"); + } + try { + final String responsibleClass = listener.getClass() + .getName(); + metadataLogger.startTimer(responsibleClass); + listener.notify(upstreamDependency, null); + } + finally { + metadataLogger.stopTimer(); + } + } + } + finally { + metadataLogger.stopEvent(); + } + } + + public void registerDependency(final String upstreamDependency, + final String downstreamDependency) { + Validate.isTrue( + isValidDependency(upstreamDependency, downstreamDependency), + "Invalid dependency between upstream '%s' and downstream '%s'", + upstreamDependency, downstreamDependency); + + // Maintain the upstream-keyed map + Set downstream = upstreamKeyed.get(upstreamDependency); + if (downstream == null) { + downstream = new HashSet(); + upstreamKeyed.put(upstreamDependency, downstream); + } + downstream.add(downstreamDependency); + + // Maintain the downstream-keyed map + Set upstream = downstreamKeyed.get(downstreamDependency); + if (upstream == null) { + upstream = new HashSet(); + downstreamKeyed.put(downstreamDependency, upstream); + } + upstream.add(upstreamDependency); + } + + public void removeNotificationListener( + final MetadataNotificationListener listener) { + Validate.notNull(listener, "Metadata notification listener required"); + + if (listener instanceof MetadataService + && listener.equals(metadataService)) { + metadataService = null; + return; + } + + listeners.remove(listener); + } +} diff --git a/metadata/src/main/java/org/springframework/roo/metadata/internal/StandardMetadataTimingStatistic.java b/metadata/src/main/java/org/springframework/roo/metadata/internal/StandardMetadataTimingStatistic.java new file mode 100644 index 000000000..69c0a40fe --- /dev/null +++ b/metadata/src/main/java/org/springframework/roo/metadata/internal/StandardMetadataTimingStatistic.java @@ -0,0 +1,96 @@ +package org.springframework.roo.metadata.internal; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.metadata.MetadataTimingStatistic; + +/** + * Standard implementation of {@link MetadataTimingStatistic}. + * + * @author Ben Alex + */ +public class StandardMetadataTimingStatistic implements MetadataTimingStatistic { + + // Chosen to match the maximum + private static final int MAXIMUM_EXPECTED_INVOCATIONS = 5; + + private static final String INVOCATION_COUNT_FORMAT = "%" + + MAXIMUM_EXPECTED_INVOCATIONS + "d"; + + static final long NANOSECONDS_IN_MILLISECOND = 1000000L; + private static final String TIME_FORMAT = "%" + + (String.valueOf(NANOSECONDS_IN_MILLISECOND).length() - 1) + "d"; + + private final long invocations; + private final String name; + private final long nanoseconds; + + /** + * Constructor + * + * @param name (required) + * @param nanoseconds the elasped time in nanoseconds (zero or more) + * @param invocations (zero or more) + */ + public StandardMetadataTimingStatistic(final String name, + final long nanoseconds, final long invocations) { + Validate.notBlank(name, "Name required"); + Validate.isTrue(invocations >= 0, "Invocations must be zero or more"); + Validate.isTrue(nanoseconds >= 0, "Nanoseconds must be zero or more"); + this.invocations = invocations; + this.name = name; + this.nanoseconds = nanoseconds; + } + + public int compareTo(final MetadataTimingStatistic o) { + int result = Long.valueOf(nanoseconds).compareTo(o.getTime()); + if (result == 0) { + result = Long.valueOf(invocations).compareTo(o.getInvocations()); + } + if (result == 0) { + result = name.compareTo(o.getName()); + } + return result; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof MetadataTimingStatistic + && compareTo((MetadataTimingStatistic) obj) == 0; + } + + public long getInvocations() { + return invocations; + } + + public String getName() { + return name; + } + + public long getTime() { + return nanoseconds; + } + + @Override + public int hashCode() { + return Long.valueOf(nanoseconds).hashCode() + * Long.valueOf(invocations).hashCode() * name.hashCode(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + if (nanoseconds < NANOSECONDS_IN_MILLISECOND) { + // Display as nanoseconds + sb.append(String.format(TIME_FORMAT, nanoseconds)).append(" ns; "); + } + else { + // Display as milliseconds + sb.append(String.format(TIME_FORMAT, nanoseconds / 1000000)) + .append(" ms; "); + } + sb.append(String.format(INVOCATION_COUNT_FORMAT, invocations)).append( + " call(s): "); + sb.append(name); + return sb.toString(); + } +} diff --git a/metadata/src/test/java/org/springframework/roo/metadata/DefaultMetadataServiceTest.java b/metadata/src/test/java/org/springframework/roo/metadata/DefaultMetadataServiceTest.java new file mode 100644 index 000000000..c2e4f5639 --- /dev/null +++ b/metadata/src/test/java/org/springframework/roo/metadata/DefaultMetadataServiceTest.java @@ -0,0 +1,23 @@ +package org.springframework.roo.metadata; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class DefaultMetadataServiceTest { + + private static final String TO_STRING_FOR_NEW_INSTANCE = "org.springframework.roo.metadata.DefaultMetadataService:" + + "[validGets=0," + + "recursiveGets=0," + + "cachePuts=0," + + "cacheHits=0," + + "cacheMisses=0," + + "cacheEvictions=0," + + "cacheCurrentSize=0," + "cacheMaximumSize=100000]"; + + @Test + public void testToStringOfNewInstance() { + assertEquals(TO_STRING_FOR_NEW_INSTANCE, + new DefaultMetadataService().toString()); + } +} diff --git a/metadata/src/test/java/org/springframework/roo/metadata/MetadataIdentificationUtilsTest.java b/metadata/src/test/java/org/springframework/roo/metadata/MetadataIdentificationUtilsTest.java new file mode 100644 index 000000000..c8577af2e --- /dev/null +++ b/metadata/src/test/java/org/springframework/roo/metadata/MetadataIdentificationUtilsTest.java @@ -0,0 +1,295 @@ +package org.springframework.roo.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.roo.metadata.MetadataIdentificationUtils.INSTANCE_DELIMITER; +import static org.springframework.roo.metadata.MetadataIdentificationUtils.MID_PREFIX; +import junit.framework.Assert; + +import org.junit.Test; + +/** + * Unit test of {@link MetadataIdentificationUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MetadataIdentificationUtilsTest { + + private static final String INSTANCE_CLASS = Integer.class.getName(); // normally + private static final String METADATA_CLASS = MetadataItem.class.getName(); + private static final String CLASS_MID = MID_PREFIX + METADATA_CLASS; + private static final String INSTANCE_MID = MID_PREFIX + METADATA_CLASS + + INSTANCE_DELIMITER + INSTANCE_CLASS; + + @Test + public void testBlankMidIsNotValid() { + assertFalse(MetadataIdentificationUtils.isValid("\t\n\r")); + } + + @Test + public void testClassIdFromBadlyFormedMetadataClassName() { + assertNull(MetadataIdentificationUtils.create("foo#bar")); + } + + @Test + public void testClassIdFromEmptyMetadataClassName() { + assertNull(MetadataIdentificationUtils.create("")); + } + + @Test + public void testClassIdFromNonNullMetadataClass() { + assertEquals(CLASS_MID, + MetadataIdentificationUtils.create(MetadataItem.class)); + } + + @Test + public void testClassIdFromNullMetadataClass() { + assertNull(MetadataIdentificationUtils.create((Class) null)); + } + + @Test + public void testClassIdFromNullMetadataClassName() { + assertNull(MetadataIdentificationUtils.create((String) null)); + } + + @Test + public void testClassIdFromWellFormedMetadataClassName() { + // Normally this would be a class that implements MetadataItem + assertEquals(MID_PREFIX + METADATA_CLASS, + MetadataIdentificationUtils.create(METADATA_CLASS)); + } + + @Test + public void testClassMidIsClassMid() { + assertTrue(MetadataIdentificationUtils.isIdentifyingClass(CLASS_MID)); + } + + @Test + public void testClassMidIsNotInstanceMid() { + assertFalse(MetadataIdentificationUtils + .isIdentifyingInstance(CLASS_MID)); + } + + @Test + public void testClassMidIsValid() { + assertTrue(MetadataIdentificationUtils.isValid(CLASS_MID)); + } + + @Test + public void testEmptyMidIsNotValid() { + assertFalse(MetadataIdentificationUtils.isValid("")); + } + + @Test + public void testGetInstanceKey() { + assertEquals(INSTANCE_CLASS, + MetadataIdentificationUtils.getMetadataInstance(INSTANCE_MID)); + } + + @Test + public void testGetMetadataClassFromClassMid() { + assertEquals(METADATA_CLASS, + MetadataIdentificationUtils.getMetadataClass(CLASS_MID)); + } + + @Test + public void testGetMetadataClassFromEmptyMid() { + assertNull(MetadataIdentificationUtils.getMetadataClass("")); + } + + @Test + public void testGetMetadataClassFromInstanceMid() { + assertEquals(METADATA_CLASS, + MetadataIdentificationUtils.getMetadataClass(INSTANCE_MID)); + } + + @Test + public void testGetMetadataClassFromMidPrefix() { + assertNull(MetadataIdentificationUtils.getMetadataClass(MID_PREFIX)); + } + + @Test + public void testGetMetadataClassFromMidPrefixPlusDelimiter() { + assertNull(MetadataIdentificationUtils.getMetadataClass(MID_PREFIX + + INSTANCE_DELIMITER)); + } + + @Test + public void testGetMetadataClassFromNullMid() { + assertNull(MetadataIdentificationUtils.getMetadataClass(null)); + } + + @Test + public void testGetMetadataClassIdFromClassMid() { + assertEquals(MID_PREFIX + METADATA_CLASS, + MetadataIdentificationUtils.getMetadataClassId(CLASS_MID)); + } + + @Test + public void testGetMetadataClassIdFromEmptyMid() { + assertNull(MetadataIdentificationUtils.getMetadataClassId("")); + } + + @Test + public void testGetMetadataClassIdFromInstanceMid() { + assertEquals(MID_PREFIX + METADATA_CLASS, + MetadataIdentificationUtils.getMetadataClassId(INSTANCE_MID)); + } + + @Test + public void testGetMetadataClassIdFromMidPrefix() { + assertNull(MetadataIdentificationUtils.getMetadataClassId(MID_PREFIX)); + } + + @Test + public void testGetMetadataClassIdFromMidPrefixPlusDelimiter() { + assertNull(MetadataIdentificationUtils.getMetadataClassId(MID_PREFIX + + INSTANCE_DELIMITER)); + } + + @Test + public void testGetMetadataClassIdFromNullMid() { + assertNull(MetadataIdentificationUtils.getMetadataClassId(null)); + } + + @Test + public void testGetMetadataInstanceFromClassMid() { + assertNull(MetadataIdentificationUtils.getMetadataInstance(CLASS_MID)); + } + + @Test + public void testGetMetadataInstanceFromEmptyMid() { + assertNull(MetadataIdentificationUtils.getMetadataInstance("")); + } + + @Test + public void testGetMetadataInstanceFromInstanceMid() { + assertEquals(INSTANCE_CLASS, + MetadataIdentificationUtils.getMetadataInstance(INSTANCE_MID)); + } + + @Test + public void testGetMetadataInstanceFromMidPrefix() { + assertNull(MetadataIdentificationUtils.getMetadataInstance(MID_PREFIX)); + } + + @Test + public void testGetMetadataInstanceFromMidPrefixPlusDelimiter() { + assertNull(MetadataIdentificationUtils.getMetadataInstance(MID_PREFIX + + INSTANCE_DELIMITER)); + } + + @Test + public void testGetMetadataInstanceFromNullMid() { + assertNull(MetadataIdentificationUtils.getMetadataInstance(null)); + } + + @Test + public void testInstanceIdFromNullInstanceKey() { + assertNull(MetadataIdentificationUtils.create(METADATA_CLASS, null)); + } + + @Test + public void testInstanceIdFromValidInputs() { + assertEquals(MID_PREFIX + METADATA_CLASS + INSTANCE_DELIMITER + + INSTANCE_CLASS, MetadataIdentificationUtils.create( + METADATA_CLASS, INSTANCE_CLASS)); + } + + @Test + public void testInstanceMidIsInstanceMid() { + assertTrue(MetadataIdentificationUtils + .isIdentifyingInstance(INSTANCE_MID)); + } + + @Test + public void testInstanceMidIsNotClassMid() { + assertFalse(MetadataIdentificationUtils + .isIdentifyingClass(INSTANCE_MID)); + } + + @Test + public void testInstanceMidIsValid() { + assertTrue(MetadataIdentificationUtils.isValid(CLASS_MID)); + } + + @Test + public void testMetadataIdentifierCreation() { + Assert.assertEquals("MID:com.foo.Bar", + MetadataIdentificationUtils.create("com.foo.Bar")); + Assert.assertNull(MetadataIdentificationUtils.create((String) null)); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar#")); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar#foo")); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar # ")); + Assert.assertNull(MetadataIdentificationUtils.create("")); + Assert.assertNull(MetadataIdentificationUtils.create("#")); + Assert.assertEquals("MID:com.foo.Bar#239", + MetadataIdentificationUtils.create("com.foo.Bar", "239")); + Assert.assertEquals("MID:com.foo.Bar#239 #40", + MetadataIdentificationUtils.create("com.foo.Bar", "239 #40")); + Assert.assertNull(MetadataIdentificationUtils.create(null, "239")); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar#", + "239")); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar#foo", + "239")); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar # ", + "239")); + Assert.assertNull(MetadataIdentificationUtils.create("", "239")); + Assert.assertNull(MetadataIdentificationUtils.create("#", "239")); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar", + null)); + Assert.assertNull(MetadataIdentificationUtils.create("com.foo.Bar", "")); + } + + @Test + public void testMetadataIdentifierParsing() { + Assert.assertFalse(MetadataIdentificationUtils + .isIdentifyingInstance("MID:com.foo.Bar")); + Assert.assertFalse(MetadataIdentificationUtils + .isIdentifyingInstance("MID:com.foo.Bar#")); + Assert.assertTrue(MetadataIdentificationUtils + .isIdentifyingInstance("MID:com.foo.Bar#239")); + Assert.assertTrue(MetadataIdentificationUtils + .isIdentifyingClass("MID:com.foo.Bar")); + Assert.assertFalse(MetadataIdentificationUtils + .isIdentifyingClass("MID:com.foo.Bar#")); + Assert.assertFalse(MetadataIdentificationUtils + .isIdentifyingClass("MID:com.foo.Bar#239")); + Assert.assertNull(MetadataIdentificationUtils.getMetadataClass("MID:")); + Assert.assertNull(MetadataIdentificationUtils.getMetadataClass("MID:#")); + Assert.assertEquals("com.foo.Bar", + MetadataIdentificationUtils.getMetadataClass("MID:com.foo.Bar")); + Assert.assertEquals("com.foo.Bar", MetadataIdentificationUtils + .getMetadataClass("MID:com.foo.Bar#")); + Assert.assertEquals("com.foo.Bar", MetadataIdentificationUtils + .getMetadataClass("MID:com.foo.Bar#239")); + Assert.assertEquals("239", MetadataIdentificationUtils + .getMetadataInstance("MID:com.foo.Bar#239")); + Assert.assertEquals("239", + MetadataIdentificationUtils.getMetadataInstance("MID:#239")); + Assert.assertEquals("239 #40", + MetadataIdentificationUtils.getMetadataInstance("MID:#239 #40")); + Assert.assertNull(MetadataIdentificationUtils + .getMetadataInstance("MID:com.foo.Bar#")); + Assert.assertNull(MetadataIdentificationUtils + .getMetadataInstance("MID:com.foo.Bar 239")); + } + + @Test + public void testMidPrefixIsNotValid() { + assertFalse(MetadataIdentificationUtils.isValid(MID_PREFIX)); + } + + @Test + public void testNullMidIsNotValid() { + assertFalse(MetadataIdentificationUtils.isValid(null)); + } + + @Test + public void testUnprefixedMidIsNotValid() { + assertFalse(MetadataIdentificationUtils.isValid(METADATA_CLASS)); + } +} diff --git a/metadata/src/test/java/org/springframework/roo/metadata/internal/DefaultMetadataDependencyRegistryTest.java b/metadata/src/test/java/org/springframework/roo/metadata/internal/DefaultMetadataDependencyRegistryTest.java new file mode 100644 index 000000000..6aff3b0cb --- /dev/null +++ b/metadata/src/test/java/org/springframework/roo/metadata/internal/DefaultMetadataDependencyRegistryTest.java @@ -0,0 +1,59 @@ +package org.springframework.roo.metadata.internal; + +import junit.framework.Assert; + +import org.junit.Test; +import org.springframework.roo.metadata.MetadataIdentificationUtils; + +public class DefaultMetadataDependencyRegistryTest { + + private static final String DISK_FILE = MetadataIdentificationUtils.create( + "com.Test", "disk file"); + private static final String JAVA_TYPE_OBJECT = MetadataIdentificationUtils + .create("com.Test", "object"); + private static final String JAVA_TYPE_PERSON = MetadataIdentificationUtils + .create("com.Test", "person"); + private static final String JSP_PAGE_1 = MetadataIdentificationUtils + .create("com.Test", "jsp 1"); + private static final String JSP_PAGE_2 = MetadataIdentificationUtils + .create("com.Test", "jsp 2"); + private static final String MVC_CONTROLLER = MetadataIdentificationUtils + .create("com.Test", "mvc ctrl"); + + @Test + public void testRegistration() { + final DefaultMetadataDependencyRegistry reg = new DefaultMetadataDependencyRegistry(); + + // Verify simple registration + reg.registerDependency(DISK_FILE, JAVA_TYPE_OBJECT); + Assert.assertEquals(1, reg.getDownstream(DISK_FILE).size()); + reg.registerDependency(JAVA_TYPE_OBJECT, JAVA_TYPE_PERSON); + Assert.assertEquals(1, reg.getDownstream(DISK_FILE).size()); + Assert.assertEquals(1, reg.getDownstream(JAVA_TYPE_OBJECT).size()); + reg.registerDependency(JAVA_TYPE_PERSON, MVC_CONTROLLER); + + // Verify dependency enforcement is valid + Assert.assertTrue(reg.isValidDependency(MVC_CONTROLLER, JSP_PAGE_1)); + Assert.assertTrue(reg.isValidDependency(MVC_CONTROLLER, JSP_PAGE_2)); + + reg.registerDependency(MVC_CONTROLLER, JSP_PAGE_1); + reg.registerDependency(MVC_CONTROLLER, JSP_PAGE_2); + Assert.assertEquals(2, reg.getDownstream(MVC_CONTROLLER).size()); + + // Can't create circular dependencies + Assert.assertTrue(!reg.isValidDependency(JSP_PAGE_2, MVC_CONTROLLER)); + Assert.assertTrue(!reg.isValidDependency(JAVA_TYPE_PERSON, + JAVA_TYPE_OBJECT)); + + // Ensure individual deregistration works + reg.deregisterDependency(DISK_FILE, JAVA_TYPE_OBJECT); + Assert.assertEquals(0, reg.getDownstream(DISK_FILE).size()); + + // Ensure bulk deregistration works + Assert.assertEquals(1, reg.getDownstream(JAVA_TYPE_PERSON).size()); + Assert.assertEquals(2, reg.getDownstream(MVC_CONTROLLER).size()); + reg.deregisterDependencies(MVC_CONTROLLER); + Assert.assertEquals(0, reg.getDownstream(JAVA_TYPE_PERSON).size()); + Assert.assertEquals(2, reg.getDownstream(MVC_CONTROLLER).size()); + } +} diff --git a/metadata/src/test/java/org/springframework/roo/metadata/internal/StandardMetadataTimingStatisticTest.java b/metadata/src/test/java/org/springframework/roo/metadata/internal/StandardMetadataTimingStatisticTest.java new file mode 100644 index 000000000..f5a4052b7 --- /dev/null +++ b/metadata/src/test/java/org/springframework/roo/metadata/internal/StandardMetadataTimingStatisticTest.java @@ -0,0 +1,61 @@ +package org.springframework.roo.metadata.internal; + +import static org.junit.Assert.assertEquals; +import static org.springframework.roo.metadata.internal.StandardMetadataTimingStatistic.NANOSECONDS_IN_MILLISECOND; + +import org.junit.Test; + +/** + * Unit test of {@link StandardMetadataTimingStatistic} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class StandardMetadataTimingStatisticTest { + + private static final long INVOCATIONS = 5; + + private static final String NAME = "MyProcess"; + + /** + * Asserts that calling {@link StandardMetadataTimingStatistic#toString()} + * on an instance with the given duration results in the expected output + * + * @param nanoseconds + * @param expectedToString + */ + private void assertToString(final long nanoseconds, + final String expectedToString) { + assertEquals(expectedToString, getTestInstance(nanoseconds).toString()); + } + + /** + * Creates an instance with fixed test values and the given duration + * + * @param nanoseconds + * @return a non-null instance + */ + private StandardMetadataTimingStatistic getTestInstance( + final long nanoseconds) { + return new StandardMetadataTimingStatistic(NAME, nanoseconds, + INVOCATIONS); + } + + @Test + public void testToStringForLessThanOneMillisecond() { + assertToString(NANOSECONDS_IN_MILLISECOND - 1, + "999999 ns; 5 call(s): MyProcess"); + } + + @Test + public void testToStringForOneMillisecond() { + assertToString(NANOSECONDS_IN_MILLISECOND, + " 1 ms; 5 call(s): MyProcess"); + } + + @Test + public void testToStringForTwoMilliseconds() { + assertToString(NANOSECONDS_IN_MILLISECOND * 2, + " 2 ms; 5 call(s): MyProcess"); + } +} diff --git a/model/pom.xml b/model/pom.xml new file mode 100644 index 000000000..38ebfcc56 --- /dev/null +++ b/model/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.model + bundle + Spring Roo - Model + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/AbstractCustomDataAccessorBuilder.java b/model/src/main/java/org/springframework/roo/model/AbstractCustomDataAccessorBuilder.java new file mode 100644 index 000000000..040331d81 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/AbstractCustomDataAccessorBuilder.java @@ -0,0 +1,70 @@ +package org.springframework.roo.model; + +import org.apache.commons.lang3.Validate; + +/** + * Assists in the creation of a {@link Builder} for types that eventually + * implement {@link CustomDataAccessor}. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractCustomDataAccessorBuilder + implements Builder { + + private CustomDataBuilder customDataBuilder; + + /** + * Constructor for an empty builder + */ + protected AbstractCustomDataAccessorBuilder() { + this.customDataBuilder = new CustomDataBuilder(); + } + + /** + * Constructor for a builder initialised with the given custom data + * + * @param existing can't be null + */ + protected AbstractCustomDataAccessorBuilder( + final CustomDataAccessor existing) { + Validate.notNull(existing, "Custom data accessor required"); + this.customDataBuilder = new CustomDataBuilder(existing.getCustomData()); + } + + /** + * Appends the given custom data to this builder + * + * @param customDataBuilder the custom data to append; can be + * null to make no changes + */ + public void append(final CustomData customData) { + if (customData != null) { + // Set the custom data builder to a new instance containing both + // builders' values + final CustomDataBuilder customDataBuilder = new CustomDataBuilder( + customData); + customDataBuilder.append(this.customDataBuilder.build()); + this.customDataBuilder = customDataBuilder; + } + } + + public CustomDataBuilder getCustomData() { + return this.customDataBuilder; + } + + public Object putCustomData(final Object key, final Object value) { + return customDataBuilder.put(key, value); + } + + /** + * Sets this builder's {@link CustomDataBuilder} to the given one (does not + * take a copy) + * + * @param customDataBuilder the builder to set (required) + */ + public void setCustomData(final CustomDataBuilder customDataBuilder) { + Validate.notNull(customDataBuilder, "Custom data builder required"); + this.customDataBuilder = customDataBuilder; + } +} diff --git a/model/src/main/java/org/springframework/roo/model/AbstractCustomDataAccessorProvider.java b/model/src/main/java/org/springframework/roo/model/AbstractCustomDataAccessorProvider.java new file mode 100644 index 000000000..f3a48e8ba --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/AbstractCustomDataAccessorProvider.java @@ -0,0 +1,29 @@ +package org.springframework.roo.model; + +import org.apache.commons.lang3.Validate; + +/** + * Convenience superclass for {@link CustomDataAccessor} implementations. + * + * @author Ben Alex + * @since 1.1 + */ +public abstract class AbstractCustomDataAccessorProvider implements + CustomDataAccessor { + + private final CustomData customData; + + /** + * Constructor + * + * @param customData + */ + protected AbstractCustomDataAccessorProvider(final CustomData customData) { + Validate.notNull(customData, "Custom data required"); + this.customData = customData; + } + + public final CustomData getCustomData() { + return customData; + } +} diff --git a/model/src/main/java/org/springframework/roo/model/Builder.java b/model/src/main/java/org/springframework/roo/model/Builder.java new file mode 100644 index 000000000..3ae4f0494 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/Builder.java @@ -0,0 +1,23 @@ +package org.springframework.roo.model; + +/** + * Indicates an implementation that helps users build objects from scratch or + * from existing objects. + *

    + * {@link Builder} objects are used in Spring Roo because most Roo types are + * intentionally and strictly immutable. This can make such objects tedious to + * create and modify. {@link Builder}s help create new instances using simple + * and convenient methods which reflect default values. They also help users + * edit existing instances. + * + * @author Ben Alex + * @since 1.1 + */ +public interface Builder { + + /** + * @return the immutable object this builder creates (never returns null, + * but may throw an exception) + */ + T build(); +} diff --git a/model/src/main/java/org/springframework/roo/model/Criteria.java b/model/src/main/java/org/springframework/roo/model/Criteria.java new file mode 100644 index 000000000..04a28f735 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/Criteria.java @@ -0,0 +1,6 @@ +package org.springframework.roo.model; + +public interface Criteria { + + boolean meets(T t); +} diff --git a/model/src/main/java/org/springframework/roo/model/CustomData.java b/model/src/main/java/org/springframework/roo/model/CustomData.java new file mode 100644 index 000000000..ee72f35d8 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/CustomData.java @@ -0,0 +1,45 @@ +package org.springframework.roo.model; + +import java.util.Map; +import java.util.Set; + +/** + * Represents an immutable collection of custom data key-value pairs. + *

    + * Several metadata interfaces in Spring Roo define the + * {@link CustomDataAccessor} interface. This is the primary mechanism to obtain + * a {@link CustomData} instance. + *

    + * While this interface is essentially a subset of {@link Map}, it has been + * introduced to simplify method signatures, descriptions and allow future + * modification of the {@link CustomData} contract. + * + * @author Ben Alex + * @since 1.1 + */ +public interface CustomData extends Iterable { + + /** + * Obtains a specific item of custom data. + *

    + * It is important that both the key and the object are immutable. Other + * parts of Spring Roo rely on the immutability guarantees of + * {@link CustomData} (and particularly the classes that implement + * {@link CustomDataAccessor}) and therefore you must ensure all keys and + * values are genuinely immutable. Most Spring Roo types are immutable and + * can be placed within {@link CustomData} key-value pairs. Most standard + * Java types are also immutable and can similarly be stored (eg + * {@link String}, {@link Boolean} etc). + * + * @param key to search for (required) + * @return the object if found, otherwise null + */ + Object get(Object key); + + /** + * Obtains an immutable representation of all custom data keys. + * + * @return the keys (never null, but may be empty) + */ + Set keySet(); +} diff --git a/model/src/main/java/org/springframework/roo/model/CustomDataAccessor.java b/model/src/main/java/org/springframework/roo/model/CustomDataAccessor.java new file mode 100644 index 000000000..c25bebda2 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/CustomDataAccessor.java @@ -0,0 +1,17 @@ +package org.springframework.roo.model; + +/** + * Allows custom data to be stored against various Spring Roo types. + * + * @author Ben Alex + * @since 1.1 + */ +public interface CustomDataAccessor { + + /** + * Provides immutable access to the custom data stored against the instance. + * + * @return the custom data (never returns null) + */ + CustomData getCustomData(); +} diff --git a/model/src/main/java/org/springframework/roo/model/CustomDataBuilder.java b/model/src/main/java/org/springframework/roo/model/CustomDataBuilder.java new file mode 100644 index 000000000..95863a39b --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/CustomDataBuilder.java @@ -0,0 +1,81 @@ +package org.springframework.roo.model; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Builder for {@link CustomData}. + *

    + * Can be used to create new instances from scratch, or based on an existing + * {@link CustomData} instance. + * + * @author Ben Alex + * @since 1.1 + */ +public class CustomDataBuilder implements Builder { + + private final Map customData = new LinkedHashMap(); + + /** + * Constructor for an empty builder + */ + public CustomDataBuilder() { + } + + /** + * Constructor for a builder initialised with the given contents + * + * @param existing can be null + */ + public CustomDataBuilder(final CustomData existing) { + append(existing); + } + + /** + * Appends the given custom data to this builder + * + * @param customData the custom data to append; can be null to + * make no changes + */ + public void append(final CustomData customData) { + if (customData != null) { + for (final Object key : customData.keySet()) { + this.customData.put(key, customData.get(key)); + } + } + } + + public CustomData build() { + return new CustomDataImpl(customData); + } + + public void clear() { + customData.clear(); + } + + public Object get(final Object key) { + return customData.get(key); + } + + public Set keySet() { + return customData.keySet(); + } + + public Object put(final Object key, final Object value) { + return customData.put(key, value); + } + + public Object remove(final Object key) { + return customData.remove(key); + } + + public int size() { + return customData.size(); + } + + public Collection values() { + return customData.values(); + } +} diff --git a/model/src/main/java/org/springframework/roo/model/CustomDataImpl.java b/model/src/main/java/org/springframework/roo/model/CustomDataImpl.java new file mode 100644 index 000000000..13a75c270 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/CustomDataImpl.java @@ -0,0 +1,76 @@ +package org.springframework.roo.model; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; + +/** + * Default implementation of {@link CustomData}. + * + * @author Ben Alex + * @since 1.1 + */ +public class CustomDataImpl implements CustomData { + + public static final CustomData NONE = new CustomDataImpl( + new LinkedHashMap()); + + private final Map customData; + + public CustomDataImpl(final Map customData) { + Validate.notNull(customData, "Custom data required"); + this.customData = Collections.unmodifiableMap(customData); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CustomDataImpl other = (CustomDataImpl) obj; + if (customData == null) { + if (other.customData != null) { + return false; + } + } + else if (!customData.equals(other.customData)) { + return false; + } + return true; + } + + public Object get(final Object key) { + return customData.get(key); + } + + @Override + public int hashCode() { + final int prime = 31; + final int result = 1; + return prime * result + + (customData == null ? 0 : customData.hashCode()); + } + + public Iterator iterator() { + return customData.keySet().iterator(); + } + + public Set keySet() { + return customData.keySet(); + } + + @Override + public String toString() { + return customData.toString(); + } +} diff --git a/model/src/main/java/org/springframework/roo/model/CustomDataKey.java b/model/src/main/java/org/springframework/roo/model/CustomDataKey.java new file mode 100644 index 000000000..5ae7f2c70 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/CustomDataKey.java @@ -0,0 +1,13 @@ +package org.springframework.roo.model; + +/** + * A type safe type that provides a key for tagging and validating + * {@link org.springframework.roo.model.CustomDataAccessor} objects. + * + * @author James Tyrrell + * @since 1.1.3 + */ +public interface CustomDataKey extends Criteria { + + String name(); +} diff --git a/model/src/main/java/org/springframework/roo/model/DataType.java b/model/src/main/java/org/springframework/roo/model/DataType.java new file mode 100644 index 000000000..f15744cb5 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/DataType.java @@ -0,0 +1,5 @@ +package org.springframework.roo.model; + +public enum DataType { + PRIMITIVE, TYPE, VARIABLE +} diff --git a/model/src/main/java/org/springframework/roo/model/EnumDetails.java b/model/src/main/java/org/springframework/roo/model/EnumDetails.java new file mode 100644 index 000000000..e93f79201 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/EnumDetails.java @@ -0,0 +1,35 @@ +package org.springframework.roo.model; + +import org.apache.commons.lang3.Validate; + +/** + * Immutable representation of an enumeration. + * + * @author Ben Alex + * @since 1.0 + */ +public class EnumDetails { + private final JavaSymbolName field; + private final JavaType type; + + public EnumDetails(final JavaType type, final JavaSymbolName field) { + Validate.notNull(type, "Type required"); + Validate.notNull(field, "Field required"); + this.type = type; + this.field = field; + } + + public JavaSymbolName getField() { + return field; + } + + public JavaType getType() { + return type; + } + + @Override + public String toString() { + return type.getFullyQualifiedTypeName() + "." + field.getSymbolName(); + } + +} \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/GoogleJavaType.java b/model/src/main/java/org/springframework/roo/model/GoogleJavaType.java new file mode 100644 index 000000000..3014c0acc --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/GoogleJavaType.java @@ -0,0 +1,28 @@ +package org.springframework.roo.model; + +/** + * Constants for Google-specific {@link JavaType}s. N.B. GWT-specific types are + * in GwtUtils. Use them in preference to creating new instances of these types. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class GoogleJavaType { + + // com.google.appengine + public static final JavaType GAE_DATASTORE_KEY = new JavaType( + "com.google.appengine.api.datastore.Key"); + public static final JavaType GAE_DATASTORE_KEY_FACTORY = new JavaType( + "com.google.appengine.api.datastore.KeyFactory"); + public static final JavaType GAE_LOCAL_SERVICE_TEST_HELPER = new JavaType( + "com.google.appengine.tools.development.testing.LocalServiceTestHelper"); + // org.datanucleus + public static final JavaType DATANUCLEUS_JPA_EXTENSION = new JavaType( + "org.datanucleus.api.jpa.annotations.Extension"); + + /** + * Constructor is private to prevent instantiation + */ + private GoogleJavaType() { + } +} diff --git a/model/src/main/java/org/springframework/roo/model/HibernateJavaType.java b/model/src/main/java/org/springframework/roo/model/HibernateJavaType.java new file mode 100644 index 000000000..c1d1b3979 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/HibernateJavaType.java @@ -0,0 +1,20 @@ +package org.springframework.roo.model; + +/** + * Constants for Hibernate {@link JavaType}s. Use them in preference to creating + * new instances of these types. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class HibernateJavaType { + + public static final JavaType VALIDATOR_CONSTRAINTS_EMAIL = new JavaType( + "org.hibernate.validator.constraints.Email"); + + /** + * Constructor is private to prevent instantiation + */ + private HibernateJavaType() { + } +} \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/ImportRegistrationResolver.java b/model/src/main/java/org/springframework/roo/model/ImportRegistrationResolver.java new file mode 100644 index 000000000..e0c1945d2 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/ImportRegistrationResolver.java @@ -0,0 +1,111 @@ +package org.springframework.roo.model; + +import java.util.List; +import java.util.Set; + +/** + * Represents the known imports for a particular compilation unit, and resolves + * whether a particular type name can be expressed as a simple type name or + * requires a fully-qualified type name. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ImportRegistrationResolver { + + /** + * Explicitly registers an import. Note that no verification will be + * performed to ensure an import is legal or does not conflict with an + * existing import (use {@link #isAdditionLegal(JavaType)} for + * verification). + * + * @param typeToImport to register (can be null to do nothing) + */ + void addImport(JavaType typeToImport); + + /** + * Explicitly registers the given imports. Note that no verification will be + * performed to ensure an import is legal or does not conflict with an + * existing import (use {@link #isAdditionLegal(JavaType)} for + * verification). + * + * @param typesToImport any null elements will be ignored + * @since 1.2.0 + */ + void addImports(JavaType... typesToImport); + + /** + * Explicitly registers the given imports. Note that no verification will be + * performed to ensure an import is legal or does not conflict with an + * existing import (use {@link #isAdditionLegal(JavaType)} for + * verification). + * + * @param typesToImport any null elements will be ignored + * @since 1.2.0 + */ + void addImports(List typesToImport); + + /** + * @return the package this compilation unit belongs to (never null) + */ + JavaPackage getCompilationUnitPackage(); + + /** + * Provides access to the registered imports. + * + * @return an unmodifiable representation of all registered imports (never + * null, but may be empty) + */ + Set getRegisteredImports(); + + /** + * Indicates whether the presented {@link JavaType} can be legally presented + * to {@link #addImport(JavaType)}. It is considered legal only if the + * presented {@link JavaType} is of type {@link DataType#TYPE}, there is not + * an existing conflicting registered import, and the proposed type is not + * within the default package. Note it is legal to add types from the same + * package as the compilation unit, and indeed may be required by + * implementations that are otherwise unaware of all the types available in + * a particular package. + * + * @param javaType + * @return true is the presented type can be legally presented to + * {@link #addImport(JavaType)}, otherwise false. + */ + boolean isAdditionLegal(JavaType javaType); + + /** + * Determines whether the presented {@link JavaType} must be used in a + * fully-qualified form or not. It may only be used in simple form if: + *
      + *
    • it is of {@link DataType#VARIABLE}; or
    • + *
    • it is of {@link DataType#PRIMITIVE}; or
    • + *
    • it is already registered as an import; or
    • + *
    • it is in the same package as the compilation unit; or
    • + *
    • it is part of java.lang
    • + *
    + *

    + * Note that advanced implementations may be able to determine all types + * available in a particular package, but this is not required. + * + * @param javaType to lookup (required) + * @return true if a fully-qualified form must be used, or false if a simple + * form can be used + */ + boolean isFullyQualifiedFormRequired(JavaType javaType); + + /** + * Automatically invokes {@link #isAdditionLegal(JavaType)}, then + * {@link #addImport(JavaType)}, and finally + * {@link #isFullyQualifiedFormRequired(JavaType)}, returning the result of + * the final method. This method is the main method that should be used by + * callers, as it will automatically attempt to cause a {@link JavaType} to + * be used in its simple form if at all possible. + * + * @param javaType to automatically register (if possible) and lookup + * whether simplified used is available (required) + * @return true if a fully-qualified form must be used, or false if a simple + * form can be used + */ + boolean isFullyQualifiedFormRequiredAfterAutoImport(JavaType javaType); +} diff --git a/model/src/main/java/org/springframework/roo/model/ImportRegistrationResolverImpl.java b/model/src/main/java/org/springframework/roo/model/ImportRegistrationResolverImpl.java new file mode 100644 index 000000000..2316893af --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/ImportRegistrationResolverImpl.java @@ -0,0 +1,135 @@ +package org.springframework.roo.model; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.Validate; + +/** + * Implementation of {@link ImportRegistrationResolver}. + * + * @author Ben Alex + * @since 1.0 + */ +public class ImportRegistrationResolverImpl implements + ImportRegistrationResolver { + + private final JavaPackage compilationUnitPackage; + private final SortedSet registeredImports = new TreeSet( + new Comparator() { + public int compare(final JavaType o1, final JavaType o2) { + return o1.getFullyQualifiedTypeName().compareTo( + o2.getFullyQualifiedTypeName()); + } + }); + + public ImportRegistrationResolverImpl( + final JavaPackage compilationUnitPackage) { + Validate.notNull(compilationUnitPackage, + "Compilation unit package required"); + this.compilationUnitPackage = compilationUnitPackage; + } + + public void addImport(final JavaType javaType) { + if (javaType != null) { + if (!JdkJavaType.isPartOfJavaLang(javaType)) { + registeredImports.add(javaType); + } + } + } + + public void addImports(final JavaType... typesToImport) { + for (final JavaType typeToImport : typesToImport) { + addImport(typeToImport); + } + } + + public void addImports(final List typesToImport) { + if (typesToImport != null) { + for (final JavaType typeToImport : typesToImport) { + addImport(typeToImport); + } + } + } + + public JavaPackage getCompilationUnitPackage() { + return compilationUnitPackage; + } + + public Set getRegisteredImports() { + return Collections.unmodifiableSet(registeredImports); + } + + public boolean isAdditionLegal(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + + if (javaType.getDataType() != DataType.TYPE) { + // It's a type variable or primitive + return false; + } + + if (javaType.isDefaultPackage()) { + // Cannot import types from the default package + return false; + } + + // Must be a class, so it's legal if there isn't an existing + // registration that conflicts + for (final JavaType candidate : registeredImports) { + if (candidate.getSimpleTypeName().equals( + javaType.getSimpleTypeName())) { + // Conflict detected + return false; + } + } + + return true; + } + + public boolean isFullyQualifiedFormRequired(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + + if (javaType.getDataType() == DataType.PRIMITIVE + || javaType.getDataType() == DataType.VARIABLE) { + // Primitives and type variables do not need to be used in + // fully-qualified form + return false; + } + + if (registeredImports.contains(javaType)) { + // Already know about this one + return false; + } + + if (compilationUnitPackage.equals(javaType.getPackage())) { + // No need for an explicit registration, given it's in the same + // package + return false; + } + + if (JdkJavaType.isPartOfJavaLang(javaType)) { + return false; + } + + // To get this far, it must need a fully-qualified name + return true; + } + + public boolean isFullyQualifiedFormRequiredAfterAutoImport( + final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + + // Try to add import if possible + if (isAdditionLegal(javaType)) { + addImport(javaType); + } + + // Indicate whether we can use in a simple or need a fully-qualified + // form + return isFullyQualifiedFormRequired(javaType); + } +} diff --git a/model/src/main/java/org/springframework/roo/model/JavaPackage.java b/model/src/main/java/org/springframework/roo/model/JavaPackage.java new file mode 100644 index 000000000..dc90f374d --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/JavaPackage.java @@ -0,0 +1,109 @@ +package org.springframework.roo.model; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * Immutable representation of a Java package. + *

    + * This class is used whenever a formal reference to a Java package is required. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaPackage implements Comparable { + + private final String fullyQualifiedPackageName; + + /** + * Construct a JavaPackage. + *

    + * The fully qualified package name will be enforced as follows: + *

      + *
    • The rules listed in + * {@link JavaSymbolName#assertJavaNameLegal(String)} + *
    + * + * @param fullyQualifiedPackageName the name (as per the above rules; + * mandatory) + */ + public JavaPackage(final String fullyQualifiedPackageName) { + Validate.notNull(fullyQualifiedPackageName, + "Fully qualified package name required"); + JavaSymbolName.assertJavaNameLegal(fullyQualifiedPackageName); + this.fullyQualifiedPackageName = fullyQualifiedPackageName; + } + + public int compareTo(final JavaPackage o) { + if (o == null) { + return -1; + } + return fullyQualifiedPackageName.compareTo(o + .getFullyQualifiedPackageName()); + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof JavaPackage && compareTo((JavaPackage) obj) == 0; + } + + /** + * Returns the elements of this package's fully-qualified name + * + * @return a non-empty list + */ + public List getElements() { + return Arrays.asList(StringUtils.split(fullyQualifiedPackageName, ".")); + } + + /** + * @return the fully qualified package name (complies with the rules + * specified in the constructor) + */ + public String getFullyQualifiedPackageName() { + return fullyQualifiedPackageName; + } + + /** + * Returns the last element of the fully-qualified package name + * + * @return a non-blank element + */ + public String getLastElement() { + final List elements = getElements(); + return elements.get(elements.size() - 1); + } + + @Override + public int hashCode() { + return fullyQualifiedPackageName.hashCode(); + } + + /** + * Indicates whether this package is anywhere within the given package, in + * other words is the same package or is a sub-package of the given one. For + * example: + *
      + *
    • com.foo is within com.foo
    • + *
    • com.foo.bar is within com.foo
    • + *
    • com.foo is not within com.foo.bar
    • + *
    + * + * @param otherPackage the package to check against (can be + * null) + * @return false if a null package is given + */ + public boolean isWithin(final JavaPackage otherPackage) { + return otherPackage != null + && fullyQualifiedPackageName.startsWith(otherPackage + .getFullyQualifiedPackageName()); + } + + @Override + public String toString() { + return fullyQualifiedPackageName; + } +} diff --git a/model/src/main/java/org/springframework/roo/model/JavaSymbolName.java b/model/src/main/java/org/springframework/roo/model/JavaSymbolName.java new file mode 100644 index 000000000..aefe2c829 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/JavaSymbolName.java @@ -0,0 +1,202 @@ +package org.springframework.roo.model; + +import java.beans.Introspector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * Immutable representation of a Java field name, method name, or other common + * legal Java identifier. + *

    + * Ensures the field is properly formed. + * + * @author Ben Alex + * @author Greg Turnquist + * @since 1.0 + */ +public class JavaSymbolName implements Comparable { + + /** Constant for keyword "false" */ + public static final JavaSymbolName FALSE = new JavaSymbolName("false"); + + /** Constant for keyword "true" */ + public static final JavaSymbolName TRUE = new JavaSymbolName("true"); + + /** + * Verifies the presented name is a valid Java name. Specifically, the + * following is enforced: + *

      + *
    • Textual content must be provided in the name
    • + *
    • Must not have any slashes in the name
    • + *
    • Must not start with a number
    • + *
    • Must not have any spaces or other illegal characters in the name
    • + *
    • Must not start or end with a period
    • + *
    + * + * @param name the name to evaluate (required) + */ + public static void assertJavaNameLegal(final String name) { + Validate.notNull(name, "Name required"); + + // Note regular expression for legal characters found to be x5 slower in + // profiling than this approach + final char[] value = name.toCharArray(); + for (int i = 0; i < value.length; i++) { + final char c = value[i]; + if ('/' == c || ' ' == c || '*' == c || '>' == c || '<' == c + || '!' == c || '@' == c || '%' == c || '^' == c || '?' == c + || '(' == c || ')' == c || '~' == c || '`' == c || '{' == c + || '}' == c || '[' == c || ']' == c || '|' == c + || '\\' == c || '\'' == c || '+' == c || '-' == c) { + throw new IllegalArgumentException("Illegal name '" + name + + "' (illegal character)"); + } + if (i == 0) { + if ('1' == c || '2' == c || '3' == c || '4' == c || '5' == c + || '6' == c || '7' == c || '8' == c || '9' == c + || '0' == c) { + throw new IllegalArgumentException("Illegal name '" + name + + "' (cannot start with a number)"); + } + } + if (i + 1 == value.length || i == 0) { + if ('.' == c) { + throw new IllegalArgumentException("Illegal name '" + name + + "' (cannot start or end with a period)"); + } + } + } + } + + /** + * @return a camel case string in human readable form + */ + public static String getReadableSymbolName(final String camelCase) { + final Pattern p = Pattern.compile("[A-Z][^A-Z]*"); + final Matcher m = p.matcher(StringUtils.capitalize(camelCase)); + final StringBuilder builder = new StringBuilder(); + while (m.find()) { + builder.append(m.group()).append(" "); + } + return builder.toString().trim(); + } + + /** + * Construct a Java symbol name which adheres to the strict JavaBean naming + * conventions and avoids use of {@link ReservedWords} by suffixing '_' + * + * @param javaType the {@link JavaType} for which the symbol name is created + * @return a Java symbol name adhering to JavaBean conventions and avoids + * reserved words + * @since 1.2.0 + */ + public static JavaSymbolName getReservedWordSafeName(final JavaType javaType) { + final String simpleTypeName = javaType.getSimpleTypeName(); + String str = Introspector.decapitalize(StringUtils + .capitalize(simpleTypeName)); + while (ReservedWords.RESERVED_JAVA_KEYWORDS.contains(str)) { + // Prefixing can create names that don't work in the Derby DB + str += "_"; + } + if (str.equals(simpleTypeName)) { // ROO-2929 + str += "_"; + } + return new JavaSymbolName(str); + } + + /** + * Construct a Java symbol name which adheres to the strict JavaBean naming + * conventions and avoids use of {@link ReservedWords} by prefixing '_' + * + * @param javaType the {@link JavaType} for which the symbol name is created + * @return a Java symbol name adhering to JavaBean conventions and avoids + * reserved words + * @deprecated use {@link #getReservedWordSafeName(JavaType)} instead (does + * the same thing, just better named) + */ + @Deprecated + public static JavaSymbolName getReservedWordSaveName(final JavaType javaType) { + return getReservedWordSafeName(javaType); + } + + public static boolean isLegalJavaName(final String name) { + try { + assertJavaNameLegal(name); + } + catch (final IllegalArgumentException e) { + return false; + } + return true; + } + + private final String symbolName; + + /** + * Construct a Java symbol name. + *

    + * The name will be enforced as follows: + *

      + *
    • The rules listed in {@link #assertJavaNameLegal(String)} + *
    + * + * @param symbolName the name (mandatory) + */ + public JavaSymbolName(final String symbolName) { + Validate.notBlank(symbolName, "Fully qualified type name required"); + assertJavaNameLegal(symbolName); + this.symbolName = symbolName; + } + + public int compareTo(final JavaSymbolName o) { + // NB: If adding more fields to this class ensure the equals(Object) + // method is updated accordingly + if (o == null) { + return -1; + } + return symbolName.compareTo(o.symbolName); + } + + @Override + public boolean equals(final Object obj) { + // NB: Not using the normal convention of delegating to compareTo (for + // efficiency reasons) + return obj instanceof JavaSymbolName + && symbolName.equals(((JavaSymbolName) obj).symbolName); + } + + /** + * @return the symbol name in human readable form + */ + public String getReadableSymbolName() { + final String camelCase = symbolName; + return getReadableSymbolName(camelCase); + } + + /** + * @return the symbol name (never null or empty) + */ + public String getSymbolName() { + return symbolName; + } + + /** + * @return the symbol name, capitalising the first letter (never null or + * empty) + */ + public String getSymbolNameCapitalisedFirstLetter() { + return StringUtils.capitalize(symbolName); + } + + @Override + public int hashCode() { + return symbolName.hashCode(); + } + + @Override + public String toString() { + return symbolName; + } +} diff --git a/model/src/main/java/org/springframework/roo/model/JavaSymbolNameEditor.java b/model/src/main/java/org/springframework/roo/model/JavaSymbolNameEditor.java new file mode 100644 index 000000000..e7bef74bf --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/JavaSymbolNameEditor.java @@ -0,0 +1,34 @@ +package org.springframework.roo.model; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; + +import org.apache.commons.lang3.StringUtils; + +/** + * {@link PropertyEditor} for {@link JavaSymbolName}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaSymbolNameEditor extends PropertyEditorSupport { + + @Override + public String getAsText() { + final JavaSymbolName obj = (JavaSymbolName) getValue(); + if (obj == null) { + return null; + } + return obj.getSymbolName(); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (text == null || "".equals(text)) { + setValue(null); + } + // Symbol names never start with a capital + text = StringUtils.uncapitalize(text); + setValue(new JavaSymbolName(text)); + } +} diff --git a/model/src/main/java/org/springframework/roo/model/JavaType.java b/model/src/main/java/org/springframework/roo/model/JavaType.java new file mode 100644 index 000000000..ab4ef8ac7 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/JavaType.java @@ -0,0 +1,685 @@ +package org.springframework.roo.model; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * The declaration of a Java type (i.e. contains no details of its members). + * Instances are immutable. + *

    + * Note that a Java type can be contained within a package, but a package is not + * a type. + *

    + * This class is used whenever a formal reference to a Java type is required. It + * provides convenient ways to determine the type's simple name and package + * name. A related {@link org.springframework.core.convert.converter.Converter} + * is also offered. + * + * @author Ben Alex + * @since 1.0 + */ +public class JavaType implements Comparable { + + public static final JavaType BOOLEAN_OBJECT = new JavaType( + "java.lang.Boolean"); + public static final JavaType BOOLEAN_PRIMITIVE = new JavaType( + "java.lang.Boolean", 0, DataType.PRIMITIVE, null, null); + public static final JavaType BYTE_ARRAY_PRIMITIVE = new JavaType( + "java.lang.Byte", 1, DataType.PRIMITIVE, null, null); + public static final JavaType BYTE_OBJECT = new JavaType("java.lang.Byte"); + public static final JavaType BYTE_PRIMITIVE = new JavaType( + "java.lang.Byte", 0, DataType.PRIMITIVE, null, null); + public static final JavaType CHAR_OBJECT = new JavaType( + "java.lang.Character"); + public static final JavaType CHAR_PRIMITIVE = new JavaType( + "java.lang.Character", 0, DataType.PRIMITIVE, null, null); + public static final JavaType CLASS = new JavaType("java.lang.Class"); + // The fully-qualified names of common collection types + private static final Set COMMON_COLLECTION_TYPES = new HashSet(); + private static final String[] CORE_TYPE_PREFIXES = { "java.", "javax." }; + public static final JavaType DOUBLE_OBJECT = new JavaType( + "java.lang.Double"); + public static final JavaType DOUBLE_PRIMITIVE = new JavaType( + "java.lang.Double", 0, DataType.PRIMITIVE, null, null); + public static final JavaType FLOAT_OBJECT = new JavaType("java.lang.Float"); + public static final JavaType FLOAT_PRIMITIVE = new JavaType( + "java.lang.Float", 0, DataType.PRIMITIVE, null, null); + public static final JavaType INT_OBJECT = new JavaType("java.lang.Integer"); + public static final JavaType INT_PRIMITIVE = new JavaType( + "java.lang.Integer", 0, DataType.PRIMITIVE, null, null); + public static final JavaType LONG_OBJECT = new JavaType("java.lang.Long"); + public static final JavaType LONG_PRIMITIVE = new JavaType( + "java.lang.Long", 0, DataType.PRIMITIVE, null, null); + public static final JavaType OBJECT = new JavaType("java.lang.Object"); + public static final JavaType SERIALIZABLE = new JavaType( + "java.io.Serializable"); + public static final JavaType SHORT_OBJECT = new JavaType("java.lang.Short"); + public static final JavaType SHORT_PRIMITIVE = new JavaType( + "java.lang.Short", 0, DataType.PRIMITIVE, null, null); + public static final JavaType STRING = new JavaType("java.lang.String"); + public static final JavaType STRING_ARRAY = new JavaType( + "java.lang.String", 1, DataType.TYPE, null, null); + + /** + * @deprecated use {@link #STRING} instead + */ + @Deprecated public static final JavaType STRING_OBJECT = STRING; + + public static final JavaType VOID_OBJECT = new JavaType("java.lang.Void"); + public static final JavaType VOID_PRIMITIVE = new JavaType( + "java.lang.Void", 0, DataType.PRIMITIVE, null, null); + // Used for wildcard type parameters; it must be one or the other + public static final JavaSymbolName WILDCARD_EXTENDS = new JavaSymbolName( + "_ROO_WILDCARD_EXTENDS_"); // List + + public static final JavaSymbolName WILDCARD_NEITHER = new JavaSymbolName( + "_ROO_WILDCARD_NEITHER_"); // List + + public static final JavaSymbolName WILDCARD_SUPER = new JavaSymbolName( + "_ROO_WILDCARD_SUPER_"); // List + + static { + COMMON_COLLECTION_TYPES.add(ArrayList.class.getName()); + COMMON_COLLECTION_TYPES.add(Collection.class.getName()); + COMMON_COLLECTION_TYPES.add(HashMap.class.getName()); + COMMON_COLLECTION_TYPES.add(HashSet.class.getName()); + COMMON_COLLECTION_TYPES.add(List.class.getName()); + COMMON_COLLECTION_TYPES.add(Map.class.getName()); + COMMON_COLLECTION_TYPES.add(Set.class.getName()); + COMMON_COLLECTION_TYPES.add(TreeMap.class.getName()); + COMMON_COLLECTION_TYPES.add(Vector.class.getName()); + } + + /** + * Factory method for a {@link JavaType} with full details. Recall that + * {@link JavaType} is immutable and therefore this is the only way of + * setting these non-default values. This is a factory method rather than a + * constructor so as not to cause ambiguity problems for existing callers of + * {@link #JavaType(String, int, DataType, JavaSymbolName, List)} + * + * @param fullyQualifiedTypeName the name (as per the rules above) + * @param arrayDimensions the number of array dimensions (0 = not an array, + * 1 = one-dimensional array, etc.) + * @param dataType the {@link DataType} (required) + * @param argName the type argument name to this particular Java type (can + * be null if unassigned) + * @param parameters the type parameters applicable (can be null if there + * aren't any) + * @return a JavaType instance constructed based on the passed in details + * @since 1.2.0 + */ + public static JavaType getInstance(final String fullyQualifiedTypeName, + final int arrayDimensions, final DataType dataType, + final JavaSymbolName argName, final JavaType... parameters) { + return new JavaType(fullyQualifiedTypeName, arrayDimensions, dataType, + argName, Arrays.asList(parameters)); + } + + /** + * Factory method for a {@link JavaType} with full details. Recall that + * {@link JavaType} is immutable and therefore this is the only way of + * setting these non-default values. This is a factory method rather than a + * constructor so as not to cause ambiguity problems for existing callers of + * {@link #JavaType(String, int, DataType, JavaSymbolName, List)} + * + * @param fullyQualifiedTypeName the name (as per the rules above) + * @param enclosingType the type's enclosing type + * @param arrayDimensions the number of array dimensions (0 = not an array, + * 1 = one-dimensional array, etc.) + * @param dataType the {@link DataType} (required) + * @param argName the type argument name to this particular Java type (can + * be null if unassigned) + * @param parameters the type parameters applicable (can be null if there + * aren't any) + * @return a JavaType instance constructed based on the passed in details + * @since 1.2.0 + */ + public static JavaType getInstance(final String fullyQualifiedTypeName, + final JavaType enclosingType, final int arrayDimensions, + final DataType dataType, final JavaSymbolName argName, + final JavaType... parameters) { + return new JavaType(fullyQualifiedTypeName, enclosingType, + arrayDimensions, dataType, argName, Arrays.asList(parameters)); + } + + /** + * Returns a {@link JavaType} for a list of the given element type + * + * @param elementType the type of element in the list (required) + * @return a non-null type + * @since 1.2.0 + */ + public static JavaType listOf(final JavaType elementType) { + return new JavaType(List.class.getName(), 0, DataType.TYPE, null, + Arrays.asList(elementType)); + } + + private final JavaSymbolName argName; + private final int arrayDimensions; + private final DataType dataType; + private final boolean defaultPackage; + private final JavaType enclosingType; + private final String fullyQualifiedTypeName; + private final List parameters; + private final String simpleTypeName; + + /** + * Constructor equivalent to {@link #JavaType(String)}, but takes a Class + * for convenience and type safety. + * + * @param type the class for which to create an instance (required) + * @since 1.2.0 + */ + public JavaType(final Class type) { + this(type.getName()); + } + + /** + * Constructs a {@link JavaType}. + *

    + * The fully qualified type name will be enforced as follows: + *

      + *
    • The rules listed in + * {@link JavaSymbolName#assertJavaNameLegal(String)} + *
    • First letter of simple type name must be upper-case
    • + *
    + *

    + * A fully qualified type name may include or exclude a package designator. + * + * @param fullyQualifiedTypeName the name (as per the above rules; + * mandatory) + */ + public JavaType(final String fullyQualifiedTypeName) { + this(fullyQualifiedTypeName, 0, DataType.TYPE, null, null); + } + + /** + * Construct a {@link JavaType} with full details. Recall that + * {@link JavaType} is immutable and therefore this is the only way of + * setting these non-default values. + * + * @param fullyQualifiedTypeName the name (as per the rules above) + * @param arrayDimensions the number of array dimensions (0 = not an array, + * 1 = one-dimensional array, etc.) + * @param dataType the {@link DataType} (required) + * @param argName the type argument name to this particular Java type (can + * be null if unassigned) + * @param parameters the type parameters applicable (can be null if there + * aren't any) + */ + public JavaType(final String fullyQualifiedTypeName, + final int arrayDimensions, final DataType dataType, + final JavaSymbolName argName, final List parameters) { + this(fullyQualifiedTypeName, null, arrayDimensions, dataType, argName, + parameters); + } + + /** + * Constructs a {@link JavaType}. + *

    + * The fully qualified type name will be enforced as follows: + *

      + *
    • The rules listed in + * {@link JavaSymbolName#assertJavaNameLegal(String)} + *
    • First letter of simple type name must be upper-case
    • + *
    + *

    + * A fully qualified type name may include or exclude a package designator. + * + * @param fullyQualifiedTypeName the name (as per the above rules; + * mandatory) + * @param enclosingType the type's enclosing type + */ + public JavaType(final String fullyQualifiedTypeName, + final JavaType enclosingType) { + this(fullyQualifiedTypeName, enclosingType, 0, DataType.TYPE, null, + null); + } + + /** + * Construct a {@link JavaType} with full details. Recall that + * {@link JavaType} is immutable and therefore this is the only way of + * setting these non-default values. + * + * @param fullyQualifiedTypeName the name (as per the rules above) + * @param enclosingType the type's enclosing type + * @param arrayDimensions the number of array dimensions (0 = not an array, + * 1 = one-dimensional array, etc.) + * @param dataType the {@link DataType} (required) + * @param argName the type argument name to this particular Java type (can + * be null if unassigned) + * @param parameters the type parameters applicable (can be null if there + * aren't any) + */ + public JavaType(final String fullyQualifiedTypeName, + final JavaType enclosingType, final int arrayDimensions, + final DataType dataType, final JavaSymbolName argName, + final List parameters) { + Validate.notBlank(fullyQualifiedTypeName, + "Fully qualified type name required"); + Validate.notNull(dataType, "Data type required"); + JavaSymbolName.assertJavaNameLegal(fullyQualifiedTypeName); + this.argName = argName; + this.arrayDimensions = arrayDimensions; + this.dataType = dataType; + this.fullyQualifiedTypeName = fullyQualifiedTypeName; + defaultPackage = !fullyQualifiedTypeName.contains("."); + if (enclosingType == null) { + this.enclosingType = determineEnclosingType(); + } + else { + this.enclosingType = enclosingType; + } + if (defaultPackage) { + simpleTypeName = fullyQualifiedTypeName; + } + else { + final int offset = fullyQualifiedTypeName.lastIndexOf("."); + simpleTypeName = fullyQualifiedTypeName.substring(offset + 1); + } + + this.parameters = new ArrayList(); + if (parameters != null) { + this.parameters.addAll(parameters); + } + } + + @Override + public int compareTo(final JavaType o) { + // NB: If adding more fields to this class ensure the equals(Object) + // method is updated accordingly + if (o == null) { + return -1; + } + if (equals(o)) { + return 0; + } + return toString().compareTo(o.toString()); + } + + private JavaType determineEnclosingType() { + final int offset = fullyQualifiedTypeName.lastIndexOf("."); + if (offset == -1) { + // There is no dot in the name, so there's no way there's an + // enclosing type + return null; + } + final String possibleName = fullyQualifiedTypeName.substring(0, offset); + final int offset2 = possibleName.lastIndexOf("."); + + // Start by handling if the type name is Foo.Bar (ie an enclosed type + // within the default package) + String enclosedWithinPackage = null; + String enclosedWithinTypeName = possibleName; + + // Handle the probability the type name is within a package like + // com.alpha.Foo.Bar + if (offset2 > -1) { + enclosedWithinPackage = possibleName.substring(0, offset2); + enclosedWithinTypeName = possibleName.substring(offset2 + 1); + } + + if (Character.isUpperCase(enclosedWithinTypeName.charAt(0))) { + // First letter is upper-case, so treat it as a type name for now + final String preTypeNamePortion = enclosedWithinPackage == null ? "" + : enclosedWithinPackage + "."; + return new JavaType(preTypeNamePortion + enclosedWithinTypeName); + } + + return null; + } + + @Override + public boolean equals(final Object obj) { + // NB: Not using the normal convention of delegating to compareTo (for + // efficiency reasons) + return obj != null + && obj instanceof JavaType + && fullyQualifiedTypeName.equals(((JavaType) obj) + .getFullyQualifiedTypeName()) + && dataType == ((JavaType) obj).getDataType() + && arrayDimensions == ((JavaType) obj).getArray() + && ((JavaType) obj).getParameters().containsAll(parameters); + } + + public JavaSymbolName getArgName() { + return argName; + } + + public int getArray() { + return arrayDimensions; + } + + private String getArraySuffix() { + if (arrayDimensions == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arrayDimensions; i++) { + sb.append("[]"); + } + return sb.toString(); + } + + /** + * Returns this type's base type, being this for single-valued + * types, otherwise the element type for collection types. + * + * @return null for an untyped collection + * @since 1.2.1 + */ + public JavaType getBaseType() { + if (isCommonCollectionType()) { + if (parameters.isEmpty()) { + return null; + } + return parameters.get(0); + } + return this; + } + + public DataType getDataType() { + return dataType; + } + + /** + * @return the enclosing type, if any (will return null if there is no + * enclosing type) + */ + public JavaType getEnclosingType() { + return enclosingType; + } + + /** + * @return the fully qualified name (complies with the rules specified in + * the constructor) + */ + public String getFullyQualifiedTypeName() { + return fullyQualifiedTypeName; + } + + /** + * Obtains the name of this type, including type parameters. It will be + * formatted in a manner compatible with non-static use. No type name import + * resolution will take place. This is a side-effect free method. + * + * @return the type name, including parameters, as legal Java code (never + * null or empty) + */ + public String getNameIncludingTypeParameters() { + return getNameIncludingTypeParameters(false, null, + new HashMap()); + } + + /** + * Obtains the name of this type, including type parameters. It will be + * formatted in a manner compatible with either static or non-static usage, + * as per the passed argument. Type names will attempt to be resolved (and + * automatically registered) using the passed resolver. This method will + * have side-effects on the passed resolver. + * + * @param staticForm true if the output should be compatible with static use + * @param resolver the resolver to use (may be null in which case no import + * resolution will occur) + * @return the type name, including parameters, as legal Java code (never + * null or empty) + */ + public String getNameIncludingTypeParameters(final boolean staticForm, + final ImportRegistrationResolver resolver) { + return getNameIncludingTypeParameters(staticForm, resolver, + new HashMap()); + } + + private String getNameIncludingTypeParameters(final boolean staticForm, + final ImportRegistrationResolver resolver, + final Map types) { + if (DataType.PRIMITIVE == dataType) { + Validate.isTrue(parameters.isEmpty(), + "A primitive cannot have parameters"); + if (fullyQualifiedTypeName.equals(Integer.class.getName())) { + return "int" + getArraySuffix(); + } + else if (fullyQualifiedTypeName.equals(Character.class.getName())) { + return "char" + getArraySuffix(); + } + else if (fullyQualifiedTypeName.equals(Void.class.getName())) { + return "void"; + } + return StringUtils.uncapitalize(getSimpleTypeName() + + getArraySuffix()); + } + + final StringBuilder sb = new StringBuilder(); + + if (WILDCARD_EXTENDS.equals(argName)) { + sb.append("?"); + if (dataType == DataType.TYPE || !staticForm) { + sb.append(" extends "); + } + else if (types.containsKey(fullyQualifiedTypeName)) { + sb.append(" extends ") + .append(types.get(fullyQualifiedTypeName)); + } + } + else if (WILDCARD_SUPER.equals(argName)) { + sb.append("?"); + if (dataType == DataType.TYPE || !staticForm) { + sb.append(" super "); + } + else if (types.containsKey(fullyQualifiedTypeName)) { + sb.append(" extends ") + .append(types.get(fullyQualifiedTypeName)); + } + } + else if (WILDCARD_NEITHER.equals(argName)) { + sb.append("?"); + } + else if (argName != null && !staticForm) { + sb.append(argName); + if (dataType == DataType.TYPE) { + if (!fullyQualifiedTypeName.equals(OBJECT + .getFullyQualifiedTypeName())) { + sb.append(" extends "); + } + } + } + + if (!WILDCARD_NEITHER.equals(argName)) { + // It wasn't a WILDCARD_NEITHER, so we might need to continue with + // more details + if (dataType == DataType.TYPE || !staticForm) { + if (resolver != null) { + if (resolver + .isFullyQualifiedFormRequiredAfterAutoImport(this)) { + sb.append(fullyQualifiedTypeName); + } + else { + sb.append(getSimpleTypeName()); + } + } + else { + if (fullyQualifiedTypeName.equals(OBJECT + .getFullyQualifiedTypeName())) { + // It's Object, so we need to only append if this isn't + // a type arg + if (argName == null) { + sb.append(fullyQualifiedTypeName); + } + } + else { + // It's ok to just append it as it's not Object + sb.append(fullyQualifiedTypeName); + } + } + } + + if (parameters.size() > 0 + && (dataType == DataType.TYPE || !staticForm)) { + sb.append("<"); + int counter = 0; + for (final JavaType param : parameters) { + counter++; + if (counter > 1) { + sb.append(", "); + } + sb.append(param.getNameIncludingTypeParameters(staticForm, + resolver, types)); + counter++; + } + sb.append(">"); + } + + sb.append(getArraySuffix()); + } + + if (argName != null && !argName.equals(WILDCARD_EXTENDS) + && !argName.equals(WILDCARD_SUPER) + && !argName.equals(WILDCARD_NEITHER)) { + types.put(argName.getSymbolName(), sb.toString()); + } + + return sb.toString(); + } + + /** + * @return the package name (never null) + */ + public JavaPackage getPackage() { + if (isDefaultPackage() + && !Character.isUpperCase(fullyQualifiedTypeName.charAt(0))) { + return new JavaPackage(""); + } + + if (enclosingType != null) { + final String enclosingTypeFullyQualifiedTypeName = enclosingType + .getFullyQualifiedTypeName(); + final int offset = enclosingTypeFullyQualifiedTypeName + .lastIndexOf("."); + // Handle case where the package name after the last period starts + // with a capital letter. + if (offset > -1 + && Character + .isUpperCase(enclosingTypeFullyQualifiedTypeName + .charAt(offset + 1))) { + return new JavaPackage(enclosingTypeFullyQualifiedTypeName); + } + return enclosingType.getPackage(); + } + + final int offset = fullyQualifiedTypeName.lastIndexOf("."); + return offset == -1 ? new JavaPackage("") : new JavaPackage( + fullyQualifiedTypeName.substring(0, offset)); + } + + public List getParameters() { + return Collections.unmodifiableList(parameters); + } + + /** + * Returns the name of the source file that contains this type, starting + * from its base package. For example, for a type called "com.example.Foo", + * this method returns "com/example/Foo.java", delimited by the platform- + * specific separator ("/" in this example). + * + * @return a non-blank path + */ + public String getRelativeFileName() { + return fullyQualifiedTypeName.replace('.', File.separatorChar) + + ".java"; + } + + /** + * @return the name (does not contain any periods; never null or empty) + */ + public String getSimpleTypeName() { + return simpleTypeName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime + * result + + (fullyQualifiedTypeName == null ? 0 : fullyQualifiedTypeName + .hashCode()); + result = prime * result + (dataType == null ? 0 : dataType.hashCode()); + result = prime * result + arrayDimensions; + return result; + } + + public boolean isArray() { + return arrayDimensions > 0; + } + + /** + * Indicates whether this type is any kind of boolean. + * + * @return see above + * @since 1.2.1 + */ + public boolean isBoolean() { + return equals(BOOLEAN_OBJECT) || equals(BOOLEAN_PRIMITIVE); + } + + public boolean isCommonCollectionType() { + return COMMON_COLLECTION_TYPES.contains(fullyQualifiedTypeName); + } + + /** + * Indicates whether this type is part of core Java. + * + * @return see above + */ + public boolean isCoreType() { + for (final String coreTypePrefix : CORE_TYPE_PREFIXES) { + if (fullyQualifiedTypeName.startsWith(coreTypePrefix)) { + return true; + } + } + return false; + } + + public boolean isDefaultPackage() { + return defaultPackage; + } + + /** + * Indicates whether a field or variable of this type can contain multiple + * values + * + * @return see above + * @since 1.2.0 + */ + public boolean isMultiValued() { + return isCommonCollectionType() || isArray(); + } + + /** + * Indicates whether this type is a primitive, or in the case of an array, + * whether its elements are primitive. + * + * @return see above + */ + public boolean isPrimitive() { + return DataType.PRIMITIVE == dataType; + } + + @Override + public String toString() { + return getNameIncludingTypeParameters(); + } +} diff --git a/model/src/main/java/org/springframework/roo/model/JdkJavaType.java b/model/src/main/java/org/springframework/roo/model/JdkJavaType.java new file mode 100644 index 000000000..9bfccbdd0 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/JdkJavaType.java @@ -0,0 +1,266 @@ +package org.springframework.roo.model; + +import static org.springframework.roo.model.JavaType.DOUBLE_OBJECT; +import static org.springframework.roo.model.JavaType.DOUBLE_PRIMITIVE; +import static org.springframework.roo.model.JavaType.FLOAT_OBJECT; +import static org.springframework.roo.model.JavaType.FLOAT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; +import static org.springframework.roo.model.JavaType.LONG_OBJECT; +import static org.springframework.roo.model.JavaType.LONG_PRIMITIVE; +import static org.springframework.roo.model.JavaType.SHORT_OBJECT; +import static org.springframework.roo.model.JavaType.SHORT_PRIMITIVE; + +import java.beans.PropertyEditorSupport; +import java.io.ByteArrayInputStream; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Ref; +import java.sql.Struct; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.apache.commons.lang3.Validate; + +/** + * Constants for JDK {@link JavaType}s. Use them in preference to creating new + * instances of these types. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public final class JdkJavaType { + + // java.sql + public static final JavaType ARRAY = new JavaType(Array.class); + // java.util + public static final JavaType ARRAY_LIST = new JavaType(ArrayList.class); + + public static final JavaType ARRAYS = new JavaType(Arrays.class); + + // java.math + public static final JavaType BIG_DECIMAL = new JavaType(BigDecimal.class); + public static final JavaType BIG_INTEGER = new JavaType(BigInteger.class); + public static final JavaType BLOB = new JavaType(Blob.class); + + // java.io + public static final JavaType BYTE_ARRAY_INPUT_STREAM = new JavaType( + ByteArrayInputStream.class); + public static final JavaType CALENDAR = new JavaType(Calendar.class); + + public static final JavaType CLOB = new JavaType(Clob.class); + public static final JavaType COLLECTION = new JavaType(Collection.class); + + public static final JavaType DATE = new JavaType(Date.class); + + // java.text + public static final JavaType DATE_FORMAT = new JavaType(DateFormat.class); + // java.lang + public static final JavaType EXCEPTION = new JavaType(Exception.class); + public static final JavaType GREGORIAN_CALENDAR = new JavaType( + GregorianCalendar.class); + public static final JavaType HASH_SET = new JavaType(HashSet.class); + public static final JavaType ITERATOR = new JavaType(Iterator.class); + + private static final List javaLangSimpleTypeNames = new ArrayList(); + private static final List javaLangTypes = new ArrayList(); + + public static final JavaType LIST = new JavaType(List.class); + public static final JavaType MAP = new JavaType(Map.class); + // javax.annotation + public static final JavaType POST_CONSTRUCT = new JavaType( + PostConstruct.class); + // java.beans + public static final JavaType PROPERTY_EDITOR_SUPPORT = new JavaType( + PropertyEditorSupport.class); + public static final JavaType RANDOM = new JavaType(Random.class); + public static final JavaType REF = new JavaType(Ref.class); + // java.security + public static final JavaType SECURE_RANDOM = new JavaType( + SecureRandom.class); + public static final JavaType SERIALIZABLE = new JavaType(Serializable.class); + public static final JavaType SET = new JavaType(Set.class); + public static final JavaType SIMPLE_DATE_FORMAT = new JavaType( + SimpleDateFormat.class); + public static final JavaType STRUCT = new JavaType(Struct.class); + public static final JavaType SUPPRESS_WARNINGS = new JavaType( + SuppressWarnings.class); + // java.sql + public static final JavaType TIMESTAMP = new JavaType(Timestamp.class); + + public static final JavaType UNSUPPORTED_ENCODING_EXCEPTION = new JavaType( + UnsupportedEncodingException.class); + + // Static methods + + static { + javaLangSimpleTypeNames.add("Appendable"); + javaLangSimpleTypeNames.add("CharSequence"); + javaLangSimpleTypeNames.add("Cloneable"); + javaLangSimpleTypeNames.add("Comparable"); + javaLangSimpleTypeNames.add("Iterable"); + javaLangSimpleTypeNames.add("Readable"); + javaLangSimpleTypeNames.add("Runnable"); + javaLangSimpleTypeNames.add("Boolean"); + javaLangSimpleTypeNames.add("Byte"); + javaLangSimpleTypeNames.add("Character"); + javaLangSimpleTypeNames.add("Class"); + javaLangSimpleTypeNames.add("ClassLoader"); + javaLangSimpleTypeNames.add("Compiler"); + javaLangSimpleTypeNames.add("Double"); + javaLangSimpleTypeNames.add("Enum"); + javaLangSimpleTypeNames.add("Float"); + javaLangSimpleTypeNames.add("InheritableThreadLocal"); + javaLangSimpleTypeNames.add("Integer"); + javaLangSimpleTypeNames.add("Long"); + javaLangSimpleTypeNames.add("Math"); + javaLangSimpleTypeNames.add("Number"); + javaLangSimpleTypeNames.add("Object"); + javaLangSimpleTypeNames.add("Package"); + javaLangSimpleTypeNames.add("Process"); + javaLangSimpleTypeNames.add("ProcessBuilder"); + javaLangSimpleTypeNames.add("Runtime"); + javaLangSimpleTypeNames.add("RuntimePermission"); + javaLangSimpleTypeNames.add("SecurityManager"); + javaLangSimpleTypeNames.add("Short"); + javaLangSimpleTypeNames.add("StackTraceElement"); + javaLangSimpleTypeNames.add("StrictMath"); + javaLangSimpleTypeNames.add("String"); + javaLangSimpleTypeNames.add("StringBuilder"); + javaLangSimpleTypeNames.add("StringBuilder"); + javaLangSimpleTypeNames.add("System"); + javaLangSimpleTypeNames.add("Thread"); + javaLangSimpleTypeNames.add("ThreadGroup"); + javaLangSimpleTypeNames.add("ThreadLocal"); + javaLangSimpleTypeNames.add("Throwable"); + javaLangSimpleTypeNames.add("Void"); + javaLangSimpleTypeNames.add("ArithmeticException"); + javaLangSimpleTypeNames.add("ArrayIndexOutOfBoundsException"); + javaLangSimpleTypeNames.add("ArrayStoreException"); + javaLangSimpleTypeNames.add("ClassCastException"); + javaLangSimpleTypeNames.add("ClassNotFoundException"); + javaLangSimpleTypeNames.add("CloneNotSupportedException"); + javaLangSimpleTypeNames.add("EnumConstantNotPresentException"); + javaLangSimpleTypeNames.add("Exception"); + javaLangSimpleTypeNames.add("IllegalAccessException"); + javaLangSimpleTypeNames.add("IllegalArgumentException"); + javaLangSimpleTypeNames.add("IllegalMonitorStateException"); + javaLangSimpleTypeNames.add("IllegalStateException"); + javaLangSimpleTypeNames.add("IllegalThreadStateException"); + javaLangSimpleTypeNames.add("IndexOutOfBoundsException"); + javaLangSimpleTypeNames.add("InstantiationException"); + javaLangSimpleTypeNames.add("InterruptedException"); + javaLangSimpleTypeNames.add("NegativeArraySizeException"); + javaLangSimpleTypeNames.add("NoSuchFieldException"); + javaLangSimpleTypeNames.add("NoSuchMethodException"); + javaLangSimpleTypeNames.add("NullPointerException"); + javaLangSimpleTypeNames.add("NumberFormatException"); + javaLangSimpleTypeNames.add("RuntimeException"); + javaLangSimpleTypeNames.add("SecurityException"); + javaLangSimpleTypeNames.add("StringIndexOutOfBoundsException"); + javaLangSimpleTypeNames.add("TypeNotPresentException"); + javaLangSimpleTypeNames.add("UnsupportedOperationException"); + javaLangSimpleTypeNames.add("AbstractMethodError"); + javaLangSimpleTypeNames.add("AssertionError"); + javaLangSimpleTypeNames.add("ClassCircularityError"); + javaLangSimpleTypeNames.add("ClassFormatError"); + javaLangSimpleTypeNames.add("Error"); + javaLangSimpleTypeNames.add("ExceptionInInitializerError"); + javaLangSimpleTypeNames.add("IllegalAccessError"); + javaLangSimpleTypeNames.add("IncompatibleClassChangeError"); + javaLangSimpleTypeNames.add("InstantiationError"); + javaLangSimpleTypeNames.add("InternalError"); + javaLangSimpleTypeNames.add("LinkageError"); + javaLangSimpleTypeNames.add("NoClassDefFoundError"); + javaLangSimpleTypeNames.add("NoSuchFieldError"); + javaLangSimpleTypeNames.add("NoSuchMethodError"); + javaLangSimpleTypeNames.add("OutOfMemoryError"); + javaLangSimpleTypeNames.add("StackOverflowError"); + javaLangSimpleTypeNames.add("ThreadDeath"); + javaLangSimpleTypeNames.add("UnknownError"); + javaLangSimpleTypeNames.add("UnsatisfiedLinkError"); + javaLangSimpleTypeNames.add("UnsupportedClassVersionError"); + javaLangSimpleTypeNames.add("VerifyError"); + javaLangSimpleTypeNames.add("VirtualMachineError"); + } + + public static boolean isDateField(final JavaType javaType) { + return javaType.equals(DATE) || javaType.equals(CALENDAR); + } + + public static boolean isDecimalType(final JavaType javaType) { + return javaType.equals(BIG_DECIMAL) || isDoubleOrFloat(javaType); + } + + public static boolean isDoubleOrFloat(final JavaType javaType) { + return javaType.equals(DOUBLE_OBJECT) + || javaType.equals(DOUBLE_PRIMITIVE) + || javaType.equals(FLOAT_OBJECT) + || javaType.equals(FLOAT_PRIMITIVE); + } + + public static boolean isIntegerType(final JavaType javaType) { + return javaType.equals(BIG_INTEGER) || javaType.equals(INT_PRIMITIVE) + || javaType.equals(INT_OBJECT) + || javaType.equals(LONG_PRIMITIVE) + || javaType.equals(LONG_OBJECT) + || javaType.equals(SHORT_PRIMITIVE) + || javaType.equals(SHORT_OBJECT); + } + + /** + * Determines whether the presented java type is in the java.lang package or + * not. + * + * @param javaType the Java type (required) + * @return whether the type is declared as part of java.lang + */ + public static boolean isPartOfJavaLang(final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + if (javaLangTypes.isEmpty()) { + for (final String javaLangSimpleTypeName : javaLangSimpleTypeNames) { + javaLangTypes.add("java.lang." + javaLangSimpleTypeName); + } + } + return javaLangTypes.contains(javaType.getFullyQualifiedTypeName()); + } + + /** + * Determines whether the presented simple type name is part of java.lang or + * not. + * + * @param simpleTypeName the simple type name (required) + * @return whether the type is declared as part of java.lang + */ + public static boolean isPartOfJavaLang(final String simpleTypeName) { + Validate.notBlank(simpleTypeName, "Simple type name required"); + return javaLangSimpleTypeNames.contains(simpleTypeName); + } + + /** + * Constructor is private to prevent instantiation + */ + private JdkJavaType() { + } +} \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/JpaJavaType.java b/model/src/main/java/org/springframework/roo/model/JpaJavaType.java new file mode 100644 index 000000000..3afa9b60f --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/JpaJavaType.java @@ -0,0 +1,91 @@ +package org.springframework.roo.model; + +/** + * Constants for javax.persistence {@link JavaType}s. Use them in preference to + * creating new instances of these types. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public final class JpaJavaType { + + // javax.persistence + public static final JavaType CASCADE_TYPE = new JavaType( + "javax.persistence.CascadeType"); + public static final JavaType COLUMN = new JavaType( + "javax.persistence.Column"); + public static final JavaType DISCRIMINATOR_COLUMN = new JavaType( + "javax.persistence.DiscriminatorColumn"); + public static final JavaType ELEMENT_COLLECTION = new JavaType( + "javax.persistence.ElementCollection"); + public static final JavaType EMBEDDABLE = new JavaType( + "javax.persistence.Embeddable"); + public static final JavaType EMBEDDED = new JavaType( + "javax.persistence.Embedded"); + public static final JavaType EMBEDDED_ID = new JavaType( + "javax.persistence.EmbeddedId"); + public static final JavaType ENTITY = new JavaType( + "javax.persistence.Entity"); + public static final JavaType ENTITY_MANAGER = new JavaType( + "javax.persistence.EntityManager"); + public static final JavaType ENUM_TYPE = new JavaType( + "javax.persistence.EnumType"); + public static final JavaType ENUMERATED = new JavaType( + "javax.persistence.Enumerated"); + public static final JavaType FETCH_TYPE = new JavaType( + "javax.persistence.FetchType"); + public static final JavaType GENERATED_VALUE = new JavaType( + "javax.persistence.GeneratedValue"); + public static final JavaType GENERATION_TYPE = new JavaType( + "javax.persistence.GenerationType"); + public static final JavaType ID = new JavaType("javax.persistence.Id"); + public static final JavaType INHERITANCE = new JavaType( + "javax.persistence.Inheritance"); + public static final JavaType INHERITANCE_TYPE = new JavaType( + "javax.persistence.InheritanceType"); + public static final JavaType JOIN_COLUMN = new JavaType( + "javax.persistence.JoinColumn"); + public static final JavaType JOIN_COLUMNS = new JavaType( + "javax.persistence.JoinColumns"); + public static final JavaType JOIN_TABLE = new JavaType( + "javax.persistence.JoinTable"); + public static final JavaType LOB = new JavaType("javax.persistence.Lob"); + public static final JavaType MANY_TO_MANY = new JavaType( + "javax.persistence.ManyToMany"); + public static final JavaType MANY_TO_ONE = new JavaType( + "javax.persistence.ManyToOne"); + public static final JavaType MAPPED_SUPERCLASS = new JavaType( + "javax.persistence.MappedSuperclass"); + public static final JavaType ONE_TO_MANY = new JavaType( + "javax.persistence.OneToMany"); + public static final JavaType ONE_TO_ONE = new JavaType( + "javax.persistence.OneToOne"); + public static final JavaType PERSISTENCE_CONTEXT = new JavaType( + "javax.persistence.PersistenceContext"); + public static final JavaType POST_PERSIST = new JavaType( + "javax.persistence.PostPersist"); + public static final JavaType POST_UPDATE = new JavaType( + "javax.persistence.PostUpdate"); + public static final JavaType PRE_REMOVE = new JavaType( + "javax.persistence.PreRemove"); + public static final JavaType QUERY = new JavaType("javax.persistence.Query"); + public static final JavaType SEQUENCE_GENERATOR = new JavaType( + "javax.persistence.SequenceGenerator"); + public static final JavaType TABLE = new JavaType("javax.persistence.Table"); + public static final JavaType TEMPORAL = new JavaType( + "javax.persistence.Temporal"); + public static final JavaType TEMPORAL_TYPE = new JavaType( + "javax.persistence.TemporalType"); + public static final JavaType TRANSIENT = new JavaType( + "javax.persistence.Transient"); + public static final JavaType TYPED_QUERY = new JavaType( + "javax.persistence.TypedQuery"); + public static final JavaType VERSION = new JavaType( + "javax.persistence.Version"); + + /** + * Constructor is private to prevent instantiation + */ + private JpaJavaType() { + } +} \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/Jsr303JavaType.java b/model/src/main/java/org/springframework/roo/model/Jsr303JavaType.java new file mode 100644 index 000000000..44116a27e --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/Jsr303JavaType.java @@ -0,0 +1,49 @@ +package org.springframework.roo.model; + +/** + * Constants for JSR303-specific {@link JavaType}s. Use them in preference to + * creating new instances of these types. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public final class Jsr303JavaType { + + public static final JavaType ASSERT_FALSE = new JavaType( + "javax.validation.constraints.AssertFalse"); + public static final JavaType ASSERT_TRUE = new JavaType( + "javax.validation.constraints.AssertTrue"); + public static final JavaType CONSTRAINT_VIOLATION = new JavaType( + "javax.validation.ConstraintViolation"); + public static final JavaType CONSTRAINT_VIOLATION_EXCEPTION = new JavaType( + "javax.validation.ConstraintViolationException"); + public static final JavaType DECIMAL_MAX = new JavaType( + "javax.validation.constraints.DecimalMax"); + public static final JavaType DECIMAL_MIN = new JavaType( + "javax.validation.constraints.DecimalMin"); + public static final JavaType DIGITS = new JavaType( + "javax.validation.constraints.Digits"); + public static final JavaType FUTURE = new JavaType( + "javax.validation.constraints.Future"); + public static final JavaType MAX = new JavaType( + "javax.validation.constraints.Max"); + public static final JavaType MIN = new JavaType( + "javax.validation.constraints.Min"); + public static final JavaType NOT_NULL = new JavaType( + "javax.validation.constraints.NotNull"); + public static final JavaType NULL = new JavaType( + "javax.validation.constraints.Null"); + public static final JavaType PAST = new JavaType( + "javax.validation.constraints.Past"); + public static final JavaType PATTERN = new JavaType( + "javax.validation.constraints.Pattern"); + public static final JavaType SIZE = new JavaType( + "javax.validation.constraints.Size"); + public static final JavaType VALID = new JavaType("javax.validation.Valid"); + + /** + * Constructor is private to prevent instantiation + */ + private Jsr303JavaType() { + } +} \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/ReservedWords.java b/model/src/main/java/org/springframework/roo/model/ReservedWords.java new file mode 100644 index 000000000..24679e67f --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/ReservedWords.java @@ -0,0 +1,187 @@ +package org.springframework.roo.model; + +import java.util.Collections; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * Provides all reserved words. + * + * @author Ben Alex + */ +public final class ReservedWords { + + private static final String[] JAVA_KEYWORDS = { "abstract", "assert", + "boolean", "break", "byte", "case", "catch", "char", "class", + "const", "continue", "default", "do", "double", "else", "enum", + "extends", "false", "final", "finally", "float", "for", "goto", + "if", "implements", "import", "instanceof", "int", "interface", + "long", "native", "new", "null", "package", "private", "protected", + "public", "return", "short", "static", "strictfp", "super", + "switch", "synchronized", "this", "throw", "throws", "transient", + "true", "try", "void", "volatile", "while" }; + + private final static String[] SQL_KEYWORDS = { "ABSOLUTE", "ACTION", "ADD", + "AFTER", "ALL", "ALLOCATE", "ALTER", "AND", "ANY", "ARE", "ARRAY", + "AS", "ASC", "ASENSITIVE", "ASSERTION", "ASYMMETRIC", "AT", + "ATOMIC", "AUTHORIZATION", "AVG", "BEFORE", "BEGIN", "BETWEEN", + "BIGINT", "BINARY", "BIT", "BIT_LENGTH", "BLOB", "BOOLEAN", "BOTH", + "BREADTH", "BY", "CALL", "CALLED", "CASCADE", "CASCADED", "CASE", + "CAST", "CATALOG", "CHAR", "CHARACTER", "CHARACTER_LENGTH", + "CHAR_LENGTH", "CHECK", "CLOB", "CLOSE", "COALESCE", "COLLATE", + "COLLATION", "COLUMN", "COMMIT", "CONDITION", "CONNECT", + "CONNECTION", "CONSTRAINT", "CONSTRAINTS", "CONSTRUCTOR", + "CONTAINS", "CONTINUE", "CONVERT", "CORRESPONDING", "COUNT", + "CREATE", "CROSS", "CUBE", "CURRENT", "CURRENT_DATE", + "CURRENT_DEFAULT_TRANSFORM_GROUP", "CURRENT_PATH", "CURRENT_ROLE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", + "CURRENT_TRANSFORM_GROUP_FOR_TYPE", "CURRENT_USER", "CURSOR", + "CYCLE", "DATA", "DATE", "DAY", "DEALLOCATE", "DEC", "DECIMAL", + "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DEPTH", + "DEREF", "DESC", "DESCRIBE", "DESCRIPTOR", "DETERMINISTIC", + "DIAGNOSTICS", "DISCONNECT", "DISTINCT", "DO", "DOMAIN", "DOUBLE", + "DROP", "DYNAMIC", "EACH", "ELEMENT", "ELSE", "ELSEIF", "END", + "EQUALS", "ESCAPE", "EXCEPT", "EXCEPTION", "EXEC", "EXECUTE", + "EXISTS", "EXIT", "EXTERNAL", "EXTRACT", "FALSE", "FETCH", + "FILTER", "FIRST", "FLOAT", "FOR", "FOREIGN", "FOUND", "FREE", + "FROM", "FULL", "FUNCTION", "GENERAL", "GET", "GLOBAL", "GO", + "GOTO", "GRANT", "GROUP", "GROUPING", "HANDLER", "HAVING", "HOLD", + "HOUR", "IDENTITY", "IF", "IMMEDIATE", "IN", "INDEX", "INDICATOR", + "INITIALLY", "INNER", "INOUT", "INPUT", "INSENSITIVE", "INSERT", + "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "IS", + "ISOLATION", "ITERATE", "JOIN", "KEY", "LANGUAGE", "LARGE", "LAST", + "LATERAL", "LEADING", "LEAVE", "LEFT", "LEVEL", "LIKE", "LOCAL", + "LOCALTIME", "LOCALTIMESTAMP", "LOCATOR", "LOOP", "LOWER", "MAP", + "MATCH", "MAX", "MEMBER", "MERGE", "METHOD", "MIN", "MINUTE", + "MODIFIES", "MODULE", "MONTH", "MULTISET", "NAMES", "NATIONAL", + "NATURAL", "NCHAR", "NCLOB", "NEW", "NEXT", "NO", "NONE", "NOT", + "NULL", "NULLIF", "NUMBER", "NUMERIC", "OBJECT", "OCTET_LENGTH", + "OF", "OLD", "ON", "ONLY", "OPEN", "OPTION", "OR", "ORDER", + "ORDINALITY", "OUT", "OUTER", "OUTPUT", "OVER", "OVERLAPS", "PAD", + "PARAMETER", "PARTIAL", "PARTITION", "PATH", "POSITION", + "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", + "PRIVILEGES", "PROCEDURE", "PUBLIC", "RANGE", "READ", "READS", + "REAL", "RECURSIVE", "REF", "REFERENCES", "REFERENCING", + "RELATIVE", "RELEASE", "REPEAT", "RESIGNAL", "RESTRICT", "RESULT", + "RETURN", "RETURNS", "REVOKE", "RIGHT", "ROLE", "ROLLBACK", + "ROLLUP", "ROUTINE", "ROW", "ROWS", "SAVEPOINT", "SCHEMA", "SCOPE", + "SCROLL", "SEARCH", "SECOND", "SECTION", "SELECT", "SENSITIVE", + "SESSION", "SESSION_USER", "SET", "SETS", "SIGNAL", "SIMILAR", + "SIZE", "SMALLINT", "SOME", "SPACE", "SPECIFIC", "SPECIFICTYPE", + "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", + "SQLWARNING", "START", "STATE", "STATIC", "SUBMULTISET", + "SUBSTRING", "SUM", "SYMMETRIC", "SYSTEM", "SYSTEM_USER", "TABLE", + "TABLESAMPLE", "TEMPORARY", "THEN", "TIME", "TIMESTAMP", + "TIMEZONE_HOUR", "TIMEZONE_MINUTE", "TO", "TRAILING", + "TRANSACTION", "TRANSLATE", "TRANSLATION", "TREAT", "TRIGGER", + "TRIM", "TRUE", "UNDER", "UNDO", "UNION", "UNIQUE", "UNKNOWN", + "UNNEST", "UNTIL", "UPDATE", "UPPER", "USAGE", "USER", "USING", + "VALUE", "VALUES", "VARCHAR", "VARYING", "VIEW", "WHEN", + "WHENEVER", "WHERE", "WHILE", "WINDOW", "WITH", "WITHIN", + "WITHOUT", "WORK", "WRITE", "YEAR", "ZONE" }; + + /** + * Represents an unmodifiable set of lowercase reserved words in Java. + */ + public static final Set RESERVED_JAVA_KEYWORDS = arrayToSet(JAVA_KEYWORDS); + + /** + * Represents an unmodifiable set of lowercase reserved words in SQL. + */ + public static final Set RESERVED_SQL_KEYWORDS = arrayToSet(SQL_KEYWORDS); + + public static void verifyReservedJavaKeywordsNotPresent( + final JavaSymbolName javaSymbolName) { + Validate.notNull(javaSymbolName, "Java symbol required"); + if (RESERVED_JAVA_KEYWORDS.contains(javaSymbolName.getSymbolName())) { + throw new IllegalStateException("Reserved Java keyword '" + + javaSymbolName.getSymbolName() + + "' is not permitted as symbol name"); + } + } + + public static void verifyReservedJavaKeywordsNotPresent( + final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + for (final String s : javaType.getFullyQualifiedTypeName().split("\\.")) { + if (RESERVED_JAVA_KEYWORDS.contains(s)) { + throw new IllegalStateException("Reserved Java keyword '" + s + + "' is not permitted within fully qualified type name"); + } + } + } + + public static void verifyReservedJavaKeywordsNotPresent(final String string) { + Validate.notNull(string, "String required"); + if (RESERVED_JAVA_KEYWORDS.contains(string.toLowerCase())) { + throw new IllegalStateException("Reserved Java keyword '" + + string.toLowerCase() + "' is not permitted"); + } + } + + public static void verifyReservedSqlKeywordsNotPresent( + final JavaSymbolName javaSymbolName) { + Validate.notNull(javaSymbolName, "Java symbol required"); + if (RESERVED_SQL_KEYWORDS.contains(javaSymbolName.getSymbolName() + .toLowerCase())) { + throw new IllegalStateException("Reserved SQL keyword '" + + javaSymbolName.getSymbolName() + + "' is not permitted as symbol name"); + } + } + + public static void verifyReservedSqlKeywordsNotPresent( + final JavaType javaType) { + Validate.notNull(javaType, "Java type required"); + if (RESERVED_SQL_KEYWORDS.contains(javaType.getSimpleTypeName() + .toLowerCase())) { + throw new IllegalStateException("Reserved SQL keyword '" + + javaType.getSimpleTypeName() + + "' is not permitted as simple type name"); + } + } + + public static void verifyReservedSqlKeywordsNotPresent(final String string) { + Validate.notNull(string, "String required"); + if (RESERVED_JAVA_KEYWORDS.contains(string.toLowerCase())) { + throw new IllegalStateException("Reserved SQL keyword '" + + string.toLowerCase() + "' is not permitted"); + } + } + + public static void verifyReservedWordsNotPresent( + final JavaSymbolName javaSymbolName) { + verifyReservedJavaKeywordsNotPresent(javaSymbolName); + verifyReservedSqlKeywordsNotPresent(javaSymbolName); + } + + public static void verifyReservedWordsNotPresent(final JavaType javaType) { + verifyReservedJavaKeywordsNotPresent(javaType); + verifyReservedSqlKeywordsNotPresent(javaType); + } + + public static void verifyReservedWordsNotPresent(final String string) { + verifyReservedJavaKeywordsNotPresent(string); + verifyReservedSqlKeywordsNotPresent(string); + } + + private static Set arrayToSet(String... tokens) { + SortedSet setOfTokens = new TreeSet(); + for (String token : tokens) { + if (StringUtils.isNotBlank(token)) { + setOfTokens.add(token.toLowerCase()); + } + } + return Collections.unmodifiableSet(setOfTokens); + } + + /** + * Constructor is private to prevent instantiation + */ + private ReservedWords() { + } +} diff --git a/model/src/main/java/org/springframework/roo/model/RooJavaType.java b/model/src/main/java/org/springframework/roo/model/RooJavaType.java new file mode 100644 index 000000000..a7d07ce4b --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/RooJavaType.java @@ -0,0 +1,93 @@ +package org.springframework.roo.model; + +/** + * Constants for Roo-specific {@link JavaType}s. Use them in preference to + * creating new instances of these types. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class RooJavaType { + + // org.springframework.roo.addon + public static final JavaType ROO_CONFIGURABLE = new JavaType( + "org.springframework.roo.addon.configurable.RooConfigurable"); + public static final JavaType ROO_CONVERSION_SERVICE = new JavaType( + "org.springframework.roo.addon.web.mvc.controller.converter.RooConversionService"); + public static final JavaType ROO_DATA_ON_DEMAND = new JavaType( + "org.springframework.roo.addon.dod.RooDataOnDemand"); + public static final JavaType ROO_DB_MANAGED = new JavaType( + "org.springframework.roo.addon.dbre.RooDbManaged"); + public static final JavaType ROO_EDITOR = new JavaType( + "org.springframework.roo.addon.property.editor.RooEditor"); + public static final JavaType ROO_EQUALS = new JavaType( + "org.springframework.roo.addon.equals.RooEquals"); + public static final JavaType ROO_GWT_LOCATOR = new JavaType( + "org.springframework.roo.addon.gwt.RooGwtLocator"); + public static final JavaType ROO_GWT_MIRRORED_FROM = new JavaType( + "org.springframework.roo.addon.gwt.RooGwtMirroredFrom"); + public static final JavaType ROO_GWT_PROXY = new JavaType( + "org.springframework.roo.addon.gwt.RooGwtProxy"); + public static final JavaType ROO_GWT_REQUEST = new JavaType( + "org.springframework.roo.addon.gwt.RooGwtRequest"); + public static final JavaType ROO_GWT_UNMANAGED_REQUEST = new JavaType( + "org.springframework.roo.addon.gwt.RooGwtUnmanagedRequest"); + public static final JavaType ROO_IDENTIFIER = new JavaType( + "org.springframework.roo.addon.jpa.identifier.RooIdentifier"); + public static final JavaType ROO_INTEGRATION_TEST = new JavaType( + "org.springframework.roo.addon.test.RooIntegrationTest"); + public static final JavaType ROO_JAVA_BEAN = new JavaType( + "org.springframework.roo.addon.javabean.RooJavaBean"); + public static final JavaType ROO_JPA_ACTIVE_RECORD = new JavaType( + "org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord"); + public static final JavaType ROO_JPA_ENTITY = new JavaType( + "org.springframework.roo.addon.jpa.entity.RooJpaEntity"); + public static final JavaType ROO_JSF_APPLICATION_BEAN = new JavaType( + "org.springframework.roo.addon.jsf.application.RooJsfApplicationBean"); + public static final JavaType ROO_JSF_CONVERTER = new JavaType( + "org.springframework.roo.addon.jsf.converter.RooJsfConverter"); + public static final JavaType ROO_JSF_MANAGED_BEAN = new JavaType( + "org.springframework.roo.addon.jsf.managedbean.RooJsfManagedBean"); + public static final JavaType ROO_JSON = new JavaType( + "org.springframework.roo.addon.json.RooJson"); + public static final JavaType ROO_MONGO_ENTITY = new JavaType( + "org.springframework.roo.addon.layers.repository.mongo.RooMongoEntity"); + public static final JavaType ROO_NEO4J_ENTITY = new JavaType( + "org.springframework.roo.addon.layers.repository.neo4j.RooNeo4jEntity"); + public static final JavaType ROO_OP4J = new JavaType( + "org.springframework.roo.addon.op4j.RooOp4j"); + public static final JavaType ROO_PERMISSION_EVALUATOR = new JavaType( + "org.springframework.roo.addon.security.RooPermissionEvaluator"); + public static final JavaType ROO_PLURAL = new JavaType( + "org.springframework.roo.addon.plural.RooPlural"); + public static final JavaType ROO_REPOSITORY_JPA = new JavaType( + "org.springframework.roo.addon.layers.repository.jpa.RooJpaRepository"); + public static final JavaType ROO_REPOSITORY_MONGO = new JavaType( + "org.springframework.roo.addon.layers.repository.mongo.RooMongoRepository"); + public static final JavaType ROO_REPOSITORY_NEO4J = new JavaType( + "org.springframework.roo.addon.layers.repository.neo4j.RooNeo4jRepository"); + public static final JavaType ROO_SERIALIZABLE = new JavaType( + "org.springframework.roo.addon.serializable.RooSerializable"); + public static final JavaType ROO_SERVICE = new JavaType( + "org.springframework.roo.addon.layers.service.RooService"); + public static final JavaType ROO_SOLR_SEARCHABLE = new JavaType( + "org.springframework.roo.addon.solr.RooSolrSearchable"); + public static final JavaType ROO_SOLR_WEB_SEARCHABLE = new JavaType( + "org.springframework.roo.addon.solr.RooSolrWebSearchable"); + public static final JavaType ROO_TO_STRING = new JavaType( + "org.springframework.roo.addon.tostring.RooToString"); + public static final JavaType ROO_UPLOADED_FILE = new JavaType( + "org.springframework.roo.classpath.operations.jsr303.RooUploadedFile"); + public static final JavaType ROO_WEB_FINDER = new JavaType( + "org.springframework.roo.addon.web.mvc.controller.finder.RooWebFinder"); + public static final JavaType ROO_WEB_JSON = new JavaType( + "org.springframework.roo.addon.web.mvc.controller.json.RooWebJson"); + public static final JavaType ROO_WEB_SCAFFOLD = new JavaType( + "org.springframework.roo.addon.web.mvc.controller.scaffold.RooWebScaffold"); + + /** + * Constructor is private to prevent instantiation + */ + private RooJavaType() { + } +} \ No newline at end of file diff --git a/model/src/main/java/org/springframework/roo/model/SpringJavaType.java b/model/src/main/java/org/springframework/roo/model/SpringJavaType.java new file mode 100644 index 000000000..f57990de5 --- /dev/null +++ b/model/src/main/java/org/springframework/roo/model/SpringJavaType.java @@ -0,0 +1,147 @@ +package org.springframework.roo.model; + +import java.util.Arrays; + +import org.apache.commons.lang3.Validate; + +/** + * Constants for Spring-specific {@link JavaType}s. Use them in preference to + * creating new instances of these types. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class SpringJavaType { + + // org.springframework + public static final JavaType ASYNC = new JavaType( + "org.springframework.scheduling.annotation.Async"); + public static final JavaType AUTHENTICATION = new JavaType( + "org.springframework.security.core.Authentication"); + public static final JavaType AUTOWIRED = new JavaType( + "org.springframework.beans.factory.annotation.Autowired"); + public static final JavaType BINDING_RESULT = new JavaType( + "org.springframework.validation.BindingResult"); + public static final JavaType CHARACTER_ENCODING_FILTER = new JavaType( + "org.springframework.web.filter.CharacterEncodingFilter"); + public static final JavaType COMPONENT = new JavaType( + "org.springframework.stereotype.Component"); + public static final JavaType CONFIGURABLE = new JavaType( + "org.springframework.beans.factory.annotation.Configurable"); + public static final JavaType CONTEXT_CONFIGURATION = new JavaType( + "org.springframework.test.context.ContextConfiguration"); + public static final JavaType CONTEXT_LOADER_LISTENER = new JavaType( + "org.springframework.web.context.ContextLoaderListener"); + public static final JavaType CONTROLLER = new JavaType( + "org.springframework.stereotype.Controller"); + public static final JavaType CONVERSION_SERVICE = new JavaType( + "org.springframework.core.convert.ConversionService"); + public static final JavaType CONVERSION_SERVICE_EXPOSING_INTERCEPTOR = new JavaType( + "org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor"); + public static final JavaType DATA_ID = new JavaType( + "org.springframework.data.annotation.Id"); + public static final JavaType DATE_TIME_FORMAT = new JavaType( + "org.springframework.format.annotation.DateTimeFormat"); + public static final JavaType DISPATCHER_SERVLET = new JavaType( + "org.springframework.web.servlet.DispatcherServlet"); + public static final JavaType FLOW_HANDLER_MAPPING = new JavaType( + "org.springframework.webflow.mvc.servlet.FlowHandlerMapping"); + public static final JavaType FORMATTER_REGISTRY = new JavaType( + "org.springframework.format.FormatterRegistry"); + public static final JavaType HIDDEN_HTTP_METHOD_FILTER = new JavaType( + "org.springframework.web.filter.HiddenHttpMethodFilter"); + public static final JavaType HTTP_HEADERS = new JavaType( + "org.springframework.http.HttpHeaders"); + public static final JavaType HTTP_STATUS = new JavaType( + "org.springframework.http.HttpStatus"); + public static final JavaType JAVA_MAIL_SENDER_IMPL = new JavaType( + "org.springframework.mail.javamail.JavaMailSenderImpl"); + public static final JavaType JMS_OPERATIONS = new JavaType( + "org.springframework.jms.core.JmsOperations"); + public static final JavaType JMS_TEMPLATE = new JavaType( + "org.springframework.jms.core.JmsTemplate"); + public static final JavaType JPA_TRANSACTION_MANAGER = new JavaType( + "org.springframework.orm.jpa.JpaTransactionManager"); + public static final JavaType LOCAL_CONTAINER_ENTITY_MANAGER_FACTORY_BEAN = new JavaType( + "org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"); + public static final JavaType LOCAL_ENTITY_MANAGER_FACTORY_BEAN = new JavaType( + "org.springframework.orm.jpa.LocalEntityManagerFactoryBean"); + public static final JavaType LOCALE_CONTEXT_HOLDER = new JavaType( + "org.springframework.context.i18n.LocaleContextHolder"); + public static final JavaType MAIL_SENDER = new JavaType( + "org.springframework.mail.MailSender"); + public static final JavaType MOCK_STATIC_ENTITY_METHODS = new JavaType( + "org.springframework.mock.staticmock.MockStaticEntityMethods"); + public static final JavaType MODEL = new JavaType( + "org.springframework.ui.Model"); + public static final JavaType MODEL_ATTRIBUTE = new JavaType( + "org.springframework.web.bind.annotation.ModelAttribute"); + public static final JavaType MODEL_MAP = new JavaType( + "org.springframework.ui.ModelMap"); + public static final JavaType NUMBER_FORMAT = new JavaType( + "org.springframework.format.annotation.NumberFormat"); + public static final JavaType OPEN_ENTITY_MANAGER_IN_VIEW_FILTER = new JavaType( + "org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter"); + public static final JavaType PATH_VARIABLE = new JavaType( + "org.springframework.web.bind.annotation.PathVariable"); + public static final JavaType PERMISSION_EVALUATOR = new JavaType( + "org.springframework.security.access.PermissionEvaluator"); + public static final JavaType PERSISTENT = new JavaType( + "org.springframework.data.annotation.Persistent"); + public static final JavaType PRE_AUTHORIZE = new JavaType( + "org.springframework.security.access.prepost.PreAuthorize"); + public static final JavaType POST_AUTHORIZE = new JavaType( + "org.springframework.security.access.prepost.PostAuthorize"); + public static final JavaType PROPAGATION = new JavaType( + "org.springframework.transaction.annotation.Propagation"); + public static final JavaType REPOSITORY = new JavaType( + "org.springframework.stereotype.Repository"); + public static final JavaType REQUEST_BODY = new JavaType( + "org.springframework.web.bind.annotation.RequestBody"); + public static final JavaType REQUEST_MAPPING = new JavaType( + "org.springframework.web.bind.annotation.RequestMapping"); + public static final JavaType REQUEST_METHOD = new JavaType( + "org.springframework.web.bind.annotation.RequestMethod"); + public static final JavaType REQUEST_PARAM = new JavaType( + "org.springframework.web.bind.annotation.RequestParam"); + public static final JavaType RESPONSE_BODY = new JavaType( + "org.springframework.web.bind.annotation.ResponseBody"); + public static final JavaType RESPONSE_ENTITY = new JavaType( + "org.springframework.http.ResponseEntity"); + public static final JavaType SERVICE = new JavaType( + "org.springframework.stereotype.Service"); + public static final JavaType SIMPLE_MAIL_MESSAGE = new JavaType( + "org.springframework.mail.SimpleMailMessage"); + public static final JavaType SIMPLE_TYPE_CONVERTER = new JavaType( + "org.springframework.beans.SimpleTypeConverter"); + public static final JavaType TRANSACTIONAL = new JavaType( + "org.springframework.transaction.annotation.Transactional"); + public static final JavaType URI_UTILS = new JavaType( + "org.springframework.web.util.UriUtils"); + public static final JavaType VALUE = new JavaType( + "org.springframework.beans.factory.annotation.Value"); + public static final JavaType WEB_UTILS = new JavaType( + "org.springframework.web.util.WebUtils"); + + /** + * Returns the {@link JavaType} for a Spring converter + * + * @param fromType the type being converted from (required) + * @param toType the type being converted to (required) + * @return a non-null type + */ + public static JavaType getConverterType(final JavaType fromType, + final JavaType toType) { + Validate.notNull(fromType, "'From' type is required"); + Validate.notNull(toType, "'To' type is required"); + return new JavaType( + "org.springframework.core.convert.converter.Converter", 0, + DataType.TYPE, null, Arrays.asList(fromType, toType)); + } + + /** + * Constructor is private to prevent instantiation + */ + private SpringJavaType() { + } +} \ No newline at end of file diff --git a/model/src/test/java/org/springframework/roo/model/JavaPackageTest.java b/model/src/test/java/org/springframework/roo/model/JavaPackageTest.java new file mode 100644 index 000000000..43972a9e5 --- /dev/null +++ b/model/src/test/java/org/springframework/roo/model/JavaPackageTest.java @@ -0,0 +1,68 @@ +package org.springframework.roo.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +/** + * Unit test of {@link JavaPackage}. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JavaPackageTest { + + private static final JavaPackage CHILD = new JavaPackage("com.foo.bar"); + private static final JavaPackage PARENT = new JavaPackage("com.foo"); + + @Test + public void testChildPackageIsWithinParent() { + assertTrue(CHILD.isWithin(PARENT)); + } + + @Test + public void testGetElementsOfMultiLevelPackage() { + // Set up + final JavaPackage javaPackage = CHILD; + + // Invoke + final List elements = javaPackage.getElements(); + + // Check + assertEquals(Arrays.asList("com", "foo", "bar"), elements); + assertEquals("bar", javaPackage.getLastElement()); + } + + @Test + public void testGetElementsOfSingleLevelPackage() { + // Set up + final JavaPackage javaPackage = new JavaPackage("me"); + + // Invoke + final List elements = javaPackage.getElements(); + + // Check + assertEquals(Arrays.asList("me"), elements); + assertEquals("me", javaPackage.getLastElement()); + } + + @Test + public void testPackageIsNotWithinNullPackage() { + assertFalse(PARENT.isWithin(null)); + } + + @Test + public void testPackageIsWithinSelf() { + assertTrue(PARENT.isWithin(PARENT)); + } + + @Test + public void testParentPackageIsNotWithinChild() { + assertFalse(PARENT.isWithin(CHILD)); + } +} diff --git a/model/src/test/java/org/springframework/roo/model/JavaSymbolNameTest.java b/model/src/test/java/org/springframework/roo/model/JavaSymbolNameTest.java new file mode 100644 index 000000000..1a03765fb --- /dev/null +++ b/model/src/test/java/org/springframework/roo/model/JavaSymbolNameTest.java @@ -0,0 +1,27 @@ +package org.springframework.roo.model; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Unit test of {@link JavaSymbolName}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class JavaSymbolNameTest { + + @Test + public void testAssertJavaNameLegal() { + final String symbolName = "META.INF.web.resources.dojo.1.5.util.shrinksafe.src.org.dojotoolkit.shrinksafe.Compressor"; + final JavaSymbolName symbol = new JavaSymbolName(symbolName); + assertEquals(symbolName, symbol.getSymbolName()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFailAssertJavaNameLegal() { + new JavaSymbolName( + "META-INF.web-resources.dojo-1.5.util.shrinksafe.src.org.dojotoolkit.shrinksafe.Compressor"); + } +} diff --git a/model/src/test/java/org/springframework/roo/model/JavaTypeTest.java b/model/src/test/java/org/springframework/roo/model/JavaTypeTest.java new file mode 100644 index 000000000..a3b13f81a --- /dev/null +++ b/model/src/test/java/org/springframework/roo/model/JavaTypeTest.java @@ -0,0 +1,117 @@ +package org.springframework.roo.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.roo.model.JavaType.BOOLEAN_OBJECT; +import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; +import static org.springframework.roo.model.JavaType.BYTE_ARRAY_PRIMITIVE; +import static org.springframework.roo.model.JavaType.INT_OBJECT; +import static org.springframework.roo.model.JavaType.OBJECT; +import static org.springframework.roo.model.JavaType.STRING; +import static org.springframework.roo.model.JavaType.listOf; + +import org.junit.Test; + +public class JavaTypeTest { + + @Test + public void testArrayTypeIsMultiValued() { + assertTrue(BYTE_ARRAY_PRIMITIVE.isMultiValued()); + } + + @Test + public void testBooleanObjectIsBoolean() { + assertTrue(BOOLEAN_OBJECT.isBoolean()); + } + + @Test + public void testBooleanPrimitiveIsBoolean() { + assertTrue(BOOLEAN_PRIMITIVE.isBoolean()); + } + + @Test + public void testCollectionTypesAreMultiValued() { + assertTrue(listOf(INT_OBJECT).isMultiValued()); + } + + @Test + public void testCoreTypeIsCoreType() { + assertTrue(STRING.isCoreType()); + } + + @Test + public void testEnclosingTypeDetection() { + + // No enclosing types + assertNull(new JavaType("BarBar").getEnclosingType()); + assertNull(new JavaType("com.foo.Car").getEnclosingType()); + assertNull(new JavaType("foo.Sar").getEnclosingType()); + assertNull(new JavaType("bob").getEnclosingType()); + + // Enclosing type in default package + assertEquals(new JavaType("Bob"), + new JavaType("Bob.Smith").getEnclosingType()); + assertEquals(new JavaPackage(""), new JavaType("Bob.Smith") + .getEnclosingType().getPackage()); + + // Enclosing type in declared package + assertEquals(new JavaType("foo.My"), + new JavaType("foo.My.Sar").getEnclosingType()); + + // Enclosing type in declared package several levels deep + assertEquals(new JavaType("foo.bar.My"), + new JavaType("foo.bar.My.Sar").getEnclosingType()); + assertEquals("com.foo._MyBar", + new JavaType("com.foo._MyBar").getFullyQualifiedTypeName()); + assertEquals(new JavaType("com.Foo.Bar"), + new JavaType("com.Foo.Bar.My").getEnclosingType()); + assertEquals(new JavaType("com.foo.BAR"), + new JavaType("com.foo.BAR.My").getEnclosingType()); + + // Enclosing type us explicitly specified + assertEquals(new JavaPackage("com.foo"), new JavaType( + "com.foo.Bob.Smith", new JavaType("com.foo.Bob")) + .getEnclosingType().getPackage()); + assertEquals(new JavaType("com.foo.Bob"), + new JavaType("com.foo.Bob.Smith", new JavaType("com.foo.Bob")) + .getEnclosingType()); + } + + @Test + public void testGetBaseTypeForNonCollectionType() { + assertEquals(STRING, STRING.getBaseType()); + } + + @Test + public void testGetBaseTypeForParameterisedCollectionType() { + assertEquals(STRING, JavaType.listOf(STRING).getBaseType()); + } + + @Test + public void testGetBaseTypeForUnparameterisedCollectionType() { + assertNull(JdkJavaType.LIST.getBaseType()); + } + + @Test + public void testObjectIsNotBoolean() { + assertFalse(OBJECT.isBoolean()); + } + + @Test + public void testSingleValuedTypeIsNotMultiValued() { + assertFalse(STRING.isMultiValued()); + } + + @Test + public void testTypeInRootPackage() { + assertEquals("", new JavaType("MyRootClass").getPackage() + .getFullyQualifiedPackageName()); + } + + @Test + public void testUserTypeIsNonCoreType() { + assertFalse(new JavaType("com.example.Thing").isCoreType()); + } +} diff --git a/model/src/test/java/org/springframework/roo/model/JdkJavaTypeTest.java b/model/src/test/java/org/springframework/roo/model/JdkJavaTypeTest.java new file mode 100644 index 000000000..59df239bc --- /dev/null +++ b/model/src/test/java/org/springframework/roo/model/JdkJavaTypeTest.java @@ -0,0 +1,47 @@ +package org.springframework.roo.model; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import org.junit.Test; + +/** + * Unit test of {@link JdkJavaType} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JdkJavaTypeTest { + + /** + * Asserts that the given {@link JavaType} represents a valid JDK type + * + * @param javaType + * @throws Exception + */ + private void assertValidJdkType(final JavaType javaType) throws Exception { + Class.forName(javaType.getFullyQualifiedTypeName()); + } + + /** + * Indicates whether the given field is a public constant + * + * @param field the field to check (required) + * @return see above + */ + private boolean isPublicConstant(final Field field) { + final int modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) + && Modifier.isFinal(modifiers); + } + + @Test + public void testClassNamesAreActualJdkTypes() throws Exception { + for (final Field field : JdkJavaType.class.getDeclaredFields()) { + if (isPublicConstant(field) + && JavaType.class.equals(field.getType())) { + assertValidJdkType((JavaType) field.get(null)); + } + } + } +} diff --git a/model/src/test/java/org/springframework/roo/model/ReservedWordsTest.java b/model/src/test/java/org/springframework/roo/model/ReservedWordsTest.java new file mode 100644 index 000000000..07f7f338a --- /dev/null +++ b/model/src/test/java/org/springframework/roo/model/ReservedWordsTest.java @@ -0,0 +1,70 @@ +package org.springframework.roo.model; + +import org.junit.Test; + +/** + * Unit test of {@link ReservedWords} + * + * @author Alan Stewart + * @since 1.2.1 + */ +public class ReservedWordsTest { + + @Test + public void testVerifyReservedJavaKeywordsNotPresent() { + ReservedWords.verifyReservedJavaKeywordsNotPresent("test"); + } + + @Test + public void testVerifyReservedJavaKeywordsNotPresent2() { + ReservedWords.verifyReservedJavaKeywordsNotPresent(new JavaSymbolName( + "test")); + } + + @Test + public void testVerifyReservedJavaKeywordsNotPresent3() { + ReservedWords.verifyReservedJavaKeywordsNotPresent(new JavaType( + "com.foo.bar.Foo")); + } + + @Test(expected = IllegalStateException.class) + public void testVerifyReservedJavaKeywordsPresent() { + ReservedWords.verifyReservedJavaKeywordsNotPresent(new JavaSymbolName( + "if")); + } + + @Test(expected = IllegalStateException.class) + public void testVerifyReservedJavaKeywordsPresent2() { + ReservedWords.verifyReservedJavaKeywordsNotPresent(new JavaType( + "com.return.bar")); + } + + @Test + public void testVerifyReservedSqlKeywordsNotPresent() { + ReservedWords.verifyReservedSqlKeywordsNotPresent("ROW_VER_NO"); + } + + @Test + public void testVerifyReservedSqlKeywordsNotPresent2() { + ReservedWords.verifyReservedSqlKeywordsNotPresent(new JavaSymbolName( + "ROW_VER_NO")); + } + + @Test + public void testVerifyReservedSqlKeywordsNotPresent3() { + ReservedWords.verifyReservedSqlKeywordsNotPresent(new JavaType( + "com.bar.Foo")); + } + + @Test(expected = IllegalStateException.class) + public void testVerifyReservedSqlKeywordsPresent() { + ReservedWords.verifyReservedSqlKeywordsNotPresent(new JavaSymbolName( + "alter")); + } + + @Test(expected = IllegalStateException.class) + public void testVerifyReservedSqlKeywordsPresent2() { + ReservedWords.verifyReservedSqlKeywordsNotPresent(new JavaType( + "com.bar.Outer")); + } +} diff --git a/osgi-bundle/pom.xml b/osgi-bundle/pom.xml new file mode 100644 index 000000000..500e1ab5d --- /dev/null +++ b/osgi-bundle/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.root + 2.0.0.BUILD-SNAPSHOT + .. + + org.springframework.roo.osgi.bundle + pom + Spring Roo - OSGi Bundle Module Parent + Provides POM configuration inheritence for standard OSGi modules. + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.1.1 + + + generate-resources + + exec + + + + + git + + log + --pretty=format:Git-Commit-Hash: %H + -n1 + + ${project.build.directory}/build-number.mf + + + 0 + 1 + 127 + 128 + + + + + org.apache.felix + maven-bundle-plugin + 2.5.3 + true + + + <_include>${project.build.directory}/build-number.mf + ${project.artifactId}.*;version=${project.version} + ${project.artifactId} + ${project.organization.name} + Copyright ${project.organization.name}. All Rights Reserved. + ${project.url} + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.4 + + + copy-dependencies + package + + copy-dependencies + + + + + ${project.build.directory}/../../target/all + true + compile + org.apache.felix.scr.annotations + org.osgi + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + + copy-jars-for-roo-dev-or-assembly + package + + + + + + + + + + + + + run + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + copy-dependencies + unpack + + [0.0,) + + + + + + + + org.codehaus.mojo + exec-maven-plugin + + exec + + [0.0,) + + + + + + + + + + + + + + diff --git a/osgi-roo-bundle/pom.xml b/osgi-roo-bundle/pom.xml new file mode 100644 index 000000000..665368d73 --- /dev/null +++ b/osgi-roo-bundle/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.osgi.roo.bundle + pom + Spring Roo - OSGi Roo Bundle Module Parent + Provides POM configuration inheritence for standard Roo SCR-requiring modules. + + + + + + + + org.apache.felix + maven-scr-plugin + ${scr.plugin.version} + + + generate-scr-scrdescriptor + + + scr + + + + + false + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..2d2f94ab5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,984 @@ + + + 4.0.0 + org.springframework.roo + org.springframework.roo.root + pom + 2.0.0.BUILD-SNAPSHOT + Spring Roo + http://projects.spring.io/spring-roo/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + https://github.com/spring-projects/spring-roo + + Spring Roo is a next-generation rapid application development tool for Java developers. It focuses on higher productivity, stock-standard Java APIs, high usability, avoiding engineering trade-offs and facilitating easy Roo removal. + 2009 + + + + deployment-support + osgi-bundle + osgi-roo-bundle + bootstrap + startlevel + support + support-osgi + url-stream + url-stream-jdk + shell + shell-osgi + shell-jline + shell-jline-osgi + uaa + felix + model + metadata + file-undo + file-monitor + file-monitor-polling + file-monitor-polling-roo + project + process-manager + classpath + classpath-antlrjavaparser + + addon-tostring + addon-equals + addon-javabean + addon-plural + addon-propfiles + addon-configurable + addon-email + addon-jpa + addon-jms + addon-finder + addon-logging + addon-property-editor + addon-dod + addon-test + addon-backup + addon-serializable + addon-web-mvc-controller + addon-web-mvc-embedded + addon-web-mvc-jsp + addon-security + addon-solr + addon-web-flow + addon-web-selenium + addon-jdbc + addon-dbre + addon-creator + addon-roobot-client + addon-json + addon-jsf + addon-op4j + addon-git + addon-cloud + addon-layers-service + addon-layers-repository-jpa + addon-layers-repository-mongo + addon-oscommands + addon-tailor + annotations + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + spring-roo-repository-release + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + false + + + + spring-roo-repository-snapshot + Spring Roo Repository + http://spring-roo-repository.springsource.org/snapshot + + never + true + + + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-roo-repository-bundles + Spring Roo Repository + http://spring-roo-repository.springsource.org/bundles + + false + + + + + + Alan Stewart + alankstewart + alankstewart at gmail dot com + +10 + + + Juan Carlos García del Canto + jcagarcia + jugade92 at gmail dot com + +1 + + + + + junit + junit + + + org.mockito + mockito-all + + + org.powermock + powermock-module-junit4 + + + org.powermock + powermock-api-mockito + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + + + commons-codec + commons-codec + + + + + + + junit + junit + 4.11 + test + + + org.hamcrest + hamcrest-core + + + + + org.mockito + mockito-all + 1.8.5 + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + + org.apache.felix + org.apache.felix.bundlerepository + 2.0.2 + + + org.easymock + easymock + + + + + org.apache.felix + org.apache.felix.framework + 4.4.1 + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + + org.apache.felix + org.apache.felix.ipojo + 1.12.0 + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + org.apache.felix + org.apache.felix.ipojo.metadata + + + asm + asm-all + + + + + org.apache.felix + org.apache.felix.log + 1.0.1 + + + org.apache.felix + org.osgi.core + + + org.apache.felix + org.osgi.compendium + + + org.apache.felix + javax.servlet + + + + + org.apache.felix + org.apache.felix.scr + 1.8.2 + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + + org.apache.felix + org.apache.felix.scr.annotations + 1.9.8 + + + org.apache.felix + org.apache.felix.shell + 1.4.3 + + + org.apache.felix + org.apache.felix.gogo.runtime + 0.12.1 + + + org.apache.felix + org.apache.felix.gogo.command + 0.14.0 + + + + org.springframework.uaa + org.springframework.uaa.client + 1.0.2.RELEASE + + + net.sourceforge.jline + jline + 1.0.S2-B + + + org.fusesource.jansi + jansi + 1.6 + + + org.codehaus.jackson + jackson-core-asl + 1.6.2 + + + org.codehaus.jackson + jackson-mapper-asl + 1.6.2 + + + org.slf4j + slf4j-api + 1.7.5 + + + org.slf4j + slf4j-nop + 1.7.5 + + + org.slf4j + jcl-over-slf4j + 1.7.5 + + + org.apache.commons + commons-lang3 + 3.1 + + + commons-io + commons-io + 2.4 + + + commons-codec + commons-codec + 1.8 + + + org.mortbay.jetty + servlet-api + 2.5-20081211 + + + com.github.antlrjavaparser + antlr-java-parser + 1.0.15 + + + org.springframework + spring-aop + ${spring.version} + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + org.springframework + spring-asm + 3.1.4 + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + org.springframework + spring-beans + ${spring.version} + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + org.springframework + spring-context + ${spring.version} + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + org.springframework + spring-core + ${spring.version} + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + org.springframework + spring-expression + ${spring.version} + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + org.springframework + spring-web + ${spring.version} + + + aopalliance + aopalliance + + + commons-logging + commons-logging + + + + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.aopalliance + 1.0.0.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.bcpg-jdk15 + 1.45.0.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.bcprov-jdk15 + 1.45.0.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.antlr4-runtime + 4.3.0002 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.hapax + 2.3.4.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.inflector + 0.7.0.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.jsch + 0.1.42.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.jgit + 0.12.1.0010 + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.json-simple + 1.1.0.0010 + + + + org.springframework.roo + org.springframework.roo.addon.propfiles + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.plural + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.configurable + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.dod + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.test + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.serializable + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.jpa + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.finder + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.json + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.controller + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.backup + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.web.mvc.jsp + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.security + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.jdbc + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.layers.repository.jpa + ${project.version} + + + org.springframework.roo + org.springframework.roo.addon.layers.service + ${project.version} + + + + org.springframework.roo + org.springframework.roo.bootstrap + ${project.version} + + + org.springframework.roo + org.springframework.roo.classpath + ${project.version} + + + org.springframework.roo + org.springframework.roo.classpath.antlrjavaparser + ${project.version} + + + org.springframework.roo + org.springframework.roo.deployment.support + ${project.version} + + + org.springframework.roo + org.springframework.roo.felix + ${project.version} + + + org.springframework.roo + org.springframework.roo.file.monitor + ${project.version} + + + org.springframework.roo + org.springframework.roo.file.monitor.polling + ${project.version} + + + org.springframework.roo + org.springframework.roo.file.monitor.polling.roo + ${project.version} + + + org.springframework.roo + org.springframework.roo.file.undo + ${project.version} + + + org.springframework.roo + org.springframework.roo.metadata + ${project.version} + + + org.springframework.roo + org.springframework.roo.model + ${project.version} + + + org.springframework.roo + org.springframework.roo.osgi.bundle + ${project.version} + + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + ${project.version} + + + org.springframework.roo + org.springframework.roo.process.manager + ${project.version} + + + org.springframework.roo + org.springframework.roo.project + ${project.version} + + + org.springframework.roo + org.springframework.roo.shell + ${project.version} + + + org.springframework.roo + org.springframework.roo.shell.jline + ${project.version} + + + org.springframework.roo + org.springframework.roo.shell.jline.osgi + ${project.version} + + + org.springframework.roo + org.springframework.roo.shell.osgi + ${project.version} + + + org.springframework.roo + org.springframework.roo.startlevel + ${project.version} + + + org.springframework.roo + org.springframework.roo.support + ${project.version} + + + org.springframework.roo + org.springframework.roo.support.osgi + ${project.version} + + + org.springframework.roo + org.springframework.roo.uaa + ${project.version} + + + org.springframework.roo + org.springframework.roo.url.stream + ${project.version} + + + org.springframework.roo + org.springframework.roo.url.stream.jdk + ${project.version} + + + + + + + org.springframework.build.aws + org.springframework.build.aws.maven + 3.1.0.RELEASE + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true + + ossrh + https://oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.3 + + true + + + + sign-artifacts + verify + + sign + + + + + + maven-antrun-plugin + 1.7 + + + ant-contrib + ant-contrib + 20020829 + + + org.apache.ant + ant-apache-regexp + 1.7.1 + + + + + org.apache.maven.plugins + maven-help-plugin + 2.1.1 + + + org.apache.maven.plugins + maven-clean-plugin + 2.5 + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + false + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.6 + 1.6 + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.2 + + + + + 3.0.1 + + + 1.6.0 + + + + + + enforce-versions + + enforce + + + + + + org.apache.maven.plugins + maven-install-plugin + 2.3.1 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + true + false + + + + + org.apache.maven.plugins + maven-idea-plugin + 2.2 + + true + true + + + + + + 1.4.12 + 3.2.8.RELEASE + UTF-8 + 5.0.0 + 1.20.0 + + + + + bamboo-build + + + + org.apache.maven.plugins + maven-gpg-plugin + + true + + + + + + + diff --git a/process-manager/pom.xml b/process-manager/pom.xml new file mode 100644 index 000000000..e4e1116d0 --- /dev/null +++ b/process-manager/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.process.manager + bundle + Spring Roo - Process Manager + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.shell.osgi + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/ActiveProcessManager.java b/process-manager/src/main/java/org/springframework/roo/process/manager/ActiveProcessManager.java new file mode 100644 index 000000000..c5a79b1a6 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/ActiveProcessManager.java @@ -0,0 +1,24 @@ +package org.springframework.roo.process.manager; + +/** + * Guarantees to provide access to the currently-executing + * {@link ProcessManager}. + * + * @author Ben Alex + * @since 1.0 + */ +public class ActiveProcessManager { + private static ThreadLocal processManager = new ThreadLocal(); + + public static void clearActiveProcessManager() { + processManager.remove(); + } + + public static ProcessManager getActiveProcessManager() { + return processManager.get(); + } + + public static void setActiveProcessManager(final ProcessManager pm) { + processManager.set(pm); + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/CommandCallback.java b/process-manager/src/main/java/org/springframework/roo/process/manager/CommandCallback.java new file mode 100644 index 000000000..b7d10c577 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/CommandCallback.java @@ -0,0 +1,18 @@ +package org.springframework.roo.process.manager; + +/** + * An interface used to execute a command within a {@link ProcessManager} + * "transaction-like" operation. + * + * @author Ben Alex + * @since 1.0 + */ +public interface CommandCallback { + + /** + * Execute the user-defined logic. + * + * @return a result of the logic (can be null) + */ + T callback(); +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/FileManager.java b/process-manager/src/main/java/org/springframework/roo/process/manager/FileManager.java new file mode 100644 index 000000000..4b0f5cf2d --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/FileManager.java @@ -0,0 +1,233 @@ +package org.springframework.roo.process.manager; + +import java.io.InputStream; +import java.util.SortedSet; + +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.file.undo.UndoManager; + +/** + * Represents the primary means for add-ons to modify the underlying disk + * storage. + *

    + * A {@link FileManager} instance is acquired from the {@link ProcessManager}. A + * {@link FileManager} implementation must guarantee to use an + * {@link UndoManager} that is available to the {@link ProcessManager}, such + * that {@link ProcessManager} can undo or reset as required. + *

    + * An implementation may elect to defer writes to disk or discard them until + * {@link #commit()} or {@link #clear()} respectively is invoked. + * + * @author Ben Alex + * @since 1.0 + */ +public interface FileManager { + + /** + * Discards proposed changes to the disk that an implementation may have + * elected to defer. + */ + void clear(); + + /** + * Commits actual changes to the disk that an implementation may have + * elected to defer. + */ + void commit(); + + /** + * Attempts to create a new directory on the disk. + *

    + * The requested file identifier path must not already exist. It should be + * in canonical file name format. + *

    + * Parent directories must also be created automatically by this method. Any + * created parent directories should be removed as part of the undo + * behaviour. + *

    + * An exception will be thrown if the path already exists. + * + * @param fileIdentifier a path to be created that does not already exist + * (required) + * @return a representation of the directory (or null if the creation + * failed) + */ + FileDetails createDirectory(String fileIdentifier); + + /** + * Attempts to create a zero-byte file on the disk. + *

    + * The requested fileIdentifier path must not already exist. It should be in + * canonical file name format. + *

    + * Implementations guarantee to {@link #createDirectory(String)} as required + * to create any required parent directories. + *

    + * An exception will be thrown if the path already exists. + * + * @param fileIdentifier a path to be created that does not already exist + * (required) + * @return a representation of the file (or null if the creation failed) + */ + MutableFile createFile(String fileIdentifier); + + /** + * Provides a simple way to create or update a file, skipping any + * modification if the file's contents match the proposed contents. This + * should only be called for text files. + *

    + * This mechanism also automatically deletes an unwanted file if the new + * contents are zero bytes. If deleting, the existence of the file need not + * be considered in advance (it will only delete if the file is present, but + * it will not fail if the file does not exist or has been separately + * deleted). + *

    + * Implementations guarantee to {@link #createDirectory(String)} as required + * to create any required parent directories. + *

    + * Implementations are required to observe the {@link #commit()} and + * {@link #clear()} semantics defined in the type-level JavaDocs. + * + * @param fileIdentifier the file to create or update as appropriate + * (required) + * @param newContents the replacement contents (required, but can be zero + * bytes if the file should be deleted) + * @param writeImmediately forces immediate write of the file to disk (false + * means it can be deferred, as recommended) + */ + void createOrUpdateTextFileIfRequired(String fileIdentifier, + String newContents, boolean writeImmediately); + + /** + * Provides a simple way to create or update a file, skipping any + * modification if the file's contents match the proposed contents. This + * should only be called for text files. + *

    + * This mechanism also automatically deletes an unwanted file if the new + * contents are zero bytes. If deleting, the existence of the file need not + * be considered in advance (it will only delete if the file is present, but + * it will not fail if the file does not exist or has been separately + * deleted). + *

    + * Implementations guarantee to {@link #createDirectory(String)} as required + * to create any required parent directories. + *

    + * Implementations are required to observe the {@link #commit()} and + * {@link #clear()} semantics defined in the type-level JavaDocs. + * + * @param fileIdentifier the file to create or update as appropriate + * (required) + * @param newContents the replacement contents (required, but can be zero + * bytes if the file should be deleted) + * @param descriptionOfChange the additional information about a change (can + * be null) + * @param writeImmediately forces immediate write of the file to disk (false + * means it can be deferred, as recommended) + */ + void createOrUpdateTextFileIfRequired(String fileIdentifier, + String newContents, String descriptionOfChange, + boolean writeImmediately); + + /** + * Attempts to delete a file or directory on the disk. The path should be in + * canonical file name format. + *

    + * If the path refers to a directory, contents of the directory will be + * recursively deleted. + *

    + * If a delete fails, an exception will be thrown. + * + * @param pathname the file to delete; can be blank to do nothing, otherwise + * should be a valid pathname as per + * {@link java.io.File#File(String)} + * @throws IllegalArgumentException if a file is specified but does not + * exist + */ + void delete(String pathname); + + /** + * Attempts to delete a file or directory on the disk. The path should be in + * canonical file name format. + *

    + * If the path refers to a directory, contents of the directory will be + * recursively deleted. + *

    + * If a delete fails, an exception will be thrown. + * + * @param pathname the file to delete; can be blank to do nothing, otherwise + * should be a valid pathname as per + * {@link java.io.File#File(String)} + * @param reasonForDeletion the reason why the file is being deleted (can be + * blank) + * @since 1.2.0 + * @throws IllegalArgumentException if a file is specified but does not + * exist + */ + void delete(String pathname, String reasonForDeletion); + + /** + * Indicates whether the file identified by the passed canonical path + * exists. + * + * @param fileIdentifier the file or directory to locate (required, in + * canonical path format) + * @return true if the file or directory exists + */ + boolean exists(String fileIdentifier); + + /** + * Delegates to {@link FileMonitorService#findMatchingAntPath(String)}. + * + * @param antPath the Ant path to evaluate, as per the canonical file path + * format (required) + * @return all matching identifiers (may be empty, but never null) + */ + SortedSet findMatchingAntPath(String antPath); + + /** + * Obtains an input stream for the indicated file identifier, which must be + * a file (not a directory) and must exist at the time the method is called. + * This method is useful if read-only access to a file is required. For + * read-write access, use one of the other methods on {@link FileManager}. + * + * @param fileIdentifier the file to read (required, in canonical path + * format) + * @return the input stream (never null) + */ + InputStream getInputStream(String fileIdentifier); + + /** + * Obtains an already-existing file for reading. The path should be in + * canonical file name format. + * + * @param fileIdentifier the file to read that already exists (required) + * @return a representation of the file (or null if the file does not exist) + */ + FileDetails readFile(String fileIdentifier); + + /** + * Delegates to {@link FileMonitorService#scanAll()} or + * {@link NotifiableFileMonitorService#scanNotified()} if available. + * + * @return the number of changes detected (can be 0 or above) + */ + int scan(); + + /** + * Provides an updatable representation of a file on the disk. + *

    + * The file identifier must refer to a file (not directory) that already + * exists. A violation of this requirement will result in an exception. The + * identifier should be in canonical file name format. + *

    + * Refer to the documentation for {@link MutableFile} for important + * restrictions on usage. + * + * @param fileIdentifier the file to update (must be a file that already + * exists, required) + * @return a mutable presentation (never null) + */ + MutableFile updateFile(String fileIdentifier); +} \ No newline at end of file diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/MutableFile.java b/process-manager/src/main/java/org/springframework/roo/process/manager/MutableFile.java new file mode 100644 index 000000000..4d0fd56bd --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/MutableFile.java @@ -0,0 +1,39 @@ +package org.springframework.roo.process.manager; + +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.roo.file.undo.UndoManager; + +/** + * Represents a handle to a file (not a directory) that can be legally modified. + *

    + * It is critical that classes using {@link MutableFile} close any streams + * acquired from the file before further {@link UndoManager} operations. Failure + * to do so many compromise the ability of {@link UndoManager} to operate + * correctly. + *

    + * Implementations must guarantee that the file physically exists and is indeed + * a file. Specifically, {@link MutableFile} implementations should not exist + * for directories or non-existent files. + * + * @author Ben Alex + * @since 1.0 + */ +public interface MutableFile { + + String getCanonicalPath(); + + InputStream getInputStream(); + + OutputStream getOutputStream(); + + /** + * Permits presentation of additional information about a change being made + * via {@link #getOutputStream()}. + * + * @param message the additional information (can be null or empty to clear + * any extra information) + */ + void setDescriptionOfChange(String message); +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManager.java b/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManager.java new file mode 100644 index 000000000..78708f429 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManager.java @@ -0,0 +1,101 @@ +package org.springframework.roo.process.manager; + +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.undo.UndoManager; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.process.manager.event.ProcessManagerStatus; +import org.springframework.roo.process.manager.event.ProcessManagerStatusProvider; + +/** + * Provides coordinated execution of major ROO operations. + *

    + * A {@link ProcessManager} delivers: + *

      + *
    • A well-defined state publication model via + * {@link ProcessManagerStatusProvider}
    • + *
    • Startup-time registration of the initial monitoring requests (after the + * {@link #completeStartup()} method has been called)
    • + *
    • Specific polling of {@link FileMonitorService} at well-defined times
    • + *
    • The ability to execute {@link CommandCallback}s for user-requested + * operations
    • + *
    • An assurance the above is conducted within a "transaction-like" model
    • + *
    + *

    + * Once constructed, all methods in {@link ProcessManager} operate in a + * "transaction-like" manner. This is achieved by use of a {@link FileManager} + * that shares the same {@link UndoManager}. + *

    + * Once available, a {@link ProcessManager} will ordinarily wait for either + * {@link #backgroundPoll()} or {@link #execute(CommandCallback)}. It will block + * until the state of the {@link ProcessManager} returns to + * {@link ProcessManagerStatus#AVAILABLE}. + *

    + * {@link ProcessManager} guarantees: + *

      + *
    • The status will remain {@link ProcessManagerStatus#STARTING} until + * {@link #completeStartup()} has been called. This is intended to allow objects + * depending on {@link ProcessManager} or other {@link MetadataService}s to be + * constructed and register for events. The initial monitoring requests will + * only be registered during {@link #completeStartup()}, which therefore + * simplifies the design of dependent objects as they generally (i) don't need + * to retrieve metadata produced before they were listening and (ii) can freely + * modify the file system pursuant to {@link FileManager} with assurance of + * correct "transaction" behaviour.
    • + *
    • At the end of a successful startup, background poll or command execution, + * {@link UndoManager#reset()} method will be called.
    • + *
    • An uncaught exception will cause {@link UndoManager#undo()} to be called + * (before re-throwing the exception).
    • + *
    • A {@link FileMonitorService#scanAll()} will be called after a command is + * executed, and will continue to be called until such time as it does not + * return any further changes. Such calls will occur within the scope of the + * same "transaction" as used for the command.
    • + *
    + *

    + * {@link ProcessManager} implementations also guarantee to update + * {@link ActiveProcessManager} whenever running an operation, and clear it when + * an operation completes. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ProcessManager extends ProcessManagerStatusProvider { + + /** + * Execute a user command within a "transaction". This method blocks until + * {@link ProcessManagerStatus#AVAILABLE}. + *

    + * This method may throw {@link RuntimeException}s that occurred while + * executing. + * + * @param the class of the object that + * {@link CommandCallback#callback()} will return (required) + * @param callback the callback to actually executed (required) + * @return the result of executing the callback + */ + T execute(CommandCallback callback); + + long getLastPollDuration(); + + long getMinimumDelayBetweenPoll(); + + /** + * @return true if the system is in development mode, which generally means + * more detailed diagnostics are requested from add-ons (defaults to + * false) + */ + boolean isDevelopmentMode(); + + void setDevelopmentMode(boolean developmentMode); + + void setMinimumDelayBetweenPoll(long minimumDelayBetweenPoll); + + /** + * Allows the process manager to terminate gracefully. In particular this + * means any background threads it has started are terminated. It is safe to + * call this method more than once, but no other method in the process + * manager need operate correctly after termination. + */ + void terminate(); + + void timerBasedPoll(); +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManagerCommands.java b/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManagerCommands.java new file mode 100644 index 000000000..1fd1ee23c --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManagerCommands.java @@ -0,0 +1,160 @@ +package org.springframework.roo.process.manager; + +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.Shell; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Commands related to file system monitoring and process management. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class ProcessManagerCommands implements CommandMarker { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ProcessManagerCommands.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private ProcessManager processManager; + private Shell shell; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + @CliCommand(value = "development mode", help = "Switches the system into development mode (greater diagnostic information)") + public String developmentMode( + @CliOption(key = { "", "enabled" }, mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "true", help = "Activates development mode") final boolean enabled) { + + if(processManager == null){ + processManager = getProcessManager(); + } + + Validate.notNull(processManager, "ProcessManager is required"); + + if(shell == null){ + shell = getShell(); + } + + Validate.notNull(shell, "Shell is required"); + + processManager.setDevelopmentMode(enabled); + shell.setDevelopmentMode(enabled); + return "Development mode set to " + enabled; + } + + @CliCommand(value = "poll now", help = "Perform a manual file system poll") + public String poll() { + if(processManager == null){ + processManager = getProcessManager(); + } + + Validate.notNull(processManager, "ProcessManager is required"); + + final long originalSetting = processManager + .getMinimumDelayBetweenPoll(); + try { + processManager.setMinimumDelayBetweenPoll(1); + processManager.timerBasedPoll(); + } + finally { + // Switch on manual polling again + processManager.setMinimumDelayBetweenPoll(originalSetting); + } + return "Manual poll completed"; + } + + @CliCommand(value = "poll status", help = "Display file system polling information") + public String pollingInfo() { + if(processManager == null){ + processManager = getProcessManager(); + } + + Validate.notNull(processManager, "ProcessManager is required"); + + final StringBuilder sb = new StringBuilder("File system polling "); + final long duration = processManager.getLastPollDuration(); + if (duration == 0) { + sb.append("never executed; "); + } + else { + sb.append("last took ").append(duration).append(" ms; "); + } + final long minimum = processManager.getMinimumDelayBetweenPoll(); + if (minimum == 0) { + sb.append("automatic polling is disabled"); + } + else if (minimum < 0) { + sb.append("auto-scaled polling is enabled"); + } + else { + sb.append("polling frequency has a minimum interval of ") + .append(minimum).append(" ms"); + } + return sb.toString(); + } + + @CliCommand(value = "poll speed", help = "Changes the file system polling speed") + public String pollingSpeed( + @CliOption(key = { "", "ms" }, mandatory = true, help = "The number of milliseconds between each poll") final long minimumDelayBetweenPoll) { + if(processManager == null){ + processManager = getProcessManager(); + } + + Validate.notNull(processManager, "ProcessManager is required"); + + processManager.setMinimumDelayBetweenPoll(minimumDelayBetweenPoll); + return pollingInfo(); + } + + public ProcessManager getProcessManager(){ + // Get all components implement ProcessManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProcessManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProcessManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProcessManager on ProcessManagerCommands."); + return null; + } + } + + public Shell getShell(){ + // Get all Shell implement Shell interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(Shell.class.getName(), null); + + for(ServiceReference ref : references){ + return (Shell) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load Shell on ProcessManagerCommands."); + return null; + } + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManagerHostedExecutionStrategy.java b/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManagerHostedExecutionStrategy.java new file mode 100644 index 000000000..34301ef41 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/ProcessManagerHostedExecutionStrategy.java @@ -0,0 +1,90 @@ +package org.springframework.roo.process.manager; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.process.manager.event.ProcessManagerStatus; +import org.springframework.roo.shell.ExecutionStrategy; +import org.springframework.roo.shell.ParseResult; + +/** + * Used to dispatch shell {@link ExecutionStrategy} requests through + * {@link ProcessManager#execute(org.springframework.roo.process.manager.CommandCallback)} + * . + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +@Reference(name = "processManager", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = ProcessManager.class, cardinality = ReferenceCardinality.MANDATORY_UNARY) +public class ProcessManagerHostedExecutionStrategy implements ExecutionStrategy { + + private final Class mutex = ProcessManagerHostedExecutionStrategy.class; + private ProcessManager processManager; + + protected void bindProcessManager(final ProcessManager processManager) { + synchronized (mutex) { + this.processManager = processManager; + } + } + + public Object execute(final ParseResult parseResult) + throws RuntimeException { + Validate.notNull(parseResult, "Parse result required"); + synchronized (mutex) { + Validate.isTrue(isReadyForCommands(), + "ProcessManagerHostedExecutionStrategy not yet ready for commands"); + return processManager.execute(new CommandCallback() { + public Object callback() { + try { + return parseResult.getMethod().invoke( + parseResult.getInstance(), + parseResult.getArguments()); + } + catch (Exception e) { + throw new RuntimeException(ObjectUtils.defaultIfNull( + ExceptionUtils.getRootCause(e), e)); + } + } + }); + } + } + + public boolean isReadyForCommands() { + synchronized (mutex) { + if (processManager != null) { + // BUSY_EXECUTION needed in case of recursive commands, such as + // if executing a script + // TERMINATED added in case of additional commands following a + // quit or exit in a script - ROO-2270 + final ProcessManagerStatus processManagerStatus = processManager + .getProcessManagerStatus(); + return processManagerStatus == ProcessManagerStatus.AVAILABLE + || processManagerStatus == ProcessManagerStatus.BUSY_EXECUTING + || processManagerStatus == ProcessManagerStatus.TERMINATED; + } + } + return false; + } + + public void terminate() { + synchronized (mutex) { + if (processManager != null) { + processManager.terminate(); + } + } + } + + protected void unbindProcessManager(final ProcessManager processManager) { + synchronized (mutex) { + this.processManager = null; + } + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/event/AbstractProcessManagerStatusPublisher.java b/process-manager/src/main/java/org/springframework/roo/process/manager/event/AbstractProcessManagerStatusPublisher.java new file mode 100644 index 000000000..eacebb6b1 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/event/AbstractProcessManagerStatusPublisher.java @@ -0,0 +1,85 @@ +package org.springframework.roo.process.manager.event; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.process.manager.ProcessManager; + +/** + * Provides a convenience superclass for those {@link ProcessManager}s wishing + * to publish status messages. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractProcessManagerStatusPublisher implements + ProcessManagerStatusProvider { + + /** + * Used so a single object instance contains the changing + * {@link ProcessManagerStatus} enum. This is needed so there is a single + * object instance for synchronization purposes. + */ + private static class StatusHolder { + + private ProcessManagerStatus status; + + /** + * Constructor + * + * @param initialStatus + */ + private StatusHolder(final ProcessManagerStatus initialStatus) { + status = initialStatus; + } + } + + protected StatusHolder processManagerStatus = new StatusHolder( + ProcessManagerStatus.STARTING); + + protected Set processManagerStatusListeners = new CopyOnWriteArraySet(); + + public final void addProcessManagerStatusListener( + final ProcessManagerStatusListener processManagerStatusListener) { + Validate.notNull(processManagerStatusListener, + "Status listener required"); + processManagerStatusListeners.add(processManagerStatusListener); + } + + /** + * Obtains the process manager status without synchronization. + */ + public final ProcessManagerStatus getProcessManagerStatus() { + return processManagerStatus.status; + } + + public final void removeProcessManagerStatusListener( + final ProcessManagerStatusListener processManagerStatusListener) { + Validate.notNull(processManagerStatusListener, + "Status listener required"); + processManagerStatusListeners.remove(processManagerStatusListener); + } + + /** + * Set the process manager status without synchronization. + */ + protected void setProcessManagerStatus( + final ProcessManagerStatus processManagerStatus) { + Validate.notNull(processManagerStatus, + "Process manager status required"); + + if (this.processManagerStatus.status == processManagerStatus) { + // No need to make a change + return; + } + + this.processManagerStatus.status = processManagerStatus; + + for (final ProcessManagerStatusListener listener : processManagerStatusListeners) { + listener.onProcessManagerStatusChange( + this.processManagerStatus.status, processManagerStatus); + } + } + +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatus.java b/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatus.java new file mode 100644 index 000000000..3d64ef294 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatus.java @@ -0,0 +1,19 @@ +package org.springframework.roo.process.manager.event; + +import org.springframework.roo.process.manager.ProcessManager; + +/** + * Represents the different states that a {@link ProcessManager} can legally be + * in. + *

    + * There is no "shut down" state because the process manager would have been + * terminated by that stage and potentially garbage collected. There is no + * guarantee that a process manager implementation will necessarily publish + * every state. + * + * @author Ben Alex + * @since 1.0 + */ +public enum ProcessManagerStatus { + AVAILABLE, BUSY_EXECUTING, BUSY_POLLING, COMPLETING_STARTUP, RESETTING_UNDOS, STARTING, TERMINATED, UNDOING +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatusListener.java b/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatusListener.java new file mode 100644 index 000000000..d9f3d220a --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatusListener.java @@ -0,0 +1,22 @@ +package org.springframework.roo.process.manager.event; + +import org.springframework.roo.process.manager.ProcessManager; + +/** + * Implemented by classes that wish to be notified of {@link ProcessManager} + * status changes. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ProcessManagerStatusListener { + + /** + * Invoked by the {@link ProcessManager} to report a new status. + * + * @param oldStatus the old status + * @param newStatus the new status + */ + void onProcessManagerStatusChange(ProcessManagerStatus oldStatus, + ProcessManagerStatus newStatus); +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatusProvider.java b/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatusProvider.java new file mode 100644 index 000000000..ae2dd939d --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/event/ProcessManagerStatusProvider.java @@ -0,0 +1,51 @@ +package org.springframework.roo.process.manager.event; + +import org.springframework.roo.process.manager.ProcessManager; + +/** + * Implemented by {@link ProcessManager}s that support the publication of shell + * status changes. + *

    + * Implementations are not required to provide any guarantees with respect to + * the order in which notifications are delivered to listeners. + *

    + * Implementations must permit modification of the listener list, even while + * delivering event notifications to listeners. However, listeners do not + * receive any guarantee that their addition or removal from the listener list + * will be effective or not for any event notification that is currently + * proceeding. + *

    + * Implementations must ensure that status notifications are only delivered when + * an actual change has taken place. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ProcessManagerStatusProvider { + + /** + * Registers a new status listener. + * + * @param processManagerStatusListener to register (cannot be null) + */ + void addProcessManagerStatusListener( + ProcessManagerStatusListener processManagerStatusListener); + + /** + * Returns the current {@link ProcessManager}. + * + * @return the current status (never null) + */ + ProcessManagerStatus getProcessManagerStatus(); + + /** + * Removes an existing status listener. + *

    + * If the presented status listener is not found, the method returns without + * exception. + * + * @param processManagerStatusListener to remove (cannot be null) + */ + void removeProcessManagerStatusListener( + ProcessManagerStatusListener processManagerStatusListener); +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultFileManager.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultFileManager.java new file mode 100644 index 000000000..527ff6c69 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultFileManager.java @@ -0,0 +1,447 @@ +package org.springframework.roo.process.manager.internal; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedSet; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.file.undo.CreateDirectory; +import org.springframework.roo.file.undo.CreateFile; +import org.springframework.roo.file.undo.DeleteDirectory; +import org.springframework.roo.file.undo.DeleteFile; +import org.springframework.roo.file.undo.FilenameResolver; +import org.springframework.roo.file.undo.UndoEvent; +import org.springframework.roo.file.undo.UndoListener; +import org.springframework.roo.file.undo.UndoManager; +import org.springframework.roo.file.undo.UpdateFile; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.process.manager.ProcessManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Default implementation of {@link FileManager}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class DefaultFileManager implements FileManager, UndoListener { + + protected final static Logger LOGGER = HandlerUtils.getLogger(DefaultFileManager.class); + + /** key: file identifier, value: new description of change */ + private final Map deferredDescriptionOfChanges = new LinkedHashMap(); + /** key: file identifier, value: new textual content */ + private final Map deferredFileWrites = new LinkedHashMap(); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private NotifiableFileMonitorService fileMonitorService; + private FilenameResolver filenameResolver; + private ProcessManager processManager; + private UndoManager undoManager; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + if(undoManager == null){ + undoManager = getUndoManager(); + } + undoManager.addUndoListener(this); + } + + public void clear() { + deferredFileWrites.clear(); + deferredDescriptionOfChanges.clear(); + } + + public void commit() { + final Map toRemove = new LinkedHashMap( + deferredFileWrites); + try { + for (final Entry entry : toRemove.entrySet()) { + final String fileIdentifier = entry.getKey(); + final String newContents = entry.getValue(); + if (StringUtils.isNotBlank(newContents)) { + createOrUpdateTextFileIfRequired(fileIdentifier, + newContents, + StringUtils + .stripToEmpty(deferredDescriptionOfChanges + .get(fileIdentifier))); + } + else if (exists(fileIdentifier)) { + delete(fileIdentifier, "empty"); + } + } + } + finally { + for (final String remove : toRemove.keySet()) { + deferredFileWrites.remove(remove); + } + deferredDescriptionOfChanges.clear(); + } + } + + public FileDetails createDirectory(final String fileIdentifier) { + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + if(filenameResolver == null){ + filenameResolver = getFileNameResolver(); + } + if(undoManager == null){ + undoManager = getUndoManager(); + } + Validate.notNull(fileIdentifier, "File identifier required"); + Validate.notNull(fileMonitorService, "FileMonitorService required"); + Validate.notNull(filenameResolver, "FilenameResolver required"); + Validate.notNull(undoManager, "UndoManager required"); + final File actual = new File(fileIdentifier); + Validate.isTrue(!actual.exists(), "File '%s' already exists", + fileIdentifier); + try { + fileMonitorService.notifyCreated(actual.getCanonicalPath()); + } + catch (final IOException ignored) { + } + new CreateDirectory(undoManager, filenameResolver, actual); + return new FileDetails(actual, actual.lastModified()); + } + + public MutableFile createFile(final String fileIdentifier) { + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + if(processManager == null){ + processManager = getProcessManager(); + } + if(filenameResolver == null){ + filenameResolver = getFileNameResolver(); + } + if(undoManager == null){ + undoManager = getUndoManager(); + } + Validate.notNull(fileIdentifier, "File identifier required"); + Validate.notNull(fileMonitorService, "FileMonitorService required"); + Validate.notNull(processManager, "ProcessManager required"); + Validate.notNull(filenameResolver, "FilenameResolver required"); + Validate.notNull(undoManager, "UndoManager required"); + final File actual = new File(fileIdentifier); + Validate.isTrue(!actual.exists(), "File '%s' already exists", + fileIdentifier); + try { + fileMonitorService.notifyCreated(actual.getCanonicalPath()); + final File parentDirectory = new File(actual.getParent()); + if (!parentDirectory.exists()) { + createDirectory(parentDirectory.getCanonicalPath()); + } + } + catch (final IOException ignored) { + } + new CreateFile(undoManager, filenameResolver, actual); + final ManagedMessageRenderer renderer = new ManagedMessageRenderer( + filenameResolver, actual, true); + renderer.setIncludeHashCode(processManager.isDevelopmentMode()); + return new DefaultMutableFile(actual, null, renderer); + } + + public void createOrUpdateTextFileIfRequired(final String fileIdentifier, + final String newContents, final boolean writeImmediately) { + createOrUpdateTextFileIfRequired(fileIdentifier, newContents, "", + writeImmediately); + } + + private void createOrUpdateTextFileIfRequired(final String fileIdentifier, + final String newContents, final String descriptionOfChange) { + MutableFile mutableFile = null; + if (exists(fileIdentifier)) { + // First verify if the file has even changed + final File file = new File(fileIdentifier); + String existing = null; + try { + existing = FileUtils.readFileToString(file); + } + catch (final IOException ignored) { + } + + if (!newContents.equals(existing)) { + mutableFile = updateFile(fileIdentifier); + } + } + else { + mutableFile = createFile(fileIdentifier); + Validate.notNull(mutableFile, "Could not create file '%s'", + fileIdentifier); + } + + if (mutableFile != null) { + OutputStream outputStream = null; + try { + if (StringUtils.isNotBlank(descriptionOfChange)) { + mutableFile.setDescriptionOfChange(descriptionOfChange); + } + outputStream = mutableFile.getOutputStream(); + IOUtils.write(newContents, outputStream); + } + catch (final IOException e) { + throw new IllegalStateException("Could not output '" + + mutableFile.getCanonicalPath() + "'", e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + } + + public void createOrUpdateTextFileIfRequired(final String fileIdentifier, + final String newContents, final String descriptionOfChange, + final boolean writeImmediately) { + if (writeImmediately) { + createOrUpdateTextFileIfRequired(fileIdentifier, newContents, + descriptionOfChange); + } + else { + deferredFileWrites.put(fileIdentifier, newContents); + + String deferredDescriptionOfChange = StringUtils.defaultIfEmpty( + deferredDescriptionOfChanges.get(fileIdentifier), ""); + if (StringUtils.isNotBlank(deferredDescriptionOfChange) + && !deferredDescriptionOfChange.trim().endsWith(";")) { + deferredDescriptionOfChange += "; "; + } + deferredDescriptionOfChanges.put( + fileIdentifier, + deferredDescriptionOfChange + + StringUtils.stripToEmpty(descriptionOfChange)); + } + } + + protected void deactivate(final ComponentContext context) { + if(undoManager == null){ + undoManager = getUndoManager(); + } + Validate.notNull(undoManager, "UndoManager is required"); + undoManager.removeUndoListener(this); + } + + public void delete(final String fileIdentifier) { + delete(fileIdentifier, null); + } + + public void delete(final String fileIdentifier, + final String reasonForDeletion) { + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + if(filenameResolver == null){ + filenameResolver = getFileNameResolver(); + } + if(undoManager == null){ + undoManager = getUndoManager(); + } + Validate.notNull(fileMonitorService, "FileMonitorService required"); + Validate.notNull(filenameResolver, "FilenameResolver required"); + Validate.notNull(undoManager, "UndoManager is required"); + if (StringUtils.isBlank(fileIdentifier)) { + return; + } + + final File actual = new File(fileIdentifier); + Validate.isTrue(actual.exists(), "File '%s' does not exist", + fileIdentifier); + try { + fileMonitorService.notifyDeleted(actual.getCanonicalPath()); + } + catch (final IOException ignored) { + } + if (actual.isDirectory()) { + new DeleteDirectory(undoManager, filenameResolver, actual, + reasonForDeletion); + } + else { + new DeleteFile(undoManager, filenameResolver, actual, + reasonForDeletion); + } + } + + public boolean exists(final String fileIdentifier) { + Validate.notBlank(fileIdentifier, "File identifier required"); + return new File(fileIdentifier).exists(); + } + + public SortedSet findMatchingAntPath(final String antPath) { + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + Validate.notNull(fileMonitorService, "FileMonitorService required"); + return fileMonitorService.findMatchingAntPath(antPath); + } + + public InputStream getInputStream(final String fileIdentifier) { + if (deferredFileWrites.containsKey(fileIdentifier)) { + return new BufferedInputStream(new ByteArrayInputStream( + deferredFileWrites.get(fileIdentifier).getBytes())); + } + + final File file = new File(fileIdentifier); + Validate.isTrue(file.exists(), "File '%s' does not exist", + fileIdentifier); + Validate.isTrue(file.isFile(), "Path '%s' is not a file", + fileIdentifier); + try { + return new BufferedInputStream(new FileInputStream(new File( + fileIdentifier))); + } + catch (final IOException ioe) { + throw new IllegalStateException( + "Could not obtain input stream to file '" + fileIdentifier + + "'", ioe); + } + } + + public void onUndoEvent(final UndoEvent event) { + if (event.isUndoing()) { + clear(); + } + else { + // It's a flush or a reset event + commit(); + } + } + + public FileDetails readFile(final String fileIdentifier) { + Validate.notNull(fileIdentifier, "File identifier required"); + final File f = new File(fileIdentifier); + if (!f.exists()) { + return null; + } + return new FileDetails(f, f.lastModified()); + } + + public int scan() { + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + Validate.notNull(fileMonitorService, "FileMonitorService required"); + return fileMonitorService.scanNotified(); + } + + public MutableFile updateFile(final String fileIdentifier) { + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + if(processManager == null){ + processManager = getProcessManager(); + } + if(filenameResolver == null){ + filenameResolver = getFileNameResolver(); + } + if(undoManager == null){ + undoManager = getUndoManager(); + } + Validate.notNull(fileIdentifier, "File identifier required"); + Validate.notNull(fileMonitorService, "FileMonitorService required"); + Validate.notNull(processManager, "ProcessManager required"); + Validate.notNull(filenameResolver, "FilenameResolver required"); + Validate.notNull(undoManager, "UndoManager required"); + final File actual = new File(fileIdentifier); + Validate.isTrue(actual.exists(), "File '%s' does not exist", + fileIdentifier); + new UpdateFile(undoManager, filenameResolver, actual); + final ManagedMessageRenderer renderer = new ManagedMessageRenderer( + filenameResolver, actual, false); + renderer.setIncludeHashCode(processManager.isDevelopmentMode()); + return new DefaultMutableFile(actual, fileMonitorService, renderer); + } + + public NotifiableFileMonitorService getFileMonitorService(){ + // Get all Services implement NotifiableFileMonitorService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(NotifiableFileMonitorService.class.getName(), null); + + for(ServiceReference ref : references){ + return (NotifiableFileMonitorService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load NotifiableFileMonitorService on DefaultFileManager."); + return null; + } + } + + public ProcessManager getProcessManager(){ + // Get all Services implement ProcessManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProcessManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProcessManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProcessManager on DefaultFileManager."); + return null; + } + } + + public FilenameResolver getFileNameResolver(){ + // Get all Services implement FilenameResolver interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FilenameResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (FilenameResolver) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FilenameResolver on DefaultFileManager."); + return null; + } + } + + public UndoManager getUndoManager(){ + // Get all Services implement UndoManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(UndoManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (UndoManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load UndoManager on DefaultFileManager."); + return null; + } + } +} \ No newline at end of file diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultMutableFile.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultMutableFile.java new file mode 100644 index 000000000..9cfc175e7 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultMutableFile.java @@ -0,0 +1,90 @@ +package org.springframework.roo.process.manager.internal; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; +import org.springframework.roo.process.manager.MutableFile; +import org.springframework.roo.support.util.FileUtils; + +/** + * Default implementation of {@link MutableFile}. + * + * @author Ben Alex + * @since 1.0 + */ +public class DefaultMutableFile implements MutableFile { + + private final File file; + private final NotifiableFileMonitorService fileMonitorService; + private final ManagedMessageRenderer managedMessageRenderer; + + public DefaultMutableFile(final File file, + final NotifiableFileMonitorService fileMonitorService, + final ManagedMessageRenderer managedMessageRenderer) { + Validate.notNull(file, "File required"); + Validate.notNull(managedMessageRenderer, "Message renderer required"); + Validate.isTrue(file.isFile(), + "A mutable file must actually be a file (not a directory)"); + Validate.isTrue(file.exists(), "A mutable file must actually exist"); + this.file = file; + this.managedMessageRenderer = managedMessageRenderer; + // null is permitted + this.fileMonitorService = fileMonitorService; + } + + public String getCanonicalPath() { + return FileUtils.getCanonicalPath(file); + } + + public InputStream getInputStream() { + // Do more checks, in case the file has changed since this instance was + // constructed + Validate.isTrue(file.isFile(), + "A mutable file must actually be a file (not a directory)"); + Validate.isTrue(file.exists(), "A mutable file must actually exist"); + try { + return new BufferedInputStream(new FileInputStream(file)); + } + catch (final IOException ioe) { + throw new IllegalStateException( + "Unable to acquire input stream for file '" + + getCanonicalPath() + "'", ioe); + } + } + + public OutputStream getOutputStream() { + // Do more checks, in case the file has changed since this instance was + // constructed + Validate.isTrue(file.isFile(), + "A mutable file must actually be a file (not a directory)"); + Validate.isTrue(file.exists(), "A mutable file must actually exist"); + + try { + return new MonitoredOutputStream(file, managedMessageRenderer, + fileMonitorService); + } + catch (final IOException ioe) { + throw new IllegalStateException( + "Unable to acquire output stream for file '" + + getCanonicalPath() + "'", ioe); + } + } + + public void setDescriptionOfChange(final String message) { + managedMessageRenderer.setDescriptionOfChange(message); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("file", getCanonicalPath()); + return builder.toString(); + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultProcessManager.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultProcessManager.java new file mode 100644 index 000000000..ec189dfdc --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/DefaultProcessManager.java @@ -0,0 +1,444 @@ +package org.springframework.roo.process.manager.internal; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.startlevel.StartLevel; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.MonitoringRequest; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; +import org.springframework.roo.file.undo.UndoManager; +import org.springframework.roo.process.manager.ActiveProcessManager; +import org.springframework.roo.process.manager.CommandCallback; +import org.springframework.roo.process.manager.ProcessManager; +import org.springframework.roo.process.manager.event.AbstractProcessManagerStatusPublisher; +import org.springframework.roo.process.manager.event.ProcessManagerStatus; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Default implementation of {@link ProcessManager} interface. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class DefaultProcessManager extends + AbstractProcessManagerStatusPublisher implements ProcessManager { + + private static final Logger LOGGER = HandlerUtils + .getLogger(DefaultProcessManager.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private boolean developmentMode = false; + private FileMonitorService fileMonitorService; + private long lastPollDuration = 0; + private long lastPollTime = 0; // What time the last poll was completed + private long minimumDelayBetweenPoll = -1; // How many ms must pass at + private StartLevel startLevel; + private UndoManager undoManager; + private String workingDir; + + public T execute(final CommandCallback callback) { + Validate.notNull(callback, "Callback required"); + synchronized (processManagerStatus) { + // For us to acquire this lock means no other thread has hold of + // process manager status + Validate.isTrue( + getProcessManagerStatus() == ProcessManagerStatus.AVAILABLE + || getProcessManagerStatus() == ProcessManagerStatus.BUSY_EXECUTING, + "Unable to execute as another thread has set status to %s", + getProcessManagerStatus()); + setProcessManagerStatus(ProcessManagerStatus.BUSY_EXECUTING); + try { + return doTransactionally(callback); + } + catch (final RuntimeException e) { + logException(e); + throw e; + } + finally { + setProcessManagerStatus(ProcessManagerStatus.AVAILABLE); + } + } + } + + /** + * @return how many milliseconds the last poll execution took to complete (0 + * = never ran; >0 = last execution time) + */ + public long getLastPollDuration() { + return lastPollDuration; + } + + /** + * @return how many milliseconds must pass between each poll (0 = manual + * only; <0 = auto-scaled; >0 = interval) + */ + public long getMinimumDelayBetweenPoll() { + return minimumDelayBetweenPoll; + } + + public boolean isDevelopmentMode() { + return developmentMode; + } + + public void setDevelopmentMode(final boolean developmentMode) { + + if(undoManager == null){ + undoManager = getUndoManager(); + } + + Validate.notNull(undoManager, "UndoManager is required"); + + this.developmentMode = developmentMode; + + // To assist with debugging, development mode does not undertake undo + // operations + undoManager.setUndoEnabled(!developmentMode); + } + + /** + * @param minimumDelayBetweenPoll how many milliseconds must pass between + * each poll + */ + public void setMinimumDelayBetweenPoll(final long minimumDelayBetweenPoll) { + this.minimumDelayBetweenPoll = minimumDelayBetweenPoll; + } + + public void terminate() { + synchronized (processManagerStatus) { + // To get this far this thread has a lock on process manager status, + // so we control process manager and can terminate its background + // timer thread + if (getProcessManagerStatus() != ProcessManagerStatus.TERMINATED) { + // The thread started above will terminate of its own accord, + // given we are shutting down + setProcessManagerStatus(ProcessManagerStatus.TERMINATED); + } + } + } + + public void timerBasedPoll() { + try { + if (minimumDelayBetweenPoll == 0) { + // Manual polling only, we never allow the timer to kick of a + // poll + return; + } + + long effectiveMinimumDelayBetweenPoll = minimumDelayBetweenPoll; + if (effectiveMinimumDelayBetweenPoll < 0) { + // A negative minimum delay between poll means auto-scaling is + // used + if (lastPollDuration < 500) { + // We've never done a poll, or they are very fast + effectiveMinimumDelayBetweenPoll = 0; + } + else { + // Use the last duration (we might make this sliding scale + // in the future) + effectiveMinimumDelayBetweenPoll = lastPollDuration; + } + } + final long started = System.currentTimeMillis(); + if (started < lastPollTime + effectiveMinimumDelayBetweenPoll) { + // Too soon to re-poll + return; + } + backgroundPoll(); + // Record the completion time so we can ensure we don't re-poll too + // soon + lastPollTime = System.currentTimeMillis(); + + // Compute how many milliseconds it took to run + lastPollDuration = lastPollTime - started; + if (lastPollDuration == 0) { + // Ensure it correctly reflects that it has ever run + lastPollDuration = 1; + } + } + catch (final Throwable t) { + LOGGER.log(Level.SEVERE, t.getMessage(), t); + } + } + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + workingDir = OSGiUtils.getRooWorkingDirectory(context); + this.context.addFrameworkListener( + new FrameworkListener() { + public void frameworkEvent(final FrameworkEvent event) { + + if(startLevel == null){ + startLevel = getStartLevel(); + } + + Validate.notNull(startLevel, "StartLevel is required"); + + if (startLevel.getStartLevel() >= 99) { + // We check we haven't already started, as this + // event listener will be called several times at SL + // >= 99 + if (getProcessManagerStatus() == ProcessManagerStatus.STARTING) { + // A proper synchronized process manager status + // check will take place in the + // completeStartup() method + completeStartup(); + } + } + } + }); + + // Now start a thread that will undertake a background poll every second + final Thread t = new Thread(new Runnable() { + public void run() { + // Unsynchronized lookup of terminated status to avoid anything + // blocking the termination of the thread + while (getProcessManagerStatus() != ProcessManagerStatus.TERMINATED) { + // We only bother doing a poll if we seem to be available (a + // proper synchronized check happens later) + if (getProcessManagerStatus() == ProcessManagerStatus.AVAILABLE) { + timerBasedPoll(); + } + try { + Thread.sleep(1000); + } + catch (final InterruptedException ignoreAndContinue) { + } + } + } + }, "Spring Roo Process Manager Background Polling Thread"); + t.start(); + } + + protected void deactivate(final ComponentContext context) { + // We have lost a required component (eg UndoManager; ROO-1037) + terminate(); // Safe to call even if we'd terminated earlier + } + + private boolean backgroundPoll() { + // Quickly determine if another thread is running; we don't need to sit + // around and wait (we'll get called again in a few hundred milliseconds + // anyway) + if (getProcessManagerStatus() != ProcessManagerStatus.AVAILABLE) { + return false; + } + synchronized (processManagerStatus) { + // Do the check again, now this thread has a lock on + // processManagerStatus + if (getProcessManagerStatus() != ProcessManagerStatus.AVAILABLE) { + throw new IllegalStateException( + "Process manager status " + + getProcessManagerStatus() + + " but background thread acquired synchronization lock"); + } + + setProcessManagerStatus(ProcessManagerStatus.BUSY_POLLING); + + try { + doTransactionally(null); + } + catch (final Throwable t) { + // We don't want a poll failure to cause the background polling + // thread to die + logException(t); + } + finally { + setProcessManagerStatus(ProcessManagerStatus.AVAILABLE); + } + } + return true; + } + + private void completeStartup() { + + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + + Validate.notNull(fileMonitorService, "FileMonitorService is required"); + + synchronized (processManagerStatus) { + if (getProcessManagerStatus() != ProcessManagerStatus.STARTING) { + throw new IllegalStateException("Process manager status " + + getProcessManagerStatus() + " but should be STARTING"); + } + setProcessManagerStatus(ProcessManagerStatus.COMPLETING_STARTUP); + try { + // Register the initial monitoring request + doTransactionally(new MonitoringRequestCommand( + fileMonitorService, + MonitoringRequest + .getInitialSubTreeMonitoringRequest(workingDir), + true)); + } + catch (final Throwable t) { + logException(t); + } + finally { + setProcessManagerStatus(ProcessManagerStatus.AVAILABLE); + } + } + } + + private T doTransactionally(final CommandCallback callback) { + + if(fileMonitorService == null){ + fileMonitorService = getFileMonitorService(); + } + + Validate.notNull(fileMonitorService, "FileMonitorService is required"); + + if(undoManager == null){ + undoManager = getUndoManager(); + } + + Validate.notNull(undoManager, "UndoManager is required"); + + T result = null; + try { + ActiveProcessManager.setActiveProcessManager(this); + + // Run the requested operation + if (callback == null) { + fileMonitorService.scanAll(); + } + else { + result = callback.callback(); + } + + // Flush the undo manager so that any changes it has been holding + // are written to disk and the file monitor service + undoManager.flush(); + + // Guarantee scans repeat until there are no more changes detected + while (fileMonitorService.isDirty()) { + if (fileMonitorService instanceof NotifiableFileMonitorService) { + ((NotifiableFileMonitorService) fileMonitorService) + .scanNotified(); + } + else { + fileMonitorService.scanAll(); + } + // In case something else happened as a result of event + // notifications above + undoManager.flush(); + } + + // It all seems to have worked, so clear the undo history + setProcessManagerStatus(ProcessManagerStatus.RESETTING_UNDOS); + + undoManager.reset(); + + } + catch (final RuntimeException e) { + // Something went wrong, so attempt to undo + try { + setProcessManagerStatus(ProcessManagerStatus.UNDOING); + throw e; + } + finally { + undoManager.undo(); + } + } + finally { + // TODO: Review in consultation with Christian as STS is clearing + // active process manager itself + // ActiveProcessManager.clearActiveProcessManager(); + } + + return result; + } + + private void logException(final Throwable t) { + final Throwable root = ObjectUtils.defaultIfNull( + ExceptionUtils.getRootCause(t), t); + if (developmentMode) { + LOGGER.log(Level.FINE, root.getMessage(), root); + } + else { + String message = root.getMessage(); + if (StringUtils.isBlank(message)) { + final StackTraceElement[] trace = root.getStackTrace(); + if (trace != null && trace.length > 0) { + message = root.getClass().getSimpleName() + " at " + + trace[0].toString(); + } + else { + message = root.getClass().getSimpleName(); + } + } + LOGGER.log(Level.FINE, message); + } + } + + public FileMonitorService getFileMonitorService(){ + // Get all Services implement FileMonitorService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(FileMonitorService.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileMonitorService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileMonitorService on DefaultProcessManager."); + return null; + } + } + + public StartLevel getStartLevel(){ + // Get all Services implement StartLevel interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(StartLevel.class.getName(), null); + + for(ServiceReference ref : references){ + return (StartLevel) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load StartLevel on DefaultProcessManager."); + return null; + } + } + + public UndoManager getUndoManager(){ + // Get all Services implement UndoManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(UndoManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (UndoManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load UndoManager on DefaultProcessManager."); + return null; + } + } + +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/ManagedMessageRenderer.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/ManagedMessageRenderer.java new file mode 100644 index 000000000..1d1bd1d38 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/ManagedMessageRenderer.java @@ -0,0 +1,69 @@ +package org.springframework.roo.process.manager.internal; + +import java.io.File; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.undo.FilenameResolver; +import org.springframework.roo.support.logging.HandlerUtils; + +public class ManagedMessageRenderer { + + private static final Logger LOGGER = HandlerUtils + .getLogger(ManagedMessageRenderer.class); + + private final boolean createOperation; + private String descriptionOfChange; + private final File file; + private final FilenameResolver filenameResolver; + private String hashCode; + private boolean includeHashCode; + + public ManagedMessageRenderer(final FilenameResolver filenameResolver, + final File file, final boolean createOperation) { + Validate.notNull(filenameResolver, "Filename resolver required"); + Validate.notNull(file, "File to manage required"); + this.filenameResolver = filenameResolver; + this.file = file; + this.createOperation = createOperation; + } + + boolean isIncludeHashCode() { + return includeHashCode; + } + + void logManagedMessage() { + final StringBuilder message = new StringBuilder(); + if (hashCode != null && includeHashCode && hashCode.length() >= 7) { + // Display only the first 6 characters, being consistent with Git + // hash code display conventions + message.append(hashCode.subSequence(0, 7)).append(" "); + } + if (createOperation) { + message.append("Created "); + } + else { + message.append("Updated "); + } + message.append(filenameResolver.getMeaningfulName(file)); + if (StringUtils.isNotBlank(descriptionOfChange)) { + message.append(" ["); + message.append(descriptionOfChange); + message.append("]"); + } + LOGGER.fine(message.toString()); + } + + public void setDescriptionOfChange(final String message) { + descriptionOfChange = message; + } + + void setHashCode(final String hashCode) { + this.hashCode = hashCode; + } + + void setIncludeHashCode(final boolean includeHashCode) { + this.includeHashCode = includeHashCode; + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/MonitoredOutputStream.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/MonitoredOutputStream.java new file mode 100644 index 000000000..afceaada4 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/MonitoredOutputStream.java @@ -0,0 +1,78 @@ +package org.springframework.roo.process.manager.internal; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; + +/** + * Ensures the {@link NotifiableFileMonitorService#notifyChanged(String)} method + * is invoked when {@link #close()} is called. + *

    + * This is useful for ensuring the file monitoring system is notified of all + * changed files, even those which are changed very rapidly on disk and would + * not normally be detected using the file system's "last updated" timestamps. + * + * @author Ben Alex + * @since 1.0 + */ +public class MonitoredOutputStream extends ByteArrayOutputStream { + + private final File file; + private final NotifiableFileMonitorService fileMonitorService; + + private final ManagedMessageRenderer managedMessageRenderer; + + /** + * Constructs a {@link MonitoredOutputStream}. + * + * @param file the file to output to (required) + * @param managedMessageRenderer a rendered for outputting a message once + * the output stream is closed (required) + * @param fileMonitorService an optional monitoring service (null is + * acceptable) + * @throws FileNotFoundException if the file cannot be found + */ + public MonitoredOutputStream(final File file, + final ManagedMessageRenderer managedMessageRenderer, + final NotifiableFileMonitorService fileMonitorService) + throws FileNotFoundException { + Validate.notNull(file, "File required"); + Validate.notNull(managedMessageRenderer, "Message renderer required"); + this.file = file; + this.fileMonitorService = fileMonitorService; + this.managedMessageRenderer = managedMessageRenderer; + } + + @Override + public void close() throws IOException { + // Obtain the bytes the user is writing out + final byte[] bytes = toByteArray(); + + // Try to calculate the SHA hash code + managedMessageRenderer.setHashCode(DigestUtils.shaHex(bytes)); + + // Log that we're writing the file + managedMessageRenderer.logManagedMessage(); + + // Write the actual file out to disk + FileUtils.writeByteArrayToFile(file, bytes); + + // Tell the FileMonitorService what happened + String fileCanonicalPath; + try { + fileCanonicalPath = file.getCanonicalPath(); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + if (fileMonitorService != null) { + fileMonitorService.notifyChanged(fileCanonicalPath); + } + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/MonitoringRequestCommand.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/MonitoringRequestCommand.java new file mode 100644 index 000000000..dba3fc55f --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/MonitoringRequestCommand.java @@ -0,0 +1,42 @@ +package org.springframework.roo.process.manager.internal; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.MonitoringRequest; +import org.springframework.roo.process.manager.CommandCallback; + +/** + * Represents a {@link CommandCallback} to start or stop monitoring a particular + * file path. + * + * @author Ben Alex + * @since 1.0 + */ +public class MonitoringRequestCommand implements CommandCallback { + + private final boolean add; + private final FileMonitorService fileMonitorService; + private final MonitoringRequest monitoringRequest; + + public MonitoringRequestCommand( + final FileMonitorService fileMonitorService, + final MonitoringRequest monitoringRequest, final boolean add) { + Validate.notNull(fileMonitorService, "File monitor service required"); + Validate.notNull(monitoringRequest, "Request required"); + this.fileMonitorService = fileMonitorService; + this.monitoringRequest = monitoringRequest; + this.add = add; + } + + public Boolean callback() { + boolean result; + if (add) { + result = fileMonitorService.add(monitoringRequest); + } + else { + result = fileMonitorService.remove(monitoringRequest); + } + fileMonitorService.scanAll(); + return result; + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/ProcessManagerDiagnosticsListener.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/ProcessManagerDiagnosticsListener.java new file mode 100644 index 000000000..bf45d2781 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/ProcessManagerDiagnosticsListener.java @@ -0,0 +1,55 @@ +package org.springframework.roo.process.manager.internal; + +import java.util.logging.Level; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.process.manager.ProcessManager; +import org.springframework.roo.process.manager.event.ProcessManagerStatus; +import org.springframework.roo.process.manager.event.ProcessManagerStatusListener; +import org.springframework.roo.process.manager.event.ProcessManagerStatusProvider; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.osgi.AbstractFlashingObject; + +/** + * Allows monitoring of {@link ProcessManager} for development mode users. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.1 + */ +@Service +@Component +public class ProcessManagerDiagnosticsListener extends AbstractFlashingObject + implements ProcessManagerStatusListener, CommandMarker { + + private boolean isDebug = false; + @Reference private ProcessManagerStatusProvider processManagerStatusProvider; + + protected void activate(final ComponentContext context) { + processManagerStatusProvider.addProcessManagerStatusListener(this); + isDebug = System.getProperty("roo-args") != null && isDevelopmentMode(); + } + + protected void deactivate(final ComponentContext context) { + processManagerStatusProvider.removeProcessManagerStatusListener(this); + } + + public void onProcessManagerStatusChange( + final ProcessManagerStatus oldStatus, + final ProcessManagerStatus newStatus) { + if (isDebug) { + flash(Level.FINE, newStatus.name(), MY_SLOT); + } + } + + @CliCommand(value = "process manager debug", help = "Indicates if process manager debugging is desired") + public void processManagerDebug( + @CliOption(key = { "", "enabled" }, mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "true", help = "Activates debug mode") final boolean debug) { + isDebug = debug; + } +} diff --git a/process-manager/src/main/java/org/springframework/roo/process/manager/internal/UndoableMonitoringRequest.java b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/UndoableMonitoringRequest.java new file mode 100644 index 000000000..fa38dc161 --- /dev/null +++ b/process-manager/src/main/java/org/springframework/roo/process/manager/internal/UndoableMonitoringRequest.java @@ -0,0 +1,65 @@ +package org.springframework.roo.process.manager.internal; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.MonitoringRequest; +import org.springframework.roo.file.undo.UndoManager; +import org.springframework.roo.file.undo.UndoableOperation; + +/** + * Allows {@link MonitoringRequest}s to be applied as {@link UndoableOperation} + * s. + * + * @author Ben Alex + * @since 1.0 + */ +public class UndoableMonitoringRequest implements UndoableOperation { + + private final boolean add; + private final FileMonitorService fileMonitorService; + private final MonitoringRequest monitoringRequest; + private boolean resetRequired; + private final UndoManager undoManager; + + public UndoableMonitoringRequest(final UndoManager undoManager, + final FileMonitorService fileMonitorService, + final MonitoringRequest monitoringRequest, final boolean add) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(fileMonitorService, "File monitor service required"); + Validate.notNull(monitoringRequest, "Request required"); + this.undoManager = undoManager; + this.fileMonitorService = fileMonitorService; + this.monitoringRequest = monitoringRequest; + this.add = add; + + if (add) { + resetRequired = fileMonitorService.add(monitoringRequest); + } + else { + resetRequired = fileMonitorService.remove(monitoringRequest); + } + + this.undoManager.add(this); + } + + public void reset() { + } + + public boolean undo() { + if (!resetRequired) { + return true; + } + try { + if (add) { + fileMonitorService.remove(monitoringRequest); + } + else { + fileMonitorService.add(monitoringRequest); + } + return true; + } + catch (final RuntimeException e) { + return false; + } + } +} diff --git a/project/pom.xml b/project/pom.xml new file mode 100644 index 000000000..207a260c2 --- /dev/null +++ b/project/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.project + bundle + Spring Roo - Project + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.framework + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.file.monitor + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.model + + + org.springframework.roo + org.springframework.roo.file.undo + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.process.manager + + + org.springframework.roo + org.springframework.roo.uaa + + + + org.springframework.uaa + org.springframework.uaa.client + + + diff --git a/project/src/main/java/org/springframework/roo/project/AbstractPathResolvingStrategy.java b/project/src/main/java/org/springframework/roo/project/AbstractPathResolvingStrategy.java new file mode 100644 index 000000000..54f72f321 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/AbstractPathResolvingStrategy.java @@ -0,0 +1,89 @@ +package org.springframework.roo.project; + +import static org.springframework.roo.support.util.FileUtils.CURRENT_DIRECTORY; + +import java.io.File; +import java.util.Collection; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.FileUtils; + +/** + * Convenient superclass for {@link PathResolvingStrategy} implementations. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component(componentAbstract = true) +public abstract class AbstractPathResolvingStrategy implements + PathResolvingStrategy { + + protected static final String ROOT_MODULE = ""; + + private String rootPath; + + // ------------ OSGi component methods ---------------- + + protected void activate(final ComponentContext context) { + final File projectDirectory = new File(StringUtils.defaultIfEmpty( + OSGiUtils.getRooWorkingDirectory(context), CURRENT_DIRECTORY)); + rootPath = FileUtils.getCanonicalPath(projectDirectory); + } + + // ------------ PathResolvingStrategy methods ---------------- + + protected abstract PhysicalPath getApplicablePhysicalPath(String identifier); + + public String getFriendlyName(final String identifier) { + Validate.notNull(identifier, "Identifier required"); + final LogicalPath p = getPath(identifier); + if (p == null) { + return identifier; + } + return p.getName() + getRelativeSegment(identifier); + } + + public LogicalPath getPath(final String identifier) { + final PhysicalPath parent = getApplicablePhysicalPath(identifier); + if (parent == null) { + return null; + } + return parent.getLogicalPath(); + } + + public Collection getPaths() { + return getPaths(false); + } + + /** + * Obtains the {@link Path}s. + * + * @param requireSource true to return only paths containing + * Java source code, or false to return all paths + * @return the matching paths (never null) + */ + protected abstract Collection getPaths(boolean sourceOnly); + + public String getRelativeSegment(final String identifier) { + final PhysicalPath parent = getApplicablePhysicalPath(identifier); + if (parent == null) { + return null; + } + final FileDetails parentFile = new FileDetails(parent.getLocation(), + null); + return parentFile.getRelativeSegment(identifier); + } + + public String getRoot() { + return rootPath; + } + + public Collection getSourcePaths() { + return getPaths(true); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/AbstractProjectOperations.java b/project/src/main/java/org/springframework/roo/project/AbstractProjectOperations.java new file mode 100644 index 000000000..f907cdddc --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/AbstractProjectOperations.java @@ -0,0 +1,1168 @@ +package org.springframework.roo.project; + +import static org.springframework.roo.project.DependencyScope.COMPILE; +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_CYAN; +import static org.springframework.roo.support.util.AnsiEscapeCode.decorate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Provides common project operations. Should be subclassed by a + * project-specific operations subclass. + * + * @author Ben Alex + * @author Adrian Colyer + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +//@SuppressWarnings("deprecation") +@Component(componentAbstract = true) +@Reference(name = "feature", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = Feature.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public abstract class AbstractProjectOperations implements ProjectOperations { + + static final String ADDED = "added"; + static final String CHANGED = "changed"; + static final String REMOVED = "removed"; + static final String SKIPPED = "skipped"; + static final String UPDATED = "updated"; + + /** + * Generates a message about the addition of the given items to the POM + * + * @param action the past tense of the action that was performed + * @param items the items that were acted upon (required, can be empty) + * @param singular the singular of this type of item (required) + * @param plural the plural of this type of item (required) + * @return a non-null message + * @since 1.2.0 + */ + static String getDescriptionOfChange(final String action, + final Collection items, final String singular, + final String plural) { + if (items.isEmpty()) { + return ""; + } + return highlight(action + " " + (items.size() == 1 ? singular : plural)) + + " " + StringUtils.join(items, ", "); + } + + /** + * Highlights the given text + * + * @param text the text to highlight (can be blank) + * @return the highlighted text + */ + static String highlight(final String text) { + return decorate(text, FG_CYAN); + } + + private final Map features = new HashMap(); + + @Reference FileManager fileManager; + @Reference MetadataService metadataService; + @Reference PathResolver pathResolver; + + @Reference protected PomManagementService pomManagementService; + @Reference protected Shell shell; + + public void addBuildPlugin(final String moduleName, final Plugin plugin) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin modification prohibited at this time"); + Validate.notNull(plugin, "Plugin required"); + addBuildPlugins(moduleName, Collections.singletonList(plugin)); + } + + public void addBuildPlugins(final String moduleName, + final Collection newPlugins) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin modification prohibited at this time"); + Validate.notNull(newPlugins, "Plugins required"); + if (CollectionUtils.isEmpty(newPlugins)) { + return; + } + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so plugin addition cannot be performed"); + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final Element pluginsElement = DomUtils.createChildIfNotExists( + "/project/build/plugins", root, document); + final List existingPluginElements = XmlUtils.findElements( + "plugin", pluginsElement); + + final List addedPlugins = new ArrayList(); + final List removedPlugins = new ArrayList(); + for (final Plugin newPlugin : newPlugins) { + if (newPlugin != null) { + + // Look for any existing instances of this plugin + boolean inserted = false; + for (final Element existingPluginElement : existingPluginElements) { + final Plugin existingPlugin = new Plugin( + existingPluginElement); + if (existingPlugin.hasSameCoordinates(newPlugin)) { + // It's the same artifact, but might have a different + // version, exclusions, etc. + if (!inserted) { + // We haven't added the new one yet; do so now + pluginsElement.insertBefore( + newPlugin.getElement(document), + existingPluginElement); + inserted = true; + if (!newPlugin.getVersion().equals( + existingPlugin.getVersion())) { + // It's a genuine version change => mention the + // old and new versions in the message + addedPlugins.add(newPlugin + .getSimpleDescription()); + removedPlugins.add(existingPlugin + .getSimpleDescription()); + } + } + // Either way, we remove the previous one in case it was + // different in any way + pluginsElement.removeChild(existingPluginElement); + } + // Keep looping in case it's present more than once + } + if (!inserted) { + // We didn't encounter any existing dependencies with the + // same coordinates; add it now + pluginsElement.appendChild(newPlugin.getElement(document)); + addedPlugins.add(newPlugin.getSimpleDescription()); + } + } + } + + if (!newPlugins.isEmpty()) { + final String message = getPomPluginsUpdateMessage(addedPlugins, + removedPlugins); + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), message, false); + } + } + + public void addDependencies(final String moduleName, + final Collection newDependencies) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited; no such module '%s'", + moduleName); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so dependencies cannot be added"); + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element dependenciesElement = DomUtils.createChildIfNotExists( + "dependencies", document.getDocumentElement(), document); + final List existingDependencyElements = XmlUtils.findElements( + "dependency", dependenciesElement); + + final List addedDependencies = new ArrayList(); + final List removedDependencies = new ArrayList(); + final List skippedDependencies = new ArrayList(); + for (final Dependency newDependency : newDependencies) { + if (pom.canAddDependency(newDependency)) { + // Look for any existing instances of this dependency + boolean inserted = false; + for (final Element existingDependencyElement : existingDependencyElements) { + final Dependency existingDependency = new Dependency( + existingDependencyElement); + if (existingDependency.hasSameCoordinates(newDependency)) { + // It's the same artifact, but might have a different + // version, exclusions, etc. + if (!inserted) { + // We haven't added the new one yet; do so now + dependenciesElement.insertBefore( + newDependency.getElement(document), + existingDependencyElement); + inserted = true; + if (!newDependency.getVersion().equals( + existingDependency.getVersion())) { + // It's a genuine version change => mention the + // old and new versions in the message + addedDependencies.add(newDependency + .getSimpleDescription()); + removedDependencies.add(existingDependency + .getSimpleDescription()); + } + } + // Either way, we remove the previous one in case it was + // different in any way + dependenciesElement + .removeChild(existingDependencyElement); + } + // Keep looping in case it's present more than once + } + if (!inserted) { + // We didn't encounter any existing dependencies with the + // same coordinates; add it now + dependenciesElement.appendChild(newDependency + .getElement(document)); + addedDependencies.add(newDependency.getSimpleDescription()); + } + } + else { + skippedDependencies.add(newDependency.getSimpleDescription()); + } + } + if (!newDependencies.isEmpty() || !skippedDependencies.isEmpty()) { + final String message = getPomDependenciesUpdateMessage( + addedDependencies, removedDependencies, skippedDependencies); + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), message, false); + } + } + + public void addDependency(final String moduleName, + final Dependency dependency) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited at this time"); + Validate.notNull(dependency, "Dependency required"); + addDependencies(moduleName, Collections.singletonList(dependency)); + } + + public final void addDependency(final String moduleName, + final String groupId, final String artifactId, final String version) { + addDependency(moduleName, groupId, artifactId, version, COMPILE); + } + + public final void addDependency(final String moduleName, + final String groupId, final String artifactId, + final String version, final DependencyScope scope) { + addDependency(moduleName, groupId, artifactId, version, scope, ""); + } + + public final void addDependency(final String moduleName, + final String groupId, final String artifactId, + final String version, DependencyScope scope, final String classifier) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited at this time"); + Validate.notNull(groupId, "Group ID required"); + Validate.notNull(artifactId, "Artifact ID required"); + Validate.notBlank(version, "Version required"); + if (scope == null) { + scope = COMPILE; + } + final Dependency dependency = new Dependency(groupId, artifactId, + version, DependencyType.JAR, scope, classifier); + addDependency(moduleName, dependency); + } + + public void addFilter(final String moduleName, final Filter filter) { + Validate.isTrue(isProjectAvailable(moduleName), + "Filter modification prohibited at this time"); + Validate.notNull(filter, "Filter required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so filter addition cannot be performed"); + if (filter == null || pom.isFilterRegistered(filter)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final String descriptionOfChange; + final Element buildElement = XmlUtils.findFirstElement( + "/project/build", root); + final Element existingFilter = XmlUtils.findFirstElement( + "filters/filter['" + filter.getValue() + "']", buildElement); + if (existingFilter == null) { + // No such filter; add it + final Element filtersElement = DomUtils.createChildIfNotExists( + "filters", buildElement, document); + filtersElement.appendChild(XmlUtils.createTextElement(document, + "filter", filter.getValue())); + descriptionOfChange = highlight(ADDED + " filter") + " '" + + filter.getValue() + "'"; + } + else { + existingFilter.setTextContent(filter.getValue()); + descriptionOfChange = highlight(UPDATED + " filter") + " '" + + filter.getValue() + "'"; + } + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public void addModuleDependency(final String moduleToDependUpon) { + if (StringUtils.isBlank(moduleToDependUpon)) { + return; // No need to ever add a dependency upon the root POM + } + final Pom focusedModule = getFocusedModule(); + if (focusedModule != null + && StringUtils.isNotBlank(focusedModule.getModuleName()) + && !moduleToDependUpon.equals(focusedModule.getModuleName())) { + final ProjectMetadata dependencyProject = getProjectMetadata(moduleToDependUpon); + if (dependencyProject != null) { + final Pom dependencyPom = dependencyProject.getPom(); + if (!dependencyPom.getPath().equals(focusedModule.getPath())) { + final Dependency dependency = dependencyPom + .asDependency(COMPILE); + if (!focusedModule + .hasDependencyExcludingVersion(dependency)) { + addDependency(focusedModule.getModuleName(), dependency); + detectCircularDependency(focusedModule, dependencyPom); + } + } + } + } + } + + public void addPluginRepositories(final String moduleName, + final Collection repositories) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin repository modification prohibited at this time"); + Validate.notNull(repositories, "Plugin repositories required"); + addRepositories(moduleName, repositories, "pluginRepositories", + "pluginRepository"); + } + + public void addPluginRepository(final String moduleName, + final Repository repository) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin repository modification prohibited at this time"); + Validate.notNull(repository, "Repository required"); + addRepository(moduleName, repository, "pluginRepositories", + "pluginRepository"); + } + + public void addProperty(final String moduleName, final Property property) { + Validate.isTrue(isProjectAvailable(moduleName), + "Property modification prohibited at this time"); + Validate.notNull(property, "Property to add required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so property addition cannot be performed"); + if (pom.isPropertyRegistered(property)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final String descriptionOfChange; + final Element existing = XmlUtils.findFirstElement( + "/project/properties/" + property.getName(), root); + if (existing == null) { + // No existing property of this name; add it + final Element properties = DomUtils.createChildIfNotExists( + "properties", document.getDocumentElement(), document); + properties.appendChild(XmlUtils.createTextElement(document, + property.getName(), property.getValue())); + descriptionOfChange = highlight(ADDED + " property") + " '" + + property.getName() + "' = '" + property.getValue() + "'"; + } + else { + // A property of this name exists; update it + existing.setTextContent(property.getValue()); + descriptionOfChange = highlight(UPDATED + " property") + " '" + + property.getName() + "' to '" + property.getValue() + "'"; + } + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public void addRepositories(final String moduleName, + final Collection repositories) { + addRepositories(moduleName, repositories, "repositories", "repository"); + } + + private void addRepositories(final String moduleName, + final Collection repositories, + final String containingPath, final String path) { + Validate.isTrue(isProjectAvailable(moduleName), + "Repository modification prohibited at this time"); + Validate.notNull(repositories, "Repositories required"); + + if (CollectionUtils.isEmpty(repositories)) { + return; + } + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so repository addition cannot be performed"); + if ("pluginRepository".equals(path)) { + if (pom.isAllPluginRepositoriesRegistered(repositories)) { + return; + } + } + else if (pom.isAllRepositoriesRegistered(repositories)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element repositoriesElement = DomUtils.createChildIfNotExists( + containingPath, document.getDocumentElement(), document); + + final List addedRepositories = new ArrayList(); + for (final Repository repository : repositories) { + if ("pluginRepository".equals(path)) { + if (pom.isPluginRepositoryRegistered(repository)) { + continue; + } + } + else { + if (pom.isRepositoryRegistered(repository)) { + continue; + } + } + if (repository != null) { + repositoriesElement.appendChild(repository.getElement(document, + path)); + addedRepositories.add(repository.getUrl()); + } + } + final String message = getDescriptionOfChange(ADDED, addedRepositories, + path, containingPath); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), message, false); + } + + public void addRepository(final String moduleName, + final Repository repository) { + addRepository(moduleName, repository, "repositories", "repository"); + } + + private void addRepository(final String moduleName, + final Repository repository, final String containingPath, + final String path) { + Validate.isTrue(isProjectAvailable(moduleName), + "Repository modification prohibited at this time"); + Validate.notNull(repository, "Repository required"); + addRepositories(moduleName, Collections.singletonList(repository), + containingPath, path); + } + + public void addResource(final String moduleName, final Resource resource) { + Validate.isTrue(isProjectAvailable(moduleName), + "Resource modification prohibited at this time"); + Validate.notNull(resource, "Resource to add required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so resource addition cannot be performed"); + if (pom.isResourceRegistered(resource)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element buildElement = XmlUtils.findFirstElement( + "/project/build", document.getDocumentElement()); + final Element resourcesElement = DomUtils.createChildIfNotExists( + "resources", buildElement, document); + resourcesElement.appendChild(resource.getElement(document)); + final String descriptionOfChange = highlight(ADDED + " resource") + " " + + resource.getSimpleDescription(); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + protected void bindFeature(final Feature feature) { + if (feature != null) { + features.put(feature.getName(), feature); + } + } + + @Deprecated + public void buildPluginUpdate(final String moduleName, final Plugin plugin) { + updateBuildPlugin(moduleName, plugin); + } + + // TODO doesn't seem to work + private void detectCircularDependency(final Pom module1, final Pom module2) { + if (module1.isDependencyRegistered(module2.asDependency(COMPILE)) + && module2 + .isDependencyRegistered(module1.asDependency(COMPILE))) { + throw new IllegalStateException("Circular dependency detected, '" + + module1.getModuleName() + "' depends on '" + + module2.getModuleName() + "' and vice versa"); + } + } + + public Pom getFocusedModule() { + final ProjectMetadata focusedProjectMetadata = getFocusedProjectMetadata(); + if (focusedProjectMetadata == null) { + return null; + } + return focusedProjectMetadata.getPom(); + } + + public String getFocusedModuleName() { + return pomManagementService.getFocusedModuleName(); + } + + public ProjectMetadata getFocusedProjectMetadata() { + return getProjectMetadata(getFocusedModuleName()); + } + + public String getFocusedProjectName() { + return getProjectName(getFocusedModuleName()); + } + + public JavaPackage getFocusedTopLevelPackage() { + return getTopLevelPackage(getFocusedModuleName()); + } + + public Pom getModuleForFileIdentifier(final String fileIdentifier) { + return pomManagementService.getModuleForFileIdentifier(fileIdentifier); + } + + public Collection getModuleNames() { + return pomManagementService.getModuleNames(); + } + + public PathResolver getPathResolver() { + return pathResolver; + } + + public final Pom getPomFromModuleName(final String moduleName) { + final ProjectMetadata projectMetadata = getProjectMetadata(moduleName); + return projectMetadata == null ? null : projectMetadata.getPom(); + } + + public Collection getPoms() { + return pomManagementService.getPoms(); + } + + private String getPomDependenciesUpdateMessage( + final Collection addedDependencies, + final Collection removedDependencies, + final Collection skippedDependencies) { + final List changes = new ArrayList(); + changes.add(getDescriptionOfChange(ADDED, addedDependencies, + "dependency", "dependencies")); + changes.add(getDescriptionOfChange(REMOVED, removedDependencies, + "dependency", "dependencies")); + changes.add(getDescriptionOfChange(SKIPPED, skippedDependencies, + "dependency", "dependencies")); + for (final Iterator iter = changes.iterator(); iter.hasNext();) { + if (StringUtils.isBlank(iter.next())) { + iter.remove(); + } + } + return StringUtils.join(changes, "; "); + } + + private String getPomPluginsUpdateMessage( + final Collection addedPlugins, + final Collection removedPlugins) { + final List changes = new ArrayList(); + changes.add(getDescriptionOfChange(ADDED, addedPlugins, "plugin", + "plugins")); + changes.add(getDescriptionOfChange(REMOVED, removedPlugins, "plugin", + "plugins")); + for (final Iterator iter = changes.iterator(); iter.hasNext();) { + if (StringUtils.isBlank(iter.next())) { + iter.remove(); + } + } + return StringUtils.join(changes, "; "); + } + + public final ProjectMetadata getProjectMetadata(final String moduleName) { + return (ProjectMetadata) metadataService.get(ProjectMetadata + .getProjectIdentifier(moduleName)); + } + + public String getProjectName(final String moduleName) { + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, "A pom with module name '%s' could not be found", + moduleName); + return pom.getDisplayName(); + } + + public JavaPackage getTopLevelPackage(final String moduleName) { + final Pom pom = getPomFromModuleName(moduleName); + if (pom != null) { + return new JavaPackage(pom.getGroupId()); + } + return null; + } + + public boolean isFeatureInstalled(final String featureName) { + final Feature feature = features.get(featureName); + if (feature == null) { + return false; + } + for (final String moduleName : getModuleNames()) { + if (feature.isInstalledInModule(moduleName)) { + return true; + } + } + return false; + } + + public boolean isFeatureInstalledInModule(String featureName, + String moduleName) { + final Feature feature = features.get(featureName); + if (feature == null) { + return false; + } + if (feature.isInstalledInModule(moduleName)) { + return true; + } + return false; + } + + public boolean isFeatureInstalledInFocusedModule( + final String... featureNames) { + for (final String featureName : featureNames) { + final Feature feature = features.get(featureName); + if (feature != null + && feature.isInstalledInModule(getFocusedModuleName())) { + return true; + } + } + return false; + } + + public boolean isFocusedProjectAvailable() { + return isProjectAvailable(getFocusedModuleName()); + } + + public boolean isModuleCreationAllowed() { + return isProjectAvailable(""); + } + + public boolean isModuleFocusAllowed() { + return getModuleNames().size() > 1; + } + + public final boolean isProjectAvailable(final String moduleName) { + return getProjectMetadata(moduleName) != null; + } + + public void removeBuildPlugin(final String moduleName, final Plugin plugin) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin modification prohibited at this time"); + Validate.notNull(plugin, "Plugin required"); + removeBuildPlugins(moduleName, Collections.singletonList(plugin)); + } + + public void removeBuildPluginImmediately(final String moduleName, + final Plugin plugin) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin modification prohibited at this time"); + Validate.notNull(plugin, "Plugin required"); + removeBuildPlugins(moduleName, Collections.singletonList(plugin), true); + } + + public void removeBuildPlugins(final String moduleName, + final Collection plugins) { + removeBuildPlugins(moduleName, plugins, false); + } + + private void removeBuildPlugins(final String moduleName, + final Collection plugins, + final boolean writeImmediately) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin modification prohibited at this time"); + Validate.notNull(plugins, "Plugins required"); + if (CollectionUtils.isEmpty(plugins)) { + return; + } + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so plugin removal cannot be performed"); + if (!pom.isAnyPluginsRegistered(plugins)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final Element pluginsElement = XmlUtils.findFirstElement( + "/project/build/plugins", root); + if (pluginsElement == null) { + return; + } + + final List removedPlugins = new ArrayList(); + for (final Plugin plugin : plugins) { + // Can't filter the XPath on groupId, as it's optional in the POM + // for Apache-owned plugins + for (final Element candidate : XmlUtils.findElements( + "plugin[artifactId = '" + plugin.getArtifactId() + + "' and version = '" + plugin.getVersion() + "']", + pluginsElement)) { + final Plugin candidatePlugin = new Plugin(candidate); + if (candidatePlugin.getGroupId().equals(plugin.getGroupId())) { + // This element has the same groupId, artifactId, and + // version as the plugin to be removed; remove it + pluginsElement.removeChild(candidate); + removedPlugins.add(candidatePlugin.getSimpleDescription()); + // Keep looping in case this plugin is in the POM more than + // once (unlikely) + } + } + } + if (removedPlugins.isEmpty()) { + return; + } + DomUtils.removeTextNodes(pluginsElement); + final String message = getDescriptionOfChange(REMOVED, removedPlugins, + "plugin", "plugins"); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), message, writeImmediately); + } + + public void removeDependencies(final String moduleName, + final Collection dependenciesToRemove) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited at this time"); + Validate.notNull(dependenciesToRemove, "Dependencies required"); + if (CollectionUtils.isEmpty(dependenciesToRemove)) { + return; + } + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so dependency removal cannot be performed"); + if (!pom.isAnyDependenciesRegistered(dependenciesToRemove)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final Element dependenciesElement = XmlUtils.findFirstElement( + "/project/dependencies", root); + if (dependenciesElement == null) { + return; + } + + final List existingDependencyElements = XmlUtils.findElements( + "dependency", dependenciesElement); + final List removedDependencies = new ArrayList(); + for (final Dependency dependencyToRemove : dependenciesToRemove) { + if (pom.isDependencyRegistered(dependencyToRemove)) { + for (final Iterator iter = existingDependencyElements + .iterator(); iter.hasNext();) { + final Element candidate = iter.next(); + final Dependency candidateDependency = new Dependency( + candidate); + if (candidateDependency.equals(dependencyToRemove)) { + // It's the same dependency; remove it + dependenciesElement.removeChild(candidate); + // Ensure we don't try to remove it again for another + // Dependency + iter.remove(); + removedDependencies.add(candidateDependency + .getSimpleDescription()); + } + // Keep looping in case it's in the POM more than once + } + } + } + if (removedDependencies.isEmpty()) { + return; + } + DomUtils.removeTextNodes(dependenciesElement); + final String message = getDescriptionOfChange(REMOVED, + removedDependencies, "dependency", "dependencies"); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), message, false); + } + + public void removeDependency(final String moduleName, + final Dependency dependency) { + removeDependency(moduleName, dependency, "/project/dependencies", + "/project/dependencies/dependency"); + } + + /** + * Removes an element identified by the given dependency, whenever it occurs + * at the given path + * + * @param moduleName the name of the module to remove the dependency from + * @param dependency the dependency to remove + * @param containingPath the path to the dependencies element + * @param path the path to the individual dependency elements + */ + private void removeDependency(final String moduleName, + final Dependency dependency, final String containingPath, + final String path) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited at this time"); + Validate.notNull(dependency, "Dependency to remove required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so dependency removal cannot be performed"); + if (!pom.isDependencyRegistered(dependency)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + + String descriptionOfChange = ""; + final Element dependenciesElement = XmlUtils.findFirstElement( + containingPath, root); + for (final Element candidate : XmlUtils.findElements(path, root)) { + if (dependency.equals(new Dependency(candidate))) { + dependenciesElement.removeChild(candidate); + descriptionOfChange = highlight(REMOVED + " dependency") + " " + + dependency.getSimpleDescription(); + // Stay in the loop, just in case it was in the POM more than + // once + } + } + + DomUtils.removeTextNodes(dependenciesElement); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public final void removeDependency(final String moduleName, + final String groupId, final String artifactId, final String version) { + removeDependency(moduleName, groupId, artifactId, version, ""); + } + + public final void removeDependency(final String moduleName, + final String groupId, final String artifactId, + final String version, final String classifier) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited at this time"); + Validate.notNull(groupId, "Group ID required"); + Validate.notNull(artifactId, "Artifact ID required"); + Validate.notBlank(version, "Version required"); + final Dependency dependency = new Dependency(groupId, artifactId, + version, DependencyType.JAR, COMPILE, classifier); + removeDependency(moduleName, dependency); + } + + public void removeFilter(final String moduleName, final Filter filter) { + Validate.isTrue(isProjectAvailable(moduleName), + "Filter modification prohibited at this time"); + Validate.notNull(filter, "Filter required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so filter removal cannot be performed"); + if (filter == null || !pom.isFilterRegistered(filter)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + + final Element filtersElement = XmlUtils.findFirstElement( + "/project/build/filters", root); + if (filtersElement == null) { + return; + } + + String descriptionOfChange = ""; + for (final Element candidate : XmlUtils.findElements("filter", + filtersElement)) { + if (filter.equals(new Filter(candidate))) { + filtersElement.removeChild(candidate); + descriptionOfChange = highlight(REMOVED + " filter") + " '" + + filter.getValue() + "'"; + // We will not break the loop (even though we could + // theoretically), just in case it was in the POM more than once + } + } + + final List filterElements = XmlUtils.findElements("filter", + filtersElement); + if (filterElements.isEmpty()) { + filtersElement.getParentNode().removeChild(filtersElement); + } + + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public void removePluginRepository(final String moduleName, + final Repository repository) { + Validate.isTrue(isProjectAvailable(moduleName), + "Plugin repository modification prohibited at this time"); + Validate.notNull(repository, "Repository required"); + removeRepository(moduleName, repository, + "/project/pluginRepositories/pluginRepository"); + } + + public void removeProperty(final String moduleName, final Property property) { + Validate.isTrue(isProjectAvailable(moduleName), + "Property modification prohibited at this time"); + Validate.notNull(property, "Property to remove required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so property removal cannot be performed"); + if (!pom.isPropertyRegistered(property)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final Element propertiesElement = XmlUtils.findFirstElement( + "/project/properties", root); + String descriptionOfChange = ""; + for (final Element candidate : XmlUtils.findElements( + "/project/properties/*", document.getDocumentElement())) { + if (property.equals(new Property(candidate))) { + propertiesElement.removeChild(candidate); + descriptionOfChange = highlight(REMOVED + " property") + " " + + property.getName(); + // Stay in the loop just in case it was in the POM more than + // once + } + } + + DomUtils.removeTextNodes(propertiesElement); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public void removeRepository(final String moduleName, + final Repository repository) { + removeRepository(moduleName, repository, + "/project/repositories/repository"); + } + + private void removeRepository(final String moduleName, + final Repository repository, final String path) { + Validate.isTrue(isProjectAvailable(moduleName), + "Repository modification prohibited at this time"); + Validate.notNull(repository, "Repository required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so repository removal cannot be performed"); + if ("pluginRepository".equals(path)) { + if (!pom.isPluginRepositoryRegistered(repository)) { + return; + } + } + else { + if (!pom.isRepositoryRegistered(repository)) { + return; + } + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + + String descriptionOfChange = ""; + for (final Element candidate : XmlUtils.findElements(path, root)) { + if (repository.equals(new Repository(candidate))) { + candidate.getParentNode().removeChild(candidate); + descriptionOfChange = highlight(REMOVED + " repository") + " " + + repository.getUrl(); + // We stay in the loop just in case it was in the POM more than + // once + } + } + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public void removeResource(final String moduleName, final Resource resource) { + Validate.isTrue(isProjectAvailable(moduleName), + "Resource modification prohibited at this time"); + Validate.notNull(resource, "Resource required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so resource removal cannot be performed"); + if (!pom.isResourceRegistered(resource)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final Element resourcesElement = XmlUtils.findFirstElement( + "/project/build/resources", root); + if (resourcesElement == null) { + return; + } + String descriptionOfChange = ""; + for (final Element candidate : XmlUtils.findElements( + "resource[directory = '" + resource.getDirectory() + "']", + resourcesElement)) { + if (resource.equals(new Resource(candidate))) { + resourcesElement.removeChild(candidate); + descriptionOfChange = highlight(REMOVED + " resource") + " " + + resource.getSimpleDescription(); + // Stay in the loop just in case it was in the POM more than + // once + } + } + + final List resourceElements = XmlUtils.findElements( + "resource", resourcesElement); + if (resourceElements.isEmpty()) { + resourcesElement.getParentNode().removeChild(resourcesElement); + } + + DomUtils.removeTextNodes(root); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } + + public void setModule(final Pom module) { + // Update window title with project name + shell.flash(Level.FINE, + "Spring Roo: " + getTopLevelPackage(module.getModuleName()), + Shell.WINDOW_TITLE_SLOT); + shell.setPromptPath(module.getModuleName()); + pomManagementService.setFocusedModule(module); + } + + protected void unbindFeature(final Feature feature) { + if (feature != null) { + features.remove(feature.getName()); + } + } + + public void updateBuildPlugin(final String moduleName, final Plugin plugin) { + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so plugins cannot be modified at this time"); + Validate.notNull(plugin, "Plugin required"); + for (final Plugin existingPlugin : pom.getBuildPlugins()) { + if (existingPlugin.equals(plugin)) { + // Already exists, so just quit + return; + } + } + + // Delete any existing plugin with a different version + removeBuildPlugin(moduleName, plugin); + + // Add the plugin + addBuildPlugin(moduleName, plugin); + } + + public void updateDependencyScope(final String moduleName, + final Dependency dependency, final DependencyScope dependencyScope) { + Validate.isTrue(isProjectAvailable(moduleName), + "Dependency modification prohibited at this time"); + Validate.notNull(dependency, "Dependency to update required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so updating a dependency cannot be performed"); + if (!pom.isDependencyRegistered(dependency)) { + return; + } + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element root = document.getDocumentElement(); + final Element dependencyElement = XmlUtils.findFirstElement( + "/project/dependencies/dependency[groupId = '" + + dependency.getGroupId() + "' and artifactId = '" + + dependency.getArtifactId() + "' and version = '" + + dependency.getVersion() + "']", root); + if (dependencyElement == null) { + return; + } + + final Element scopeElement = XmlUtils.findFirstElement("scope", + dependencyElement); + final String descriptionOfChange; + if (scopeElement == null) { + if (dependencyScope != null) { + dependencyElement.appendChild(new XmlElementBuilder("scope", + document).setText(dependencyScope.name().toLowerCase()) + .build()); + descriptionOfChange = highlight(ADDED + " scope") + " " + + dependencyScope.name().toLowerCase() + + " to dependency " + dependency.getSimpleDescription(); + } + else { + descriptionOfChange = null; + } + } + else { + if (dependencyScope != null) { + scopeElement.setTextContent(dependencyScope.name() + .toLowerCase()); + descriptionOfChange = highlight(CHANGED + " scope") + " to " + + dependencyScope.name().toLowerCase() + + " in dependency " + dependency.getSimpleDescription(); + } + else { + dependencyElement.removeChild(scopeElement); + descriptionOfChange = highlight(REMOVED + " scope") + + " from dependency " + + dependency.getSimpleDescription(); + } + } + + if (descriptionOfChange != null) { + fileManager + .createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), + descriptionOfChange, false); + } + } + + public void updateProjectType(final String moduleName, + final ProjectType projectType) { + Validate.notNull(projectType, "Project type required"); + final Pom pom = getPomFromModuleName(moduleName); + Validate.notNull(pom, + "The pom is not available, so the project type cannot be changed"); + + final Document document = XmlUtils.readXml(fileManager + .getInputStream(pom.getPath())); + final Element packaging = DomUtils.createChildIfNotExists("packaging", + document.getDocumentElement(), document); + if (packaging.getTextContent().equals(projectType.getType())) { + return; + } + + packaging.setTextContent(projectType.getType()); + final String descriptionOfChange = highlight(UPDATED + " project type") + + " to " + projectType.getType(); + + fileManager.createOrUpdateTextFileIfRequired(pom.getPath(), + XmlUtils.nodeToString(document), descriptionOfChange, false); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/ApplicationContextOperations.java b/project/src/main/java/org/springframework/roo/project/ApplicationContextOperations.java new file mode 100644 index 000000000..8e8434ffc --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ApplicationContextOperations.java @@ -0,0 +1,24 @@ +package org.springframework.roo.project; + +import org.springframework.roo.model.JavaPackage; + +/** + * Interface to methods available in {@link ApplicationContextOperationsImpl}. + * + * @author Ben Alex + * @since 1.1 + */ +public interface ApplicationContextOperations { + + /** + * Creates a Spring application context XML configuration file + * + * @param topLevelPackage + * @param moduleName the fully-qualified name of the Maven module to which + * the new application context belongs (empty means the root or + * only module, otherwise a relative path delimited with + * {@link java.io.File#separator}) + */ + void createMiddleTierApplicationContext(JavaPackage topLevelPackage, + String moduleName); +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/ApplicationContextOperationsImpl.java b/project/src/main/java/org/springframework/roo/project/ApplicationContextOperationsImpl.java new file mode 100644 index 000000000..cbd87e972 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ApplicationContextOperationsImpl.java @@ -0,0 +1,130 @@ +package org.springframework.roo.project; + +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; +import org.osgi.service.component.ComponentContext; + +/** + * Provides Spring application context-related operations. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +@Component +@Service +public class ApplicationContextOperationsImpl implements + ApplicationContextOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(ApplicationContextOperationsImpl.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + private FileManager fileManager; + private MetadataService metadataService; + private PathResolver pathResolver; + + public void createMiddleTierApplicationContext( + final JavaPackage topLevelPackage, final String moduleName) { + final ProjectMetadata projectMetadata = (ProjectMetadata) getMetadataService() + .get(ProjectMetadata.getProjectIdentifier(moduleName)); + Validate.notNull(projectMetadata, + "Project metadata required for module '%s'", moduleName); + final Document document = XmlUtils.readXml(FileUtils.getInputStream( + getClass(), "applicationContext-template.xml")); + final Element root = document.getDocumentElement(); + DomUtils.findFirstElementByName("context:component-scan", root) + .setAttribute("base-package", + topLevelPackage.getFullyQualifiedPackageName()); + getFileManager().createOrUpdateTextFileIfRequired(getPathResolver() + .getIdentifier( + Path.SPRING_CONFIG_ROOT.getModulePathId(moduleName), + "applicationContext.xml"), XmlUtils + .nodeToString(document), false); + getFileManager().scan(); + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on ApplicationContextOperationsImpl."); + return null; + } + }else{ + return fileManager; + } + } + + public MetadataService getMetadataService(){ + if(metadataService == null){ + // Get all Services implement MetadataService interface + try { + ServiceReference[] references = context.getAllServiceReferences(MetadataService.class.getName(), null); + + for(ServiceReference ref : references){ + return (MetadataService) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MetadataService on ApplicationContextOperationsImpl."); + return null; + } + }else{ + return metadataService; + } + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on ApplicationContextOperationsImpl."); + return null; + } + }else{ + return pathResolver; + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/AutomaticProjectUpgradeService.java b/project/src/main/java/org/springframework/roo/project/AutomaticProjectUpgradeService.java new file mode 100644 index 000000000..fa46140c3 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/AutomaticProjectUpgradeService.java @@ -0,0 +1,168 @@ +package org.springframework.roo.project; + +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.osgi.framework.Bundle; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataNotificationListener; +import org.springframework.roo.project.maven.Pom; + +/** + * Automatically upgrades a Spring Roo annotation JAR to the current version of + * Roo. If the annotation JAR is equal to or newer than the version of Roo + * running, the upgrade service makes no changes. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +public class AutomaticProjectUpgradeService implements + MetadataNotificationListener { + + private static class VersionInfo implements Comparable { + + private Integer major = 0; + private Integer minor = 0; + private Integer patch = 0; + private String qualifier = ""; + + public int compareTo(final VersionInfo v) { + if (v == null) { + throw new NullPointerException("VersionInfo is null"); + } + int result = major.compareTo(v.major); + if (result != 0) { + return result; + } + result = minor.compareTo(v.minor); + if (result != 0) { + return result; + } + result = patch.compareTo(v.patch); + if (result != 0) { + return result; + } + result = qualifier.compareTo(v.qualifier); + if (result != 0) { + return result; + } + return 0; + } + + @Override + public String toString() { + return major + "." + minor + "." + patch + "." + qualifier; + } + } + + private static final String MY_BUNDLE_SYMBOLIC_NAME = AutomaticProjectUpgradeService.class + .getPackage().getName(); + + private static final String SPRING_VERSION = "3.1.0.RELEASE"; + private VersionInfo bundleVersionInfo; + @Reference private MetadataDependencyRegistry metadataDependencyRegistry; + @Reference private ProjectOperations projectOperations; + + protected void activate(final ComponentContext componentContext) { + metadataDependencyRegistry.addNotificationListener(this); + for (final Bundle b : componentContext.getBundleContext().getBundles()) { + if (!MY_BUNDLE_SYMBOLIC_NAME.equals(b.getSymbolicName())) { + continue; + } + final Object v = b.getHeaders().get("Bundle-Version"); + if (v != null) { + final String version = v.toString(); + bundleVersionInfo = extractVersionInfoFromString(version); + } + break; + } + } + + protected void deactivate(final ComponentContext componentContext) { + metadataDependencyRegistry.removeNotificationListener(this); + } + + /** + * Extracts the version information from the string. Never throws an + * exception. + * + * @param version to extract from (can be null or empty) + * @return the version information or null if it was not in a normal form + */ + private VersionInfo extractVersionInfoFromString(final String version) { + if (StringUtils.isBlank(version)) { + return null; + } + + final String[] ver = version.split("\\."); + try { + if (ver.length == 4) { + final VersionInfo result = new VersionInfo(); + result.major = new Integer(ver[0]); + result.minor = new Integer(ver[1]); + result.patch = new Integer(ver[2]); + result.qualifier = ver[3]; + return result; + } + } + catch (final RuntimeException e) { + return null; + } + return null; + } + + public void notify(final String upstreamDependency, + final String downstreamDependency) { + if (bundleVersionInfo != null + && ProjectMetadata.isValid(upstreamDependency)) { + final String moduleName = ProjectMetadata + .getModuleName(upstreamDependency); + // Project Metadata available. + if (!projectOperations.isProjectAvailable(moduleName)) { + return; + } + + for (final Pom pom : projectOperations.getPoms()) { + final Set rooVersionResults = pom + .getPropertiesExcludingValue(new Property("roo.version")); + for (final Property existingProperty : rooVersionResults) { + final VersionInfo rooVersion = extractVersionInfoFromString(existingProperty + .getValue()); + if (rooVersion != null) { + if (rooVersion.compareTo(bundleVersionInfo) < 0) { + final Property newProperty = new Property( + existingProperty.getName(), + bundleVersionInfo.toString()); + projectOperations.addProperty(moduleName, + newProperty); + break; + } + } + } + + final Set springVersionResults = pom + .getPropertiesExcludingValue(new Property( + "spring.version")); + for (final Property existingProperty : springVersionResults) { + final VersionInfo springVersion = extractVersionInfoFromString(existingProperty + .getValue()); + if (springVersion != null) { + final VersionInfo latestSpringVersion = extractVersionInfoFromString(SPRING_VERSION); + if (springVersion.compareTo(latestSpringVersion) < 0) { + final Property newProperty = new Property( + existingProperty.getName(), + latestSpringVersion.toString()); + projectOperations.addProperty(moduleName, + newProperty); + break; + } + } + } + } + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Configuration.java b/project/src/main/java/org/springframework/roo/project/Configuration.java new file mode 100644 index 000000000..3f9fb2a8e --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Configuration.java @@ -0,0 +1,75 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * Immutable representation of an configuration specification for a (Maven) + * build plugin + * + * @author Alan Stewart + * @since 1.1 + */ +public class Configuration implements Comparable { + + /** + * Factory method + * + * @param configurationElement the XML node from which to parse the instance + * (can be null) + * @return null if a null element is given + * @since 1.2.0 + */ + public static Configuration getInstance(final Element configurationElement) { + if (configurationElement == null) { + return null; + } + return new Configuration(configurationElement); + } + + private final Element configuration; + + /** + * Constructor from an XML element. Consider using + * {@link #getInstance(Element)} instead for null-safety. + * + * @param configuration the XML element specifying the configuration + * (required) + */ + public Configuration(final Element configuration) { + Validate.notNull(configuration, "configuration must be specified"); + this.configuration = configuration; + } + + public int compareTo(final Configuration o) { + if (o == null) { + throw new NullPointerException(); + } + return XmlUtils.compareNodes(configuration, o.configuration) ? 0 : 1; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Configuration + && compareTo((Configuration) obj) == 0; + } + + /** + * Returns the XML element that defines this configuration + * + * @return a non-null element + */ + public Element getConfiguration() { + return configuration; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (configuration == null ? 0 : configuration.hashCode()); + return result; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/DefaultPathResolvingStrategy.java b/project/src/main/java/org/springframework/roo/project/DefaultPathResolvingStrategy.java new file mode 100644 index 000000000..1a790de62 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/DefaultPathResolvingStrategy.java @@ -0,0 +1,170 @@ +package org.springframework.roo.project; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.FileUtils; + +@Component +@Service +public class DefaultPathResolvingStrategy extends AbstractPathResolvingStrategy { + + protected final static Logger LOGGER = HandlerUtils + .getLogger(DefaultPathResolvingStrategy.class); + + private final Map rootModulePaths = new LinkedHashMap(); + + // ------------ OSGi component attributes ---------------- + + private BundleContext context; + + // ------------ OSGi component methods ---------------- + + @Override + protected void activate(final ComponentContext cContext) { + super.activate(cContext); + this.context = cContext.getBundleContext(); + populatePaths(getRoot()); + } + + /** + * Locates the first {@link PhysicalPath} which can be construed as a parent + * of the presented identifier. + * + * @param identifier to locate the parent of (required) + * @return the first matching parent, or null if not found + */ + @Override + protected PhysicalPath getApplicablePhysicalPath(final String identifier) { + Validate.notNull(identifier, "Identifier required"); + for (final PhysicalPath pi : rootModulePaths.values()) { + final FileDetails possibleParent = new FileDetails( + pi.getLocation(), null); + if (possibleParent.isParentOf(identifier)) { + return pi; + } + } + return null; + } + + public String getCanonicalPath(final LogicalPath path, + final JavaType javaType) { + return null; + } + + public String getFocusedCanonicalPath(final Path path, + final JavaType javaType) { + return null; + } + + // ------------ PathResolvingStrategy methods ---------------- + + public String getFocusedIdentifier(final Path path, + final String relativePath) { + return null; + } + + public LogicalPath getFocusedPath(final Path path) { + return null; + } + + public String getFocusedRoot(final Path path) { + return null; + } + + public String getIdentifier(final LogicalPath path, + final String relativePath) { + return FileUtils.ensureTrailingSeparator(rootModulePaths.get( + path.getPath()).getLocationPath()) + + relativePath; + } + + @Override + protected Collection getPaths(final boolean sourceOnly) { + final List result = new ArrayList(); + for (final PhysicalPath modulePath : rootModulePaths.values()) { + if (!sourceOnly || modulePath.isSource()) { + result.add(modulePath.getLogicalPath()); + } + } + return result; + } + + List getPhysicalPaths() { + return new ArrayList(rootModulePaths.values()); + } + + public String getRoot(final LogicalPath logicalPath) { + Validate.notNull(logicalPath, "Path required"); + final PhysicalPath pathInfo = rootModulePaths + .get(logicalPath.getPath()); + Validate.notNull(pathInfo, "Unable to determine information for path '" + + logicalPath + "'"); + final File root = pathInfo.getLocation(); + return FileUtils.getCanonicalPath(root); + } + + /** + * {@inheritDoc} + * This {@code PathResolvingStrategy} is not active if there are any other + * active strategy, otherwise it is active. + */ + public boolean isActive() { + try { + // Get all Services implement PathResolvingStrategy interface + ServiceReference[] references = context.getAllServiceReferences( + PathResolvingStrategy.class.getName(), null); + + // There aren't any other implementation, this instance is Active + if (references == null) { + return true; + } + else if (references.length == 0) { + return true; + } + + // Search for other service implementations + for (ServiceReference ref : references) { + PathResolvingStrategy strategy = (PathResolvingStrategy) context.getService(ref); + + if(!strategy.getClass().equals( this.getClass() )) { + // If there is any other impl active, this strategy is not + // active + if (strategy.isActive()) { + return false; + } + } + } + + // There aren't any other active strategy + return true; + } + catch (InvalidSyntaxException ex) { + // Cannot occur because filter param is not used + LOGGER.warning("Invalid filter expression."); + return true; + } + } + + private void populatePaths(final String projectDirectory) { + for (final Path subPath : Path.values()) { + rootModulePaths.put(subPath, + subPath.getRootModulePath(projectDirectory)); + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/DelegatePathResolver.java b/project/src/main/java/org/springframework/roo/project/DelegatePathResolver.java new file mode 100644 index 000000000..e6250574c --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/DelegatePathResolver.java @@ -0,0 +1,118 @@ +package org.springframework.roo.project; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaType; + +/** + * Abstract {@link PathResolver} implementation. + *

    + * Subclasses should be created for common build system structures. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +@Reference(name = "pathResolvingStrategy", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = PathResolvingStrategy.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) +public class DelegatePathResolver implements PathResolver { + + // Mutex + private final Object lock = new Object(); + + private final Set pathResolvingStrategies = new HashSet(); + + protected void bindPathResolvingStrategy( + final PathResolvingStrategy strategy) { + synchronized (lock) { + pathResolvingStrategies.add(strategy); + } + } + + public String getCanonicalPath(final LogicalPath path, + final JavaType javaType) { + return getStrategy().getCanonicalPath(path, javaType); + } + + public String getFocusedCanonicalPath(final Path path, + final JavaType javaType) { + return getStrategy().getFocusedCanonicalPath(path, javaType); + } + + public String getFocusedIdentifier(final Path path, + final String relativePath) { + return getStrategy().getFocusedIdentifier(path, relativePath); + } + + public LogicalPath getFocusedPath(final Path path) { + return getStrategy().getFocusedPath(path); + } + + public String getFocusedRoot(final Path path) { + return getStrategy().getFocusedRoot(path); + } + + public String getFriendlyName(final String identifier) { + return getStrategy().getFriendlyName(identifier); + } + + public String getIdentifier(final LogicalPath path, + final String relativePath) { + return getStrategy().getIdentifier(path, relativePath); + } + + public LogicalPath getPath(final String identifier) { + return getStrategy().getPath(identifier); + } + + public Collection getPaths() { + return getStrategy().getPaths(); + } + + public String getRelativeSegment(final String identifier) { + return getStrategy().getRelativeSegment(identifier); + } + + public String getRoot() { + return getStrategy().getRoot(); + } + + public String getRoot(final LogicalPath path) { + return getStrategy().getRoot(path); + } + + public Collection getSourcePaths() { + return getStrategy().getSourcePaths(); + } + + private PathResolvingStrategy getStrategy() { + PathResolvingStrategy chosenStrategy = null; + for (final PathResolvingStrategy pathResolvingStrategy : pathResolvingStrategies) { + if (pathResolvingStrategy.isActive()) { + if (chosenStrategy != null) { + throw new IllegalArgumentException( + "Multiple path resolving strategies are active :<"); + } + else { + chosenStrategy = pathResolvingStrategy; + } + } + } + return chosenStrategy; + } + + protected void unbindPathResolvingStrategy( + final PathResolvingStrategy strategy) { + synchronized (lock) { + pathResolvingStrategies.remove(strategy); + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Dependency.java b/project/src/main/java/org/springframework/roo/project/Dependency.java new file mode 100644 index 000000000..d8ba9db1e --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Dependency.java @@ -0,0 +1,442 @@ +package org.springframework.roo.project; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * Simplified immutable representation of a dependency. + *

    + * Structured after the model used by Maven and Ivy. This may be replaced in a + * future release with a more OSGi-centric model. + *

    + * According to the Maven docs, "the minimal set of information for matching a + * dependency reference against a dependencyManagement section is actually + * {groupId, artifactId, type, classifier}"; see + * http://maven.apache.org/guides/introduction + * /introduction-to-dependency-mechanism.html#Dependency_Scope + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @author Andrew Swan + * @since 1.0 + */ +public class Dependency implements Comparable { + + // Known dependency types in increasing containment order + private static final List TYPE_HIERARCHY = Arrays.asList("jar", + "war", "ear", "pom"); + + /** + * Indicates whether one dependency type is at a higher logical level than + * another. + * + * @param type1 the first dependency type to compare (required) + * @param type2 the second dependency type to compare (required) + * @return false if they are at the same level or the first is + * at a lower level + * @since 1.2.1 + */ + public static boolean isHigherLevel(final String type1, final String type2) { + final int type1Index = TYPE_HIERARCHY.indexOf(type1.toLowerCase()); + final int type2Index = TYPE_HIERARCHY.indexOf(type2.toLowerCase()); + return type2Index >= 0 && type1Index > type2Index; + } + + private final String artifactId; + private final String classifier; + private final List exclusions = new ArrayList(); + // -- Identifying + private final String groupId; + // -- Non-identifying + private final DependencyScope scope; + private final String systemPath; + private final DependencyType type; + private final String version; + + /** + * Constructs a {@link Dependency} from a Maven-style <dependency> + * element. + * + * @param dependency to parse (required) + */ + public Dependency(final Element dependency) { + // Test if it has Maven format + if (dependency.hasChildNodes() + && dependency.getElementsByTagName("artifactId").getLength() > 0) { + groupId = dependency.getElementsByTagName("groupId").item(0) + .getTextContent().trim(); + artifactId = dependency.getElementsByTagName("artifactId").item(0) + .getTextContent().trim(); + + final NodeList versionElements = dependency + .getElementsByTagName("version"); + if (versionElements.getLength() > 0) { + version = versionElements.item(0).getTextContent(); + } + else { + version = ""; + } + + // POM attributes supported in Maven 3.1 + type = DependencyType.getType(dependency); + + // POM attributes supported in Maven 3.1 + scope = DependencyScope.getScope(dependency); + if (scope == DependencyScope.SYSTEM) { + if (XmlUtils.findFirstElement("systemPath", dependency) != null) { + systemPath = XmlUtils + .findFirstElement("systemPath", dependency) + .getTextContent().trim(); + } + else { + throw new IllegalArgumentException( + "Missing declaration for system scope"); + } + } + else { + systemPath = null; + } + + classifier = DomUtils.getChildTextContent(dependency, "classifier"); + + // Parsing for exclusions + final List exclusionList = XmlUtils.findElements( + "exclusions/exclusion", dependency); + if (exclusionList.size() > 0) { + for (final Element exclusion : exclusionList) { + final Element exclusionE = XmlUtils.findFirstElement( + "groupId", exclusion); + String exclusionId = ""; + if (exclusionE != null) { + exclusionId = exclusionE.getTextContent(); + } + final Element exclusionArtifactE = XmlUtils + .findFirstElement("artifactId", exclusion); + String exclusionArtifactId = ""; + if (exclusionArtifactE != null) { + exclusionArtifactId = exclusionArtifactE + .getTextContent(); + } + if (!(exclusionArtifactId.length() < 1) + && !(exclusionId.length() < 1)) { + exclusions.add(new Dependency(exclusionId, + exclusionArtifactId, "ignored")); + } + } + } + } + // Otherwise test for Ivy format + else if (dependency.hasAttribute("org") + && dependency.hasAttribute("name") + && dependency.hasAttribute("rev")) { + artifactId = dependency.getAttribute("name"); + classifier = dependency.getAttribute("classifier"); + groupId = dependency.getAttribute("org"); + scope = DependencyScope.COMPILE; + systemPath = null; + type = DependencyType.JAR; + version = dependency.getAttribute("rev"); + // TODO: Implement exclusions parser for IVY format + } + else { + throw new IllegalStateException( + "Dependency XML format not supported or is missing a mandatory node ('" + + dependency + "')"); + } + } + + /** + * Constructor for a dependency with the given attributes. + * + * @param gav the coordinates to use (required) + * @param type the dependency type (required) + * @param scope the dependency scope (required) + * @since 1.2.1 + */ + public Dependency(final GAV gav, final DependencyType type, + final DependencyScope scope) { + this(gav.getGroupId(), gav.getArtifactId(), gav.getVersion(), type, + scope); + } + + /** + * Constructs a compile-scoped JAR dependency. + * + * @param groupId the group ID (required) + * @param artifactId the artifact ID (required) + * @param version the version (required) + */ + public Dependency(final String groupId, final String artifactId, + final String version) { + this(groupId, artifactId, version, DependencyType.JAR, + DependencyScope.COMPILE); + } + + /** + * Constructs a compile-scoped JAR dependency with optional exclusions. + * + * @param groupId the group ID (required) + * @param artifactId the artifact ID (required) + * @param version the version ID (required) + * @param exclusions the exclusions for this dependency (can be null) + */ + public Dependency(final String groupId, final String artifactId, + final String version, + final Collection exclusions) { + this(groupId, artifactId, version, DependencyType.JAR, + DependencyScope.COMPILE); + if (exclusions != null) { + this.exclusions.addAll(exclusions); + } + } + + /** + * Constructs a dependency with the given type and scope. + * + * @param groupId the group ID (required) + * @param artifactId the artifact ID (required) + * @param version the version ID (required) + * @param type the dependency type (required) + * @param scope the dependency scope (required) + */ + public Dependency(final String groupId, final String artifactId, + final String version, final DependencyType type, + final DependencyScope scope) { + this(groupId, artifactId, version, type, scope, ""); + } + + /** + * Creates an immutable {@link Dependency}. + * + * @param groupId the group ID (required) + * @param artifactId the artifact ID (required) + * @param version the version ID (required) + * @param type the dependency type (required) + * @param scope the dependency scope (required) + * @param classifier the dependency classifier (required) + */ + public Dependency(final String groupId, final String artifactId, + final String version, final DependencyType type, + final DependencyScope scope, final String classifier) { + XmlUtils.assertElementLegal(groupId); + XmlUtils.assertElementLegal(artifactId); + Validate.notBlank(version, "Version required"); + Validate.notNull(scope, "Dependency scope required"); + Validate.notNull(type, "Dependency type required"); + this.artifactId = artifactId; + this.classifier = classifier; + this.groupId = groupId; + this.scope = scope; + systemPath = null; + this.type = type; + this.version = version; + } + + /** + * Adds the given exclusion to this dependency + * + * @param exclusionGroupId the groupId of the dependency to exclude + * (required) + * @param exclusionArtifactId the artifactId of the dependency to exclude + * (required) + */ + public void addExclusion(final String exclusionGroupId, + final String exclusionArtifactId) { + Validate.notBlank(exclusionGroupId, "Excluded groupId required"); + Validate.notBlank(exclusionArtifactId, "Excluded artifactId required"); + exclusions.add(new Dependency(exclusionGroupId, exclusionArtifactId, + "ignored")); + } + + /** + * Compares this dependency's identifying coordinates (i.e. not the version) + * to those of the given dependency + * + * @param other the dependency being compared to (required) + * @return see {@link Comparable#compareTo(Object)} + */ + private int compareCoordinates(final Dependency other) { + Validate.notNull(other, "Dependency being compared to cannot be null"); + int result = groupId.compareTo(other.getGroupId()); + if (result == 0) { + result = artifactId.compareTo(other.getArtifactId()); + } + if (result == 0) { + result = StringUtils.stripToEmpty(classifier).compareTo( + StringUtils.stripToEmpty(other.getClassifier())); + } + if (result == 0 && type != null) { + result = type.compareTo(other.getType()); + } + return result; + } + + public int compareTo(final Dependency o) { + final int result = compareCoordinates(o); + if (result != 0) { + return result; + } + return version.compareTo(o.getVersion()); + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Dependency && compareTo((Dependency) obj) == 0; + } + + public String getArtifactId() { + return artifactId; + } + + public String getClassifier() { + return classifier; + } + + /** + * Returns the XML element for this dependency + * + * @param document the parent XML document + * @return a non-null element + * @since 1.2.0 + */ + public Element getElement(final Document document) { + final Element dependencyElement = document.createElement("dependency"); + dependencyElement.appendChild(XmlUtils.createTextElement(document, + "groupId", groupId)); + dependencyElement.appendChild(XmlUtils.createTextElement(document, + "artifactId", artifactId)); + dependencyElement.appendChild(XmlUtils.createTextElement(document, + "version", version)); + + if (type != null && type != DependencyType.JAR) { + // Keep the XML short, we don't need "JAR" given it's the default + final Element typeElement = XmlUtils.createTextElement(document, + "type", type.toString().toLowerCase()); + dependencyElement.appendChild(typeElement); + } + + // Keep the XML short, we don't need "compile" given it's the default + if (scope != null && scope != DependencyScope.COMPILE) { + dependencyElement.appendChild(XmlUtils.createTextElement(document, + "scope", scope.toString().toLowerCase())); + if (scope == DependencyScope.SYSTEM + && StringUtils.isNotBlank(systemPath)) { + dependencyElement.appendChild(XmlUtils.createTextElement( + document, "systemPath", systemPath)); + } + } + + if (StringUtils.isNotBlank(classifier)) { + dependencyElement.appendChild(XmlUtils.createTextElement(document, + "classifier", classifier)); + } + + // Add exclusions if any + if (!exclusions.isEmpty()) { + final Element exclusionsElement = DomUtils.createChildElement( + "exclusions", dependencyElement, document); + for (final Dependency exclusion : exclusions) { + final Element exclusionElement = DomUtils.createChildElement( + "exclusion", exclusionsElement, document); + exclusionElement.appendChild(XmlUtils.createTextElement( + document, "groupId", exclusion.getGroupId())); + exclusionElement.appendChild(XmlUtils.createTextElement( + document, "artifactId", exclusion.getArtifactId())); + } + } + + return dependencyElement; + } + + /** + * @return list of exclusions (never null) + */ + public List getExclusions() { + return exclusions; + } + + public String getGroupId() { + return groupId; + } + + public DependencyScope getScope() { + return scope; + } + + /** + * @return a simple description, as would be used for console output + */ + public String getSimpleDescription() { + return groupId + ":" + artifactId + ":" + version + + (StringUtils.isNotBlank(classifier) ? ":" + classifier : ""); + } + + public String getSystemPath() { + return systemPath; + } + + public DependencyType getType() { + return type; + } + + public String getVersion() { + return version; + } + + @Deprecated + public String getVersionId() { + return version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (artifactId == null ? 0 : artifactId.hashCode()); + result = prime * result + (groupId == null ? 0 : groupId.hashCode()); + result = prime * result + + (classifier == null ? 0 : classifier.hashCode()); + result = prime * result + (type == null ? 0 : type.hashCode()); + return result; + } + + /** + * Indicates whether the given {@link Dependency} has the same Maven + * coordinates as this one; this is not necessarily the same as calling + * {@link #equals(Object)}, which may compare more fields beyond the basic + * coordinates. + * + * @param dependency the dependency to check (can be null) + * @return false if any coordinates are different + */ + public boolean hasSameCoordinates(final Dependency dependency) { + return dependency != null && compareCoordinates(dependency) == 0; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("groupId", groupId); + builder.append("artifactId", artifactId); + builder.append("version", version); + builder.append("type", type); + builder.append("scope", scope); + if (classifier != null) { + builder.append("classifier", classifier); + } + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/DependencyScope.java b/project/src/main/java/org/springframework/roo/project/DependencyScope.java new file mode 100644 index 000000000..c5af3448b --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/DependencyScope.java @@ -0,0 +1,46 @@ +package org.springframework.roo.project; + +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * The scope of the dependency. + * + * @author Alan Stewart + * @since 1.1 + */ +public enum DependencyScope { + COMPILE, IMPORT, PROVIDED, RUNTIME, SYSTEM, TEST; + + /** + * Parses the scope of the given dependency XML element + * + * @param dependency the element to parse (required) + * @return a non-null scope + */ + public static DependencyScope getScope(final Element dependency) { + final String scopeString; + if (dependency.hasAttribute("scope")) { + scopeString = dependency.getAttribute("scope"); + } + else { + // Check for a child element + final Element scopeElement = XmlUtils.findFirstElement("scope", + dependency); + if (scopeElement == null) { + scopeString = COMPILE.name(); + } + else { + scopeString = scopeElement.getTextContent(); + } + } + + try { + return valueOf(scopeString.toUpperCase().trim()); + } + catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid dependency scope '" + + scopeString.toUpperCase().trim() + "'", e); + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/DependencyType.java b/project/src/main/java/org/springframework/roo/project/DependencyType.java new file mode 100644 index 000000000..b354932e3 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/DependencyType.java @@ -0,0 +1,63 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +/** + * The type of a {@link Dependency}. + * + * @author Ben Alex + * @since 1.0 + */ +public enum DependencyType { + + JAR, OTHER, WAR, ZIP, APKLIB; + + /** + * Returns the type of the dependency represented by the given XML element + * + * @param dependencyElement the element from which to parse the type + * (required) + * @return a non-null type + * @since 1.2.0 + */ + public static DependencyType getType(final Element dependencyElement) { + // Find the type code, if any + final String type = getTypeCode(dependencyElement); + + // Resolve this to a DependencyType + return valueOfTypeCode(type); + } + + private static String getTypeCode(final Element dependencyElement) { + if (dependencyElement.hasAttribute("type")) { + return dependencyElement.getAttribute("type"); + } + // Read it from the "type" child element, if any + return DomUtils.getTextContent( + XmlUtils.findFirstElement("type", dependencyElement), "") + .trim(); + } + + /** + * Returns the {@link DependencyType} with the given code. + * + * @param typeCode the type code to decode (can be anything, case doesn't + * matter) + * @return {@link #OTHER} if the given code is non-blank and unrecognised + * @since 1.2.0 + */ + public static DependencyType valueOfTypeCode(final String typeCode) { + if (StringUtils.isBlank(typeCode)) { + return JAR; + } + try { + return valueOf(typeCode.toUpperCase()); + } + catch (final IllegalArgumentException invalidCode) { + return OTHER; + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Execution.java b/project/src/main/java/org/springframework/roo/project/Execution.java new file mode 100644 index 000000000..487d9922d --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Execution.java @@ -0,0 +1,191 @@ +package org.springframework.roo.project; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Immutable representation of an execution specification for a (Maven) build + * plugin + * + * @author Adrian Colyer + * @author Alan Stewart + * @author Andrew Swan + * @since 1.0 + */ +public class Execution implements Comparable { + + private final Configuration configuration; + private final List goals; + private final String id; + private final String phase; + + /** + * Constructor + * + * @param id the unique ID of this execution (required) + * @param phase the Maven life-cycle phase to which this execution is bound + * (required) + * @param configuration the execution-level configuration; can be + * null + * @param goals the goals to execute (must be at least one) + * @since 1.2.0 + */ + public Execution(final String id, final String phase, + final Configuration configuration, final String... goals) { + Validate.notNull(id, "execution id must be specified"); + Validate.notNull(phase, "execution phase must be specified"); + this.configuration = configuration; + this.goals = goals == null ? Collections. emptyList() + : Collections.unmodifiableList(Arrays.asList(goals)); + this.id = id.trim(); + this.phase = phase.trim(); + } + + /** + * Constructor for no execution-level {@link Configuration} + * + * @param id the unique ID of this execution (required) + * @param phase the Maven life-cycle phase to which this execution is bound + * (required) + * @param goals the goals to execute (must be at least one) + */ + public Execution(final String id, final String phase, final String... goals) { + this(id, phase, null, goals); + } + + public int compareTo(final Execution other) { + if (other == null) { + throw new NullPointerException(); + } + int result = id.compareTo(other.id); + if (result == 0) { + result = phase.compareTo(other.phase); + } + if (result == 0) { + final String[] thisGoals = (String[]) goals.toArray(); + final String[] oGoals = (String[]) other.goals.toArray(); + Arrays.sort(thisGoals); + Arrays.sort(oGoals); + result = Arrays.toString(thisGoals).compareTo( + Arrays.toString(oGoals)); + } + if (result == 0) { + result = ObjectUtils.compare(configuration, other.configuration); + } + return result; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Execution && compareTo((Execution) obj) == 0; + } + + /** + * Returns this execution's configuration, if any; this is separate from any + * configuration defined at the plugin level + * + * @return null if there is none + * @since 1.2.0 + */ + public Configuration getConfiguration() { + return configuration; + } + + /** + * Returns the XML element for this execution within the given Maven POM + * + * @param document the Maven POM to which to add the element (required) + * @return a non-null element + */ + public Element getElement(final Document document) { + final Element executionElement = document.createElement("execution"); + + // ID + if (StringUtils.isNotBlank(id)) { + executionElement.appendChild(XmlUtils.createTextElement(document, + "id", id)); + } + + // Phase + if (StringUtils.isNotBlank(phase)) { + executionElement.appendChild(XmlUtils.createTextElement(document, + "phase", phase)); + } + + // Goals + final Element goalsElement = DomUtils.createChildElement("goals", + executionElement, document); + for (final String goal : goals) { + goalsElement.appendChild(XmlUtils.createTextElement(document, + "goal", goal)); + } + + // Configuration + if (configuration != null) { + final Node configurationNode = document.importNode( + configuration.getConfiguration(), true); + executionElement.appendChild(configurationNode); + } + + return executionElement; + } + + /** + * Returns the goals this execution will execute + * + * @return a non-empty list + */ + public List getGoals() { + return goals; + } + + /** + * Returns the unique ID of this execution + * + * @return a non-blank ID + */ + public String getId() { + return id; + } + + /** + * Returns the Maven lifecycle phase to which this execution is bound + * + * @return a non-blank phase name + */ + public String getPhase() { + return phase; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ObjectUtils.hashCode(goals); + result = prime * result + ObjectUtils.hashCode(id); + result = prime * result + ObjectUtils.hashCode(phase); + result = prime * result + ObjectUtils.hashCode(configuration); + return result; + } + + @Override + public String toString() { + final ToStringBuilder tsb = new ToStringBuilder(this); + tsb.append("id", id); + tsb.append("phase", phase); + tsb.append("goals", goals); + tsb.append("configuration", configuration); + return tsb.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Feature.java b/project/src/main/java/org/springframework/roo/project/Feature.java new file mode 100644 index 000000000..d9715e5bf --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Feature.java @@ -0,0 +1,18 @@ +package org.springframework.roo.project; + +/** + * Implemented by classes to identify them as an installed project feature. + *

    + * For example, add-ons such as JSF and MVC can each use the + * {@link Feature#isInstalledInModule(String)} to verify that they can be + * installed in the current module. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public interface Feature { + + String getName(); + + boolean isInstalledInModule(String moduleName); +} diff --git a/project/src/main/java/org/springframework/roo/project/FeatureNames.java b/project/src/main/java/org/springframework/roo/project/FeatureNames.java new file mode 100644 index 000000000..fd211f731 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/FeatureNames.java @@ -0,0 +1,26 @@ +package org.springframework.roo.project; + +/** + * Constants class to hold feature names. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public final class FeatureNames { + + public static final String DATABASE_DOT_COM = "database.com"; + public static final String GAE = "gae"; + public static final String GWT = "gwt"; + public static final String JPA = "jpa"; + public static final String JSF = "jsf"; + public static final String MONGO = "mongo"; + public static final String MVC = "mvc"; + public static final String NEO4J = "ne04j"; + public static final String SECURITY = "security"; + + /** + * Constructor is private to prevent instantiation + */ + private FeatureNames() { + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Filter.java b/project/src/main/java/org/springframework/roo/project/Filter.java new file mode 100644 index 000000000..4a843a759 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Filter.java @@ -0,0 +1,72 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.w3c.dom.Element; + +/** + * Simplified immutable representation of a filter. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Filter implements Comparable { + + private final String value; + + /** + * Convenience constructor for creating a filter instance from a XML Element + * + * @param element containing the property definition (required) + */ + public Filter(final Element element) { + Validate.notNull(element, "Element required"); + value = element.getTextContent(); + } + + /** + * Convenience constructor creating a filter instance + * + * @param value the property value (required) + */ + public Filter(final String value) { + Validate.notBlank(value, "Value required"); + this.value = value; + } + + public int compareTo(final Filter o) { + if (o == null) { + throw new NullPointerException(); + } + return value.compareTo(o.value); + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Filter && compareTo((Filter) obj) == 0; + } + + /** + * The value of a filter + * + * @return the value + */ + public String getValue() { + return value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (value == null ? 0 : value.hashCode()); + return result; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("value", value); + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/GAV.java b/project/src/main/java/org/springframework/roo/project/GAV.java new file mode 100644 index 000000000..07f8c8c00 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/GAV.java @@ -0,0 +1,109 @@ +package org.springframework.roo.project; + +import java.util.Arrays; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * The combination of Maven-style groupId, artifactId, and version. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class GAV implements Comparable { + + /** + * Returns an instance based on the given concatenated Maven coordinates. + * + * @param coordinates the groupId, artifactId, and version, separated by + * {@link MavenUtils#COORDINATE_SEPARATOR} + * @return a non-blank instance + * @throws IllegalArgumentException if the string is not formatted as + * explained above, or if any of the elements are themselves + * invalid. + */ + public static GAV getInstance(final String coordinates) { + final String[] coordinateArray = ArrayUtils.nullToEmpty(StringUtils + .split(coordinates, MavenUtils.COORDINATE_SEPARATOR)); + Validate.isTrue( + coordinateArray.length == 3, + "Expected three coordinates, but found " + + coordinateArray.length + ": " + + Arrays.toString(coordinateArray) + + "; did you use the '" + + MavenUtils.COORDINATE_SEPARATOR + "' separator?"); + return new GAV(coordinateArray[0], coordinateArray[1], + coordinateArray[2]); + } + + private final String artifactId; + private final String groupId; + private final String version; + + /** + * Constructor + * + * @param groupId must be a valid Maven ID + * @param artifactId must be a valid Maven ID + * @param version cannot be blank + */ + public GAV(final String groupId, final String artifactId, + final String version) { + // Check + Validate.isTrue(MavenUtils.isValidMavenId(groupId), + "Invalid groupId '%s'", groupId); + Validate.isTrue(MavenUtils.isValidMavenId(artifactId), + "Invalid artifactId '%s'", artifactId); + Validate.notBlank(version, "Version is required for %s:%s", groupId, + artifactId); + + // Assign + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + public int compareTo(final GAV other) { + Validate.notNull(other, "Cannot compare %s to null", this); + int result = groupId.compareTo(other.getGroupId()); + if (result == 0) { + result = artifactId.compareTo(other.getArtifactId()); + } + if (result == 0) { + result = version.compareTo(other.getVersion()); + } + return result; + } + + @Override + public boolean equals(final Object other) { + return other == this || other instanceof GAV + && compareTo((GAV) other) == 0; + } + + public String getArtifactId() { + return artifactId; + } + + public String getGroupId() { + return groupId; + } + + public String getVersion() { + return version; + } + + @Override + public int hashCode() { + return artifactId.hashCode(); + } + + @Override + public String toString() { + // For debugging + return StringUtils.join( + ArrayUtils.toArray(groupId, artifactId, version), ":"); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/LogicalPath.java b/project/src/main/java/org/springframework/roo/project/LogicalPath.java new file mode 100644 index 000000000..c375833e1 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/LogicalPath.java @@ -0,0 +1,155 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.project.maven.Pom; + +/** + * A given {@link Path} within the context of a specific project module. + *

    + * To obtain the physical location on the file system, pass an instance of this + * class to the {@link PathResolver}. + * + * @author James Tyrrell + * @since 1.2.0 + */ +public class LogicalPath { + + /** + * The character that appears between the module name and the path name in + * the textual representation of a {@link LogicalPath}. This cannot be any + * character that could feasibly occur in a module name or {@link Path} + * name. + */ + public static final String MODULE_PATH_SEPARATOR = "|"; + + /** + * Creates an instance with the given path in the given module + * + * @param path the path to set (required) + * @param module can be blank for none + * @return a non-null instance + */ + public static LogicalPath getInstance(final Path path, final String module) { + return new LogicalPath(module, path); + } + + /** + * Creates a {@link LogicalPath} by parsing the given concatenation of + * optional module name and mandatory path name. + * + * @param modulePlusPath a string consisting of an optional module name plus + * the {@link #MODULE_PATH_SEPARATOR} plus the path, or more + * precisely: + * [module_name{@value #MODULE_PATH_SEPARATOR}]path + */ + public static LogicalPath getInstance(final String modulePlusPath) { + Validate.notBlank(modulePlusPath, "Module path required"); + final int separatorIndex = modulePlusPath + .indexOf(MODULE_PATH_SEPARATOR); + if (separatorIndex == -1) { + return new LogicalPath(null, Path.valueOf(modulePlusPath)); + } + final Path path = Path.valueOf(modulePlusPath.substring( + separatorIndex + 1, modulePlusPath.length())); + final String module = modulePlusPath.substring(0, separatorIndex); + return new LogicalPath(module, path); + } + + private final String module; + private final Path path; + + /** + * Constructor + * + * @param module the module containing the given path (can be blank) + * @param path the path within the module, if any (required) + */ + private LogicalPath(final String module, final Path path) { + Validate.notNull(path, "Path required"); + this.module = StringUtils.stripToEmpty(module); + this.path = path; + } + + public int compareTo(final LogicalPath o) { + if (o == null) { + throw new NullPointerException(); + } + return getName().compareTo(o.getName()); + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof LogicalPath && compareTo((LogicalPath) obj) == 0; + } + + /** + * Returns the name of the module containing this path, if any + * + * @return a non-null name (might be empty) + */ + public String getModule() { + return module; + } + + /** + * Returns the display name of this {@link LogicalPath}. + * + * @return a non-blank name + */ + public String getName() { + final StringBuilder name = new StringBuilder(); + if (StringUtils.isNotBlank(module)) { + name.append(module).append(MODULE_PATH_SEPARATOR); + } + name.append(path); + return name.toString(); + } + + /** + * Returns the path component of this {@link LogicalPath} + * + * @return a non-null path + */ + public Path getPath() { + return path; + } + + /** + * Returns the physical path of this logical path relative to the given POM + * + * @param pom can be null + * @return a non-null path + */ + public String getPathRelativeToPom(final Pom pom) { + return path.getPathRelativeToPom(pom); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + /** + * Indicates whether this is the root of the owning module. + * + * @return see above + */ + public boolean isModuleRoot() { + return path == Path.ROOT; + } + + /** + * Indicates whether this is the root of the entire user project. + * + * @return see above + */ + public boolean isProjectRoot() { + return isModuleRoot() && StringUtils.isBlank(module); + } + + @Override + public final String toString() { + return getName(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/MavenCommands.java b/project/src/main/java/org/springframework/roo/project/MavenCommands.java new file mode 100644 index 000000000..82e97097d --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/MavenCommands.java @@ -0,0 +1,225 @@ +package org.springframework.roo.project; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.project.packaging.JarPackaging; +import org.springframework.roo.project.packaging.PackagingProvider; +import org.springframework.roo.shell.CliAvailabilityIndicator; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + + +/** + * Shell commands for {@link MavenOperations} and also to launch native mvn + * commands. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class MavenCommands implements CommandMarker { + + protected final static Logger LOGGER = HandlerUtils.getLogger(MavenCommands.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + private static final String DEPENDENCY_ADD_COMMAND = "dependency add"; + private static final String DEPENDENCY_REMOVE_COMMAND = "dependency remove"; + private static final String MODULE_CREATE_COMMAND = "module create"; + private static final String MODULE_FOCUS_COMMAND = "module focus"; + private static final String PERFORM_ASSEMBLY_COMMAND = "perform assembly"; + private static final String PERFORM_CLEAN_COMMAND = "perform clean"; + private static final String PERFORM_COMMAND_COMMAND = "perform command"; + private static final String PERFORM_ECLIPSE_COMMAND = "perform eclipse"; + private static final String PERFORM_PACKAGE_COMMAND = "perform package"; + private static final String PERFORM_TESTS_COMMAND = "perform tests"; + private static final String PROJECT_COMMAND = "project"; + private static final String REPOSITORY_ADD_COMMAND = "maven repository add"; + private static final String REPOSITORY_REMOVE_COMMAND = "maven repository remove"; + + private MavenOperations mavenOperations; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + @CliCommand(value = DEPENDENCY_ADD_COMMAND, help = "Adds a new dependency to the Maven project object model (POM)") + public void addDependency( + @CliOption(key = "groupId", mandatory = true, help = "The group ID of the dependency") final String groupId, + @CliOption(key = "artifactId", mandatory = true, help = "The artifact ID of the dependency") final String artifactId, + @CliOption(key = "version", mandatory = true, help = "The version of the dependency") final String version, + @CliOption(key = "classifier", help = "The classifier of the dependency") final String classifier, + @CliOption(key = "scope", help = "The scope of the dependency") final DependencyScope scope) { + + getMavenOperations().addDependency(getMavenOperations().getFocusedModuleName(), + groupId, artifactId, version, scope, classifier); + } + + @CliCommand(value = REPOSITORY_ADD_COMMAND, help = "Adds a new repository to the Maven project object model (POM)") + public void addRepository( + @CliOption(key = "id", mandatory = true, help = "The ID of the repository") final String id, + @CliOption(key = "name", mandatory = false, help = "The name of the repository") final String name, + @CliOption(key = "url", mandatory = true, help = "The URL of the repository") final String url) { + + getMavenOperations().addRepository(getMavenOperations().getFocusedModuleName(), + new Repository(id, name, url)); + } + + @CliCommand(value = MODULE_CREATE_COMMAND, help = "Creates a new Maven module") + public void createModule( + @CliOption(key = "moduleName", mandatory = true, help = "The name of the module") final String moduleName, + @CliOption(key = "topLevelPackage", mandatory = true, optionContext = "update", help = "The uppermost package name (this becomes the in Maven and also the '~' value when using Roo's shell)") final JavaPackage topLevelPackage, + @CliOption(key = "java", help = "Forces a particular major version of Java to be used (will be auto-detected if unspecified; specify 6 or 7 only)") final Integer majorJavaVersion, + @CliOption(key = "parent", help = "The Maven coordinates of the parent POM, in the form \"groupId:artifactId:version\"") final GAV parentPom, + @CliOption(key = "packaging", help = "The Maven packaging of this module", unspecifiedDefaultValue = JarPackaging.NAME) final PackagingProvider packaging, + @CliOption(key = "artifactId", help = "The artifact ID of this module (defaults to moduleName if not specified)") final String artifactId) { + + getMavenOperations().createModule(topLevelPackage, parentPom, moduleName, + packaging, majorJavaVersion, artifactId); + } + + @CliCommand(value = PROJECT_COMMAND, help = "Creates a new Maven project") + public void createProject( + @CliOption(key = { "", "topLevelPackage" }, mandatory = true, optionContext = "update", help = "The uppermost package name (this becomes the in Maven and also the '~' value when using Roo's shell)") final JavaPackage topLevelPackage, + @CliOption(key = "projectName", help = "The name of the project (last segment of package name used as default)") final String projectName, + @CliOption(key = "java", help = "Forces a particular major version of Java to be used (will be auto-detected if unspecified; specify 5 or 6 or 7 only)") final Integer majorJavaVersion, + @CliOption(key = "parent", help = "The Maven coordinates of the parent POM, in the form \"groupId:artifactId:version\"") final GAV parentPom, + @CliOption(key = "packaging", help = "The Maven packaging of this project", unspecifiedDefaultValue = JarPackaging.NAME) final PackagingProvider packaging) { + + getMavenOperations().createProject(topLevelPackage, projectName, + majorJavaVersion, parentPom, packaging); + } + + @CliCommand(value = MODULE_FOCUS_COMMAND, help = "Changes focus to a different project module") + public void focusModule( + @CliOption(key = "moduleName", mandatory = true, help = "The module to focus on") final Pom module) { + + getMavenOperations().setModule(module); + } + + @CliAvailabilityIndicator(PROJECT_COMMAND) + public boolean isCreateProjectAvailable() { + + return getMavenOperations().isCreateProjectAvailable(); + } + + @CliAvailabilityIndicator({ DEPENDENCY_ADD_COMMAND, + DEPENDENCY_REMOVE_COMMAND }) + public boolean isDependencyModificationAllowed() { + + return getMavenOperations().isFocusedProjectAvailable(); + } + + @CliAvailabilityIndicator(MODULE_CREATE_COMMAND) + public boolean isModuleCreationAllowed() { + + return getMavenOperations().isModuleCreationAllowed(); + } + + @CliAvailabilityIndicator(MODULE_FOCUS_COMMAND) + public boolean isModuleFocusAllowed() { + + + return getMavenOperations().isModuleFocusAllowed(); + } + + @CliAvailabilityIndicator({ PERFORM_PACKAGE_COMMAND, + PERFORM_ECLIPSE_COMMAND, PERFORM_TESTS_COMMAND, + PERFORM_CLEAN_COMMAND, PERFORM_ASSEMBLY_COMMAND, + PERFORM_COMMAND_COMMAND }) + public boolean isPerformCommandAllowed() { + return getMavenOperations().isFocusedProjectAvailable(); + } + + @CliCommand(value = { PERFORM_COMMAND_COMMAND }, help = "Executes a user-specified Maven command") + public void mvn( + @CliOption(key = "mavenCommand", mandatory = true, help = "User-specified Maven command (eg test:test)") final String command) + throws IOException { + + getMavenOperations().executeMvnCommand(command); + } + + @CliCommand(value = DEPENDENCY_REMOVE_COMMAND, help = "Removes an existing dependency from the Maven project object model (POM)") + public void removeDependency( + @CliOption(key = "groupId", mandatory = true, help = "The group ID of the dependency") final String groupId, + @CliOption(key = "artifactId", mandatory = true, help = "The artifact ID of the dependency") final String artifactId, + @CliOption(key = "version", mandatory = true, help = "The version of the dependency") final String version, + @CliOption(key = "classifier", help = "The classifier of the dependency") final String classifier) { + + + getMavenOperations().removeDependency( + getMavenOperations().getFocusedModuleName(), groupId, artifactId, + version, classifier); + } + + @CliCommand(value = REPOSITORY_REMOVE_COMMAND, help = "Removes an existing repository from the Maven project object model (POM)") + public void removeRepository( + @CliOption(key = "id", mandatory = true, help = "The ID of the repository") final String id, + @CliOption(key = "url", mandatory = true, help = "The URL of the repository") final String url) { + + getMavenOperations().removeRepository( + getMavenOperations().getFocusedModuleName(), new Repository(id, + null, url)); + } + + @CliCommand(value = { PERFORM_ASSEMBLY_COMMAND }, help = "Executes the assembly goal via Maven") + public void runAssembly() throws IOException { + mvn("assembly:assembly"); + } + + @CliCommand(value = { PERFORM_CLEAN_COMMAND }, help = "Executes a full clean (including Eclipse files) via Maven") + public void runClean() throws IOException { + mvn("clean"); + } + + @CliCommand(value = { PERFORM_ECLIPSE_COMMAND }, help = "Sets up Eclipse configuration via Maven (only necessary if you have not installed the m2eclipse plugin in Eclipse)") + public void runEclipse() throws IOException { + mvn("eclipse:clean eclipse:eclipse"); + } + + @CliCommand(value = { PERFORM_PACKAGE_COMMAND }, help = "Packages the application using Maven, but does not execute any tests") + public void runPackage() throws IOException { + mvn("-DskipTests=true package"); + } + + @CliCommand(value = { PERFORM_TESTS_COMMAND }, help = "Executes the tests via Maven") + public void runTest() throws IOException { + mvn("test"); + } + + public MavenOperations getMavenOperations(){ + if(mavenOperations == null){ + // Get all Services implement MavenOperations interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(MavenOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (MavenOperations) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load MavenOperations on MavenCommands."); + return null; + } + }else{ + return mavenOperations; + } + + } +} diff --git a/project/src/main/java/org/springframework/roo/project/MavenOperations.java b/project/src/main/java/org/springframework/roo/project/MavenOperations.java new file mode 100644 index 000000000..95eb8506c --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/MavenOperations.java @@ -0,0 +1,67 @@ +package org.springframework.roo.project; + +import java.io.IOException; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.packaging.PackagingProvider; + +/** + * Provides Maven project operations. + * + * @author Ben Alex + * @since 1.1 + */ +public interface MavenOperations extends ProjectOperations { + + /** + * Creates a module within an existing Maven project + * + * @param topLevelPackage the top-level Java package (required) + * @param parentPom the Maven coordinates of the parent POM (can be + * null for none) + * @param moduleName the name and artifactId of the new module + * @param packagingType the packaging of the module (can be + * null to use the default) + * @param majorJavaVersion the major Java version to which this module is + * targetted (can be null to autodetect) + * @param artifactId the artifact ID of the module (defaults to moduleName) + */ + void createModule(JavaPackage topLevelPackage, GAV parentPom, + String moduleName, PackagingProvider packagingType, + Integer majorJavaVersion, String artifactId); + + /** + * Creates a Maven-based project + * + * @param topLevelPackage the top-level Java package (required) + * @param projectName the name of the project (can be blank to generate it + * from the top-level package) + * @param majorJavaVersion the major Java version to which this project is + * targetted (can be null to autodetect) + * @param parentPom the Maven coordinates of the parent POM (can be + * null for none) + * @param packagingType the packaging of the project (can be + * null to use the default) + */ + void createProject(JavaPackage topLevelPackage, String projectName, + Integer majorJavaVersion, GAV parentPom, + PackagingProvider packagingType); + + /** + * Executes the given Maven command + * + * @param command the command and any arguments it requires (e.g. + * "-o clean install") + * @throws IOException + */ + void executeMvnCommand(String command) throws IOException; + + String getProjectRoot(); + + /** + * Indicates whether a new Maven project can be created + * + * @return see above + */ + boolean isCreateProjectAvailable(); +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/MavenOperationsImpl.java b/project/src/main/java/org/springframework/roo/project/MavenOperationsImpl.java new file mode 100644 index 000000000..50a92b382 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/MavenOperationsImpl.java @@ -0,0 +1,289 @@ +package org.springframework.roo.project; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.ActiveProcessManager; +import org.springframework.roo.process.manager.ProcessManager; +import org.springframework.roo.project.packaging.PackagingProvider; +import org.springframework.roo.project.packaging.PackagingProviderRegistry; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Implementation of {@link MavenOperations}. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class MavenOperationsImpl extends AbstractProjectOperations implements + MavenOperations { + + protected final static Logger LOGGER = HandlerUtils.getLogger(MavenOperationsImpl.class); + + private PackagingProviderRegistry packagingProviderRegistry; + private ProcessManager processManager; + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + private static class LoggingInputStream extends Thread { + private InputStream inputStream; + private final ProcessManager processManager; + + /** + * Constructor + * + * @param inputStream + * @param processManager + */ + public LoggingInputStream(final InputStream inputStream, + final ProcessManager processManager) { + this.inputStream = inputStream; + this.processManager = processManager; + } + + @Override + public void run() { + ActiveProcessManager.setActiveProcessManager(processManager); + try { + for (String line : IOUtils.readLines(inputStream)) { + if (line.startsWith("[ERROR]")) { + LOGGER.severe(line); + } + else if (line.startsWith("[WARNING]")) { + LOGGER.warning(line); + } + else { + LOGGER.info(line); + } + } + } + catch (final IOException e) { + // 1st condition for *nix/Mac, 2nd condition for Windows + if (e.getMessage().contains("No such file or directory") + || e.getMessage().contains("CreateProcess error=2")) { + LOGGER.severe("Could not locate Maven executable; please ensure mvn command is in your path"); + } + } + finally { + IOUtils.closeQuietly(inputStream); + ActiveProcessManager.clearActiveProcessManager(); + } + } + } + + private void addModuleDeclaration(final String moduleName, + final Document pomDocument, final Element root) { + final Element modulesElement = createModulesElementIfNecessary( + pomDocument, root); + if (!isModuleAlreadyPresent(moduleName, modulesElement)) { + modulesElement.appendChild(XmlUtils.createTextElement(pomDocument, + "module", moduleName)); + } + } + + public void createModule(final JavaPackage topLevelPackage, + final GAV parentPom, final String moduleName, + final PackagingProvider selectedPackagingProvider, + final Integer majorJavaVersion, final String artifactId) { + Validate.isTrue(isCreateModuleAvailable(), + "Cannot create modules at this time"); + final PackagingProvider packagingProvider = getPackagingProvider(selectedPackagingProvider); + final String pathToNewPom = packagingProvider.createArtifacts( + topLevelPackage, artifactId, getJavaVersion(majorJavaVersion), + parentPom, moduleName, this); + updateParentModulePom(moduleName); + setModule(pomManagementService.getPomFromPath(pathToNewPom)); + } + + private Element createModulesElementIfNecessary(final Document pomDocument, + final Element root) { + Element modulesElement = XmlUtils.findFirstElement("/project/modules", + root); + if (modulesElement == null) { + modulesElement = pomDocument.createElement("modules"); + final Element repositories = XmlUtils.findFirstElement( + "/project/repositories", root); + root.insertBefore(modulesElement, repositories); + } + return modulesElement; + } + + public void createProject(final JavaPackage topLevelPackage, + final String projectName, final Integer majorJavaVersion, + final GAV parentPom, + final PackagingProvider selectedPackagingProvider) { + Validate.isTrue(isCreateProjectAvailable(), + "Project creation is unavailable at this time"); + final PackagingProvider packagingProvider = getPackagingProvider(selectedPackagingProvider); + packagingProvider.createArtifacts(topLevelPackage, projectName, + getJavaVersion(majorJavaVersion), parentPom, "", this); + } + + public void executeMvnCommand(final String extra) throws IOException { + + if(processManager == null){ + processManager = getProcessManager(); + } + + Validate.notNull(processManager, "ProcessManager is required"); + + final File root = new File(getProjectRoot()); + Validate.isTrue(root.isDirectory() && root.exists(), + "Project root does not currently exist as a directory ('%s')", + root.getCanonicalPath()); + + final String cmd = (File.separatorChar == '\\' ? "mvn.bat " : "mvn ") + + extra; + final Process p = Runtime.getRuntime().exec(cmd, null, root); + + // Ensure separate threads are used for logging, as per ROO-652 + final LoggingInputStream input = new LoggingInputStream( + p.getInputStream(), processManager); + final LoggingInputStream errors = new LoggingInputStream( + p.getErrorStream(), processManager); + + // Close OutputStream to avoid blocking by Maven commands that expect + // input, as per ROO-2034 + IOUtils.closeQuietly(p.getOutputStream()); + input.start(); + errors.start(); + + try { + if (p.waitFor() != 0) { + LOGGER.warning("The command '" + cmd + + "' did not complete successfully"); + } + } + catch (final InterruptedException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns the project's target Java version in POM format + * + * @param majorJavaVersion the major version provided by the user; can be + * null to auto-detect it + * @return a non-blank string + */ + private String getJavaVersion(final Integer majorJavaVersion) { + if (majorJavaVersion != null && majorJavaVersion >= 6 + && majorJavaVersion <= 7) { + return String.valueOf(majorJavaVersion); + } + // To be running Roo they must be on Java 6 or above + return "1.6"; + } + + private PackagingProvider getPackagingProvider( + final PackagingProvider selectedPackagingProvider) { + if(packagingProviderRegistry == null){ + packagingProviderRegistry = getPackagingProviderRegistry(); + } + Validate.notNull(packagingProviderRegistry, "PackagingProviderRegistry is required"); + return ObjectUtils.defaultIfNull(selectedPackagingProvider, + packagingProviderRegistry.getDefaultPackagingProvider()); + } + + public String getProjectRoot() { + return pathResolver.getRoot(Path.ROOT + .getModulePathId(pomManagementService.getFocusedModuleName())); + } + + public boolean isCreateModuleAvailable() { + return true; + } + + public boolean isCreateProjectAvailable() { + return !isProjectAvailable(getFocusedModuleName()); + } + + private boolean isModuleAlreadyPresent(final String moduleName, + final Element modulesElement) { + for (final Element element : XmlUtils.findElements("module", + modulesElement)) { + if (element.getTextContent().trim().equals(moduleName)) { + return true; + } + } + return false; + } + + private void updateParentModulePom(final String moduleName) { + final String parentPomPath = pomManagementService.getFocusedModule() + .getPath(); + final Document parentPomDocument = XmlUtils.readXml(fileManager + .getInputStream(parentPomPath)); + final Element parentPomRoot = parentPomDocument.getDocumentElement(); + DomUtils.createChildIfNotExists("packaging", parentPomRoot, + parentPomDocument).setTextContent("pom"); + addModuleDeclaration(moduleName, parentPomDocument, parentPomRoot); + final String addModuleMessage = getDescriptionOfChange(ADDED, + Collections.singleton(moduleName), "module", "modules"); + fileManager.createOrUpdateTextFileIfRequired(getFocusedModule() + .getPath(), XmlUtils.nodeToString(parentPomDocument), + addModuleMessage, false); + } + + public PackagingProviderRegistry getPackagingProviderRegistry(){ + // Get all Services implement UndoManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(PackagingProviderRegistry.class.getName(), null); + + for(ServiceReference ref : references){ + return (PackagingProviderRegistry) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PackagingProviderRegistry on MavenOperationsImpl."); + return null; + } + } + + public ProcessManager getProcessManager(){ + // Get all Services implement ProcessManager interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(ProcessManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (ProcessManager) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ProcessManager on MavenOperationsImpl."); + return null; + } + } + +} diff --git a/project/src/main/java/org/springframework/roo/project/MavenPathResolvingStrategy.java b/project/src/main/java/org/springframework/roo/project/MavenPathResolvingStrategy.java new file mode 100644 index 000000000..f42261e48 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/MavenPathResolvingStrategy.java @@ -0,0 +1,147 @@ +package org.springframework.roo.project; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.support.util.FileUtils; + +@Component +@Service +public class MavenPathResolvingStrategy extends AbstractPathResolvingStrategy { + + @Reference protected PomManagementService pomManagementService; + + /** + * Locates the first {@link PhysicalPath} which can be construed as a parent + * of the presented identifier. + * + * @param identifier to locate the parent of (required) + * @return the first matching parent, or null if not found + */ + @Override + protected PhysicalPath getApplicablePhysicalPath(final String identifier) { + Validate.notNull(identifier, "Identifier required"); + PhysicalPath physicalPath = null; + int longest = 0; + for (final Pom pom : pomManagementService.getPoms()) { + if (removeTrailingSeparator(identifier).startsWith( + removeTrailingSeparator(pom.getRoot())) + && removeTrailingSeparator(pom.getRoot()).length() > longest) { + longest = removeTrailingSeparator(pom.getRoot()).length(); + int nextLongest = 0; + for (final PhysicalPath thisPhysicalPath : pom + .getPhysicalPaths()) { + final String possibleParent = new FileDetails( + thisPhysicalPath.getLocation(), null) + .getCanonicalPath(); + if (removeTrailingSeparator(identifier).startsWith( + possibleParent) + && possibleParent.length() > nextLongest) { + nextLongest = possibleParent.length(); + physicalPath = thisPhysicalPath; + } + } + } + } + return physicalPath; + } + + public String getCanonicalPath(final LogicalPath path, + final JavaType javaType) { + return getIdentifier(path, javaType.getRelativeFileName()); + } + + public String getFocusedCanonicalPath(final Path path, + final JavaType javaType) { + return getCanonicalPath(path.getModulePathId(pomManagementService + .getFocusedModuleName()), javaType); + } + + public String getFocusedIdentifier(final Path path, + final String relativePath) { + return getIdentifier( + LogicalPath.getInstance(path, + pomManagementService.getFocusedModuleName()), + relativePath); + } + + public LogicalPath getFocusedPath(final Path path) { + final PhysicalPath physicalPath = pomManagementService + .getFocusedModule().getPhysicalPath(path); + Validate.notNull(physicalPath, "Physical path for '%s' not found", + path.name()); + return physicalPath.getLogicalPath(); + } + + public String getFocusedRoot(final Path path) { + return pomManagementService.getFocusedModule().getPathLocation(path); + } + + public String getIdentifier(final LogicalPath logicalPath, + final String relativePath) { + Validate.notNull(logicalPath, "Path required"); + Validate.notNull(relativePath, + "Relative path cannot be null, although it can be empty"); + + String initialPath = FileUtils.getCanonicalPath(getPath(logicalPath)); + initialPath = FileUtils.ensureTrailingSeparator(initialPath); + return initialPath + StringUtils.strip(relativePath, File.separator); + } + + private File getModuleRoot(final String module, final Pom pom) { + if (pom == null) { + // No POM exists for this module; we must be creating it + return new File(pomManagementService.getFocusedModule().getRoot(), + module); + } + // This is a known module; use its known root path + return new File(pom.getRoot()); + } + + private File getPath(final LogicalPath logicalPath) { + final Pom pom = pomManagementService.getPomFromModuleName(logicalPath + .getModule()); + final File moduleRoot = getModuleRoot(logicalPath.getModule(), pom); + final String pathRelativeToPom = logicalPath.getPathRelativeToPom(pom); + return new File(moduleRoot, pathRelativeToPom); + } + + @Override + protected Collection getPaths(final boolean sourceOnly) { + final Collection pathIds = new ArrayList(); + for (final Pom pom : pomManagementService.getPoms()) { + for (final PhysicalPath modulePath : pom.getPhysicalPaths()) { + if (!sourceOnly || modulePath.isSource()) { + pathIds.add(modulePath.getLogicalPath()); + } + } + } + return pathIds; + } + + public String getRoot(final LogicalPath modulePathId) { + final Pom pom = pomManagementService.getPomFromModuleName(modulePathId + .getModule()); + return pom.getPhysicalPath(modulePathId.getPath()).getLocationPath(); + } + + public boolean isActive() { + return pomManagementService.getRootPom() != null; + } + + private String removeTrailingSeparator(final String pomPath) { + if (pomPath.endsWith(File.separator)) { + return pomPath.substring(0, pomPath.length() - 1); + } + return pomPath + File.separator; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/MavenProjectMetadataProvider.java b/project/src/main/java/org/springframework/roo/project/MavenProjectMetadataProvider.java new file mode 100644 index 000000000..e73cbc30b --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/MavenProjectMetadataProvider.java @@ -0,0 +1,150 @@ +package org.springframework.roo.project; + +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.file.monitor.event.FileEvent; +import org.springframework.roo.file.monitor.event.FileEventListener; +import org.springframework.roo.file.monitor.event.FileOperation; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.metadata.MetadataItem; +import org.springframework.roo.metadata.MetadataProvider; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.uaa.UaaRegistrationService; +import org.springframework.uaa.client.UaaDetectedProducts; +import org.springframework.uaa.client.UaaDetectedProducts.ProductInfo; +import org.springframework.uaa.client.VersionHelper; +import org.springframework.uaa.client.protobuf.UaaClient.Product; + +/** + * Provides {@link ProjectMetadata}. + *

    + * For simplicity of operation, this is the only implementation shipping with + * ROO that supports {@link ProjectMetadata}. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +@Component +@Service +public class MavenProjectMetadataProvider implements MetadataProvider, + FileEventListener { + + static final String POM_RELATIVE_PATH = "/pom.xml"; + + private static final String PROVIDES_TYPE = MetadataIdentificationUtils + .create(MetadataIdentificationUtils + .getMetadataClass(ProjectMetadata.getProjectIdentifier(""))); + + @Reference FileManager fileManager; + @Reference private PomManagementService pomManagementService; + @Reference private UaaDetectedProducts uaaDetectedProducts; + @Reference private UaaRegistrationService uaaRegistrationService; + + public MetadataItem get(final String metadataId) { + Validate.isTrue(ProjectMetadata.isValid(metadataId), + "Unexpected metadata request '%s' for this provider", + metadataId); + // Just rebuild on demand. We always do this as we expect + // MetadataService to cache on our behalf + + final Pom pom = pomManagementService + .getPomFromModuleName(ProjectMetadata.getModuleName(metadataId)); + // Read the file, if it is available + if (pom == null || !fileManager.exists(pom.getPath())) { + return null; + } + + final JavaPackage topLevelPackage = new JavaPackage(pom.getGroupId()); + // Update UAA with the project name + uaaRegistrationService.registerProject( + UaaRegistrationService.SPRING_ROO, + topLevelPackage.getFullyQualifiedPackageName()); + + // Update UAA with the well-known Spring-related open source + // dependencies + for (final ProductInfo productInfo : uaaDetectedProducts + .getDetectedProductInfos()) { + if (productInfo.getProductName().equals( + UaaRegistrationService.SPRING_ROO.getName())) { + // No need to register with a less robust pom.xml-declared + // dependency metadata when we did it ourselves with a proper + // bundle version number lookup a moment ago... + continue; + } + if (productInfo.getProductName().equals( + UaaDetectedProducts.SPRING_UAA.getProductName())) { + // No need to register Spring UAA as this happens automatically + // internal to UAA + continue; + } + final Dependency dependency = new Dependency( + productInfo.getGroupId(), productInfo.getArtifactId(), + "version_is_ignored_for_searching"); + final Set dependenciesExcludingVersion = pom + .getDependenciesExcludingVersion(dependency); + if (!dependenciesExcludingVersion.isEmpty()) { + // This dependency was detected + final Dependency first = dependenciesExcludingVersion + .iterator().next(); + // Convert the detected dependency into a Product as best we can + String versionSequence = first.getVersion(); + // Version sequence given; see if it looks like a property + if (versionSequence != null && versionSequence.startsWith("${") + && versionSequence.endsWith("}")) { + // Strip the ${ } from the version sequence + final String propertyName = versionSequence.replace("${", + "").replace("}", ""); + final Set prop = pom + .getPropertiesExcludingValue(new Property( + propertyName)); + if (!prop.isEmpty()) { + // Take the first one's value and treat that as the + // version sequence + versionSequence = prop.iterator().next().getValue(); + } + } + // Handle there being no version sequence + if (versionSequence == null || "".equals(versionSequence)) { + versionSequence = "0.0.0.UNKNOWN"; + } + final Product product = VersionHelper.getProduct( + productInfo.getProductName(), versionSequence); + // Register the Spring Product with UAA + uaaRegistrationService.registerProject(product, + topLevelPackage.getFullyQualifiedPackageName()); + } + } + + return new ProjectMetadata(pom); + } + + public String getProvidesType() { + return PROVIDES_TYPE; + } + + public void onFileEvent(final FileEvent fileEvent) { + Validate.notNull(fileEvent, "File event required"); + + if (fileEvent.getFileDetails().getCanonicalPath() + .endsWith(POM_RELATIVE_PATH)) { + // Something happened to the POM + + // Don't notify if we're shutting down + if (fileEvent.getOperation() == FileOperation.MONITORING_FINISH) { + return; + } + + // Retrieval will cause an eviction and notification + pomManagementService.getPomFromPath(fileEvent.getFileDetails() + .getCanonicalPath()); + } + } +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/MavenUtils.java b/project/src/main/java/org/springframework/roo/project/MavenUtils.java new file mode 100644 index 000000000..320f70b65 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/MavenUtils.java @@ -0,0 +1,43 @@ +package org.springframework.roo.project; + +import java.util.regex.Pattern; + +/** + * Maven-related utility methods. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class MavenUtils { + + /** + * The separator conventionally used between Maven coordinates (groupId, + * artifactId, etc.) when specifying an artifact via a single string. + */ + public static final String COORDINATE_SEPARATOR = ":"; + + /** + * The pattern that a String must match in order to be considered a valid + * Maven ID (e.g. groupId or artifactId). Copied from + * org.apache.maven.project.validation.DefaultModelValidator + */ + public static final Pattern MAVEN_ID_REGEX = Pattern + .compile("[A-Za-z0-9_\\-.]+"); + + /** + * Indicates whether the given String is a valid Maven ID, i.e. matches + * {@link #MAVEN_ID_REGEX} + * + * @param id the String to check (can be null) + * @return see above + */ + public static boolean isValidMavenId(final String id) { + return id != null && MAVEN_ID_REGEX.matcher(id).matches(); + } + + /** + * Constructor is private to prevent instantiation + */ + private MavenUtils() { + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Path.java b/project/src/main/java/org/springframework/roo/project/Path.java new file mode 100644 index 000000000..c1e2cf100 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Path.java @@ -0,0 +1,161 @@ +package org.springframework.roo.project; + +import java.io.File; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.support.util.FileUtils; + +/** + * Common file paths used in Maven projects. + *

    + * {@link PathResolver}s can convert these paths to and from physical locations. + * + * @author Ben Alex + * @since 1.0 + */ +public enum Path { + + // These paths might be in a special order => don't reorder them here + + /** + * The module's root directory. + */ + ROOT(false, ""), + + /** + * The module's base directory for production Spring-related resource files. + */ + SPRING_CONFIG_ROOT(false, "src/main/resources/META-INF/spring"), + + /** + * The module sub-path containing production Java source code. + */ + SRC_MAIN_JAVA(true, "src/main/java") { + @Override + public String getPathRelativeToPom(final Pom pom) { + if (pom != null && StringUtils.isNotBlank(pom.getSourceDirectory())) { + return pom.getSourceDirectory(); + } + return getDefaultLocation(); + } + }, + + /** + * The module sub-path containing production resource files. + */ + SRC_MAIN_RESOURCES(false, "src/main/resources"), + + /** + * The module sub-path containing web resource files. + */ + SRC_MAIN_WEBAPP(false, "src/main/webapp"), + + /** + * The module sub-path containing test Java source code. + */ + SRC_TEST_JAVA(true, "src/test/java") { + @Override + public String getPathRelativeToPom(final Pom pom) { + if (pom != null + && StringUtils.isNotBlank(pom.getTestSourceDirectory())) { + return pom.getTestSourceDirectory(); + } + return getDefaultLocation(); + } + }, + + /** + * The module sub-path containing test resource files. + */ + SRC_TEST_RESOURCES(false, "src/test/resources"); + + private final String defaultLocation; + private final boolean javaSource; + + /** + * Constructor + * + * @param javaSource indicates whether this path contains Java source code + * @param defaultLocation the location relative to the module's root + * directory in which this path is located by default (can't be + * null) + */ + private Path(final boolean javaSource, final String defaultLocation) { + Validate.notNull(defaultLocation, "Default location is required"); + this.defaultLocation = defaultLocation; + this.javaSource = javaSource; + } + + /** + * Returns the default location of this path relative to the module's root + * directory + * + * @return a relative file path, e.g. "src/main/java" + */ + public String getDefaultLocation() { + return defaultLocation; + } + + /** + * Returns the {@link PhysicalPath} of this {@link Path} within the module + * to which the given POM belongs. + * + * @param pom the POM of the module in question (required) + * @return a non-null instance + */ + public PhysicalPath getModulePath(final Pom pom) { + return getModulePath(pom.getModuleName(), + FileUtils.getFirstDirectory(pom.getPath()), pom); + } + + private PhysicalPath getModulePath(final String moduleName, + final String moduleRoot, final Pom pom) { + return new PhysicalPath(getModulePathId(moduleName), new File( + moduleRoot, getPathRelativeToPom(pom))); + } + + /** + * Returns the {@link LogicalPath} for this path in the given module + * + * @param moduleName can be blank for the root or only module + * @return a non-null instance + */ + public LogicalPath getModulePathId(final String moduleName) { + return LogicalPath.getInstance(this, moduleName); + } + + /** + * Returns the physical path of this logical {@link Path} relative to the + * given POM. This implementation simply delegates to + * {@link #getDefaultLocation()}; individual enum values can override this. + * + * @param pom can be null + * @return + */ + public String getPathRelativeToPom(final Pom pom) { + return getDefaultLocation(); + } + + /** + * Returns the {@link PhysicalPath} of this {@link Path} within the root + * module, when no POM exists to customise its location. + * + * @param projectDirectory the root directory of the user project + * @return a non-null instance + */ + public PhysicalPath getRootModulePath(final String projectDirectory) { + return getModulePath("", projectDirectory, null); + } + + /** + * Indicates whether this path contains Java source code + * + * @return false if it only contains other types of source + * code, e.g. XML config files, JSPX files, property files, etc. + */ + public boolean isJavaSource() { + return javaSource; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/PathResolver.java b/project/src/main/java/org/springframework/roo/project/PathResolver.java new file mode 100644 index 000000000..dc2870edf --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/PathResolver.java @@ -0,0 +1,145 @@ +package org.springframework.roo.project; + +import java.io.File; +import java.util.Collection; + +import org.springframework.roo.file.monitor.event.FileDetails; +import org.springframework.roo.model.JavaType; + +/** + * Allows resolution between {@link File}, {@link Path} and canonical path + * {@link String}s. + *

    + * Add-ons should use this class as their primary mechanism to resolve paths in + * order to maximize future compatibility with any design refactoring, project + * structural enhancements or alternate build systems. Add-ons should generally + * avoid using {@link File} directly. + * + * @author Ben Alex + * @since 1.0 + */ +public interface PathResolver { + + /** + * Returns the canonical path to the given {@link JavaType} within the given + * {@link LogicalPath} + * + * @param path the path to the type's base package (required) + * @param javaType the type for which to get the path (required) + * @return a valid canonical path (N.B. this file might not exist) + * @since 1.2.0 + */ + String getCanonicalPath(LogicalPath path, JavaType javaType); + + /** + * Returns the canonical path of the given {@link JavaType} in the given + * {@link Path} of the currently focused module. + * + * @param path + * @param javaType + * @return + * @since 1.2.0 + */ + String getFocusedCanonicalPath(Path path, JavaType javaType); + + /** + * Returns the canonical path of the given path relative to the given + * {@link Path} of the currently focused module. + * + * @param path + * @param relativePath + * @return a canonical path + * @since 1.2.0 + */ + String getFocusedIdentifier(Path path, String relativePath); + + /** + * Returns the {@link LogicalPath} for the given {@link Path} within the + * currently focused module. + * + * @param path the path within the currently focused module (required) + * @return a non-null instance + * @since 1.2.0 + */ + LogicalPath getFocusedPath(Path path); + + /** + * @param path + * @return + * @since 1.2.0 + */ + String getFocusedRoot(Path path); + + /** + * Converts the presented canonical path into a human-friendly name. + * + * @param identifier to resolve (required) + * @return a human-friendly name for the identifier (required) + */ + String getFriendlyName(String identifier); + + /** + * Produces a canonical path for the presented {@link Path} and relative + * path. + * + * @param path to use (required) + * @param relativePath to use (cannot be null, but may be empty if referring + * to the path itself) + * @return the canonical path to the file (never null) + */ + String getIdentifier(LogicalPath path, String relativePath); + + /** + * Attempts to determine which {@link Path} the specified canonical path + * falls under. + * + * @param identifier to lookup (required) + * @return the {@link Path}, or null if the identifier refers to a location + * not under a know path. + */ + LogicalPath getPath(String identifier); + + /** + * Returns all known paths within the user project. + * + * @return a non-null list + */ + Collection getPaths(); + + /** + * Attempts to determine which {@link Path} the specified canonical path + * falls under, and then returns the relative portion of the file name. + *

    + * See {@link FileDetails#getRelativeSegment(String)} for related + * information. + * + * @param identifier to resolve (required) + * @return the relative segment (which may be an empty string if the + * identifier referred to the {@link Path} directly), or null if the + * identifier does not have a corresponding {@link Path} + */ + String getRelativeSegment(String identifier); + + /** + * Returns the canonical path of the user project's root directory + * + * @return a valid directory path + */ + String getRoot(); + + /** + * Returns the canonical path of the root of the given {@link LogicalPath}. + * + * @param path to lookup (required) + * @return null if the root path cannot be determined + */ + String getRoot(LogicalPath path); + + /** + * Returns the {@link LogicalPath}s of user project directories that can + * contain Java source code. + * + * @return a non-null list + */ + Collection getSourcePaths(); +} diff --git a/project/src/main/java/org/springframework/roo/project/PathResolvingAwareFilenameResolver.java b/project/src/main/java/org/springframework/roo/project/PathResolvingAwareFilenameResolver.java new file mode 100644 index 000000000..a10449447 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/PathResolvingAwareFilenameResolver.java @@ -0,0 +1,28 @@ +package org.springframework.roo.project; + +import java.io.File; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.file.undo.FilenameResolver; +import org.springframework.roo.support.util.FileUtils; + +/** + * {@link FilenameResolver} that delegates to {@link PathResolver}. + * + * @author Ben Alex + * @since 1.0 + */ +@Component +@Service +public class PathResolvingAwareFilenameResolver implements FilenameResolver { + + @Reference private PathResolver pathResolver; + + public String getMeaningfulName(final File file) { + Validate.notNull(file, "File required"); + return pathResolver.getFriendlyName(FileUtils.getCanonicalPath(file)); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/PathResolvingStrategy.java b/project/src/main/java/org/springframework/roo/project/PathResolvingStrategy.java new file mode 100644 index 000000000..42c1590f3 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/PathResolvingStrategy.java @@ -0,0 +1,119 @@ +package org.springframework.roo.project; + +import java.util.Collection; + +import org.springframework.roo.model.JavaType; + +/** + * A strategy for resolving logical {@link Path}s to physical file system + * locations. + * + * @author James Tyrrell + * @since 1.2.0 + */ +public interface PathResolvingStrategy { + + /** + * @param path the focus path + * @param javaType the type t + * @return + */ + String getCanonicalPath(LogicalPath path, JavaType javaType); + + /** + * @param path + * @param javaType + * @return + */ + String getFocusedCanonicalPath(Path path, JavaType javaType); + + /** + * @param path + * @param relativePath + * @return + */ + String getFocusedIdentifier(Path path, String relativePath); + + /** + * @see PathResolver#getFocusedPath(Path) + */ + LogicalPath getFocusedPath(Path path); + + /** + * @param path + * @return + */ + String getFocusedRoot(Path path); + + /** + * Converts the presented canonical path into a human-friendly name. + * + * @param identifier to resolve (required) + * @return a human-friendly name for the identifier (required) + */ + String getFriendlyName(String identifier); + + /** + * Produces a canonical path for the presented {@link LogicalPath} and + * relative path. + * + * @param path to use (required) + * @param relativePath to use (cannot be null, but may be empty if referring + * to the path itself) + * @return the canonical path to the file (never null) + */ + String getIdentifier(LogicalPath path, String relativePath); + + /** + * Attempts to determine which {@link Path} the specified canonical path + * falls under. + * + * @param identifier to lookup (required) + * @return the {@link Path}, or null if the identifier refers to a location + * not under a know path. + */ + LogicalPath getPath(String identifier); + + /** + * @see PathResolver#getPaths() + */ + Collection getPaths(); + + /** + * Attempts to determine which {@link Path} the specified canonical path + * falls under, and then returns the relative portion of the file name. + *

    + * See + * {@link org.springframework.roo.file.monitor.event.FileDetails#getRelativeSegment(String)} + * for related information. + * + * @param identifier to resolve (required) + * @return the relative segment (which may be an empty string if the + * identifier referred to the {@link Path} directly), or null if the + * identifier does not have a corresponding {@link Path} + */ + String getRelativeSegment(String identifier); + + /** + * @return the directory where Roo was launched + */ + String getRoot(); + + /** + * @see PathResolver#getRoot(LogicalPath) + */ + String getRoot(LogicalPath path); + + /** + * @see PathResolver#getSourcePaths() + */ + Collection getSourcePaths(); + + /** + * Indicates whether this strategy is active. The {@link PathResolver} will + * typically expect exactly one strategy to be active at a time. + * + * @return see above + */ + boolean isActive(); +} diff --git a/project/src/main/java/org/springframework/roo/project/PhysicalPath.java b/project/src/main/java/org/springframework/roo/project/PhysicalPath.java new file mode 100644 index 000000000..1f8fb2be8 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/PhysicalPath.java @@ -0,0 +1,80 @@ +package org.springframework.roo.project; + +import java.io.File; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.support.util.FileUtils; + +/** + * The physical location of a given {@link LogicalPath} within the user's + * project. + *

    + * Renamed from PathInformation in version 1.2.0. + * + * @author Ben Alex + * @since 1.0 + */ +public class PhysicalPath { + + private final String canonicalPath; + private final File location; + private final LogicalPath logicalPath; + + /** + * Constructor + * + * @param logicalPath (required) + * @param location the physical location of this path (required) + */ + public PhysicalPath(final LogicalPath logicalPath, final File location) { + Validate.notNull(logicalPath, "Module path required"); + Validate.notNull(location, "Location required"); + canonicalPath = FileUtils.getCanonicalPath(location); + this.logicalPath = logicalPath; + this.location = location; + } + + /** + * Returns the physical location of this path + * + * @return a non-null location + */ + public File getLocation() { + return location; + } + + /** + * Returns the canonical path of this {@link PhysicalPath} + * + * @return a non-blank canonical path + */ + public String getLocationPath() { + return canonicalPath; + } + + public LogicalPath getLogicalPath() { + return logicalPath; + } + + public Path getPath() { + return logicalPath.getPath(); + } + + /** + * Indicates whether this path contains Java source code + * + * @return see above + */ + public boolean isSource() { + return logicalPath.getPath().isJavaSource(); + } + + @Override + public final String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("logicalPath", logicalPath); + builder.append("location", location); + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Plugin.java b/project/src/main/java/org/springframework/roo/project/Plugin.java new file mode 100644 index 000000000..ea353e17c --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Plugin.java @@ -0,0 +1,392 @@ +package org.springframework.roo.project; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Simplified immutable representation of a maven build plugin. + *

    + * Structured after the model used by Maven. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Plugin implements Comparable { + + /** + * The Maven groupId that will be assigned to a plugin if one is not + * provided + */ + public static final String DEFAULT_GROUP_ID = "org.apache.maven.plugins"; + + /** + * Parses the given plugin XML element for the plugin's Maven artifactId + * + * @param plugin the XML element to parse (required) + * @return a non-blank id + */ + private static String getArtifactId(final Element plugin) { + return plugin.getElementsByTagName("artifactId").item(0) + .getTextContent(); + } + + /** + * Parses the configuration of the given plugin (global, not + * execution-scoped) + * + * @param plugin the XML element to parse (required) + * @return null if there isn't one + */ + private static Configuration getConfiguration(final Element plugin) { + return Configuration.getInstance(XmlUtils.findFirstElement( + "configuration", plugin)); + } + + /** + * Parses the given XML plugin element for the plugin's dependencies + * + * @param plugin the XML element to parse (required) + * @return a non-null list + */ + private static List getDependencies(final Element plugin) { + final List dependencies = new ArrayList(); + for (final Element dependencyElement : XmlUtils.findElements( + "dependencies/dependency", plugin)) { + // groupId + final Element groupIdElement = XmlUtils.findFirstElement("groupId", + dependencyElement); + final String groupId = DomUtils.getTextContent(groupIdElement, ""); + + // artifactId + final Element artifactIdElement = XmlUtils.findFirstElement( + "artifactId", dependencyElement); + final String artifactId = DomUtils.getTextContent( + artifactIdElement, ""); + + // version + final Element versionElement = XmlUtils.findFirstElement("version", + dependencyElement); + final String version = DomUtils.getTextContent(versionElement, ""); + + final Dependency dependency = new Dependency(groupId, artifactId, + version); + + // Parse any exclusions + for (final Element exclusion : XmlUtils.findElements( + "exclusions/exclusion", dependencyElement)) { + // groupId + final Element exclusionGroupIdElement = XmlUtils + .findFirstElement("groupId", exclusion); + final String exclusionGroupId = DomUtils.getTextContent( + exclusionGroupIdElement, ""); + + // artifactId + final Element exclusionArtifactIdElement = XmlUtils + .findFirstElement("artifactId", exclusion); + final String exclusionArtifactId = DomUtils.getTextContent( + exclusionArtifactIdElement, ""); + + if (StringUtils.isNotBlank(exclusionGroupId) + && StringUtils.isNotBlank(exclusionArtifactId)) { + dependency.addExclusion(exclusionGroupId, + exclusionArtifactId); + } + } + dependencies.add(dependency); + } + return dependencies; + } + + /** + * Parses the given XML plugin element for the plugin's executions + * + * @param plugin the XML element to parse (required) + * @return a non-null list + */ + private static List getExecutions(final Element plugin) { + final List executions = new ArrayList(); + // Loop through the "execution" elements in the plugin element + for (final Element execution : XmlUtils.findElements( + "executions/execution", plugin)) { + final Element idElement = XmlUtils + .findFirstElement("id", execution); + final String id = DomUtils.getTextContent(idElement, ""); + final Element phaseElement = XmlUtils.findFirstElement("phase", + execution); + final String phase = DomUtils.getTextContent(phaseElement, ""); + final List goals = new ArrayList(); + for (final Element goalElement : XmlUtils.findElements( + "goals/goal", execution)) { + goals.add(goalElement.getTextContent()); + } + final Configuration configuration = Configuration + .getInstance(XmlUtils.findFirstElement("configuration", + execution)); + executions.add(new Execution(id, phase, configuration, goals + .toArray(new String[goals.size()]))); + } + return executions; + } + + /** + * Parses the plugin's Maven groupId from the given element + * + * @param plugin the element to parse (required) + * @return a non-blank groupId + */ + public static String getGroupId(final Element plugin) { + if (plugin.getElementsByTagName("groupId").getLength() > 0) { + return plugin.getElementsByTagName("groupId").item(0) + .getTextContent(); + } + return DEFAULT_GROUP_ID; + } + + /** + * Parses the plugin's version number from the given XML element + * + * @param plugin the element to parse (required) + * @return a non-null version number (might be empty) + */ + private static String getVersion(final Element plugin) { + final List versionElements = XmlUtils.findElements("./version", plugin); + if (!versionElements.isEmpty()) { + return versionElements.get(0).getTextContent(); + } + return ""; + } + + private final Configuration configuration; + private final List dependencies = new ArrayList(); + private final List executions = new ArrayList(); + private final GAV gav; + + /** + * Constructor from a POM-style XML element that defines a Maven . + * + * @param plugin the XML element to parse (required) + */ + public Plugin(final Element plugin) { + this(getGroupId(plugin), getArtifactId(plugin), getVersion(plugin), + getConfiguration(plugin), getDependencies(plugin), + getExecutions(plugin)); + } + + /** + * Constructor that takes the minimal Maven artifact coordinates. + * + * @param groupId the group ID (required) + * @param artifactId the artifact ID (required) + * @param version the version (required) + */ + public Plugin(final String groupId, final String artifactId, + final String version) { + this(groupId, artifactId, version, null, null, null); + } + + /** + * Constructor that allows all fields to be set. + * + * @param groupId the group ID (required) + * @param artifactId the artifact ID (required) + * @param version the version (required) + * @param configuration the configuration for this plugin (optional) + * @param dependencies the dependencies for this plugin (can be + * null) + * @param executions the executions for this plugin (can be + * null) + */ + public Plugin(final String groupId, final String artifactId, + final String version, final Configuration configuration, + final Collection dependencies, + final Collection executions) { + Validate.notNull(groupId, "Group ID required"); + Validate.notNull(artifactId, "Artifact ID required"); + //Validate.notNull(version, "Version required"); + if(version == null || version == "") { + gav = new GAV(groupId, artifactId, "-"); + } else { + gav = new GAV(groupId, artifactId, version); + } + this.configuration = configuration; + // Defensively copy the given nullable collections + CollectionUtils.populate(this.dependencies, dependencies); + CollectionUtils.populate(this.executions, executions); + } + + public int compareTo(final Plugin o) { + if (o == null) { + throw new NullPointerException(); + } + int result = gav.compareTo(o.getGAV()); + if (result == 0 && configuration != null && o.configuration != null) { + result = configuration.compareTo(o.configuration); + } + return result; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Plugin && compareTo((Plugin) obj) == 0; + } + + public String getArtifactId() { + return gav.getArtifactId(); + } + + /** + * Returns the top-level configuration of this plugin, if any. Note that + * individual {@link Execution}s may have their own {@link Configuration}s + * instead of or in addition to this configuration. + * + * @return null if none exists + */ + public Configuration getConfiguration() { + return configuration; + } + + public List getDependencies() { + return dependencies; + } + + /** + * Returns the {@link Element} to add to the given POM {@link Document} for + * this plugin + * + * @param plugin the plugin for which to create an XML Element (required) + * @param document the document to which the element will belong (required) + * @return a non-null element + * @since 1.2.0 + */ + public Element getElement(final Document document) { + final Element pluginElement = document.createElement("plugin"); + + // Basic coordinates + pluginElement.appendChild(XmlUtils.createTextElement(document, + "groupId", getGroupId())); + pluginElement.appendChild(XmlUtils.createTextElement(document, + "artifactId", getArtifactId())); + pluginElement.appendChild(XmlUtils.createTextElement(document, + "version", getVersion())); + + // Configuration + if (configuration != null) { + final Node configuration = document.importNode( + this.configuration.getConfiguration(), true); + pluginElement.appendChild(configuration); + } + + // Executions + if (!executions.isEmpty()) { + final Element executionsElement = DomUtils.createChildElement( + "executions", pluginElement, document); + for (final Execution execution : executions) { + executionsElement.appendChild(execution.getElement(document)); + } + } + + // Dependencies + if (!dependencies.isEmpty()) { + final Element dependenciesElement = DomUtils.createChildElement( + "dependencies", pluginElement, document); + for (final Dependency dependency : dependencies) { + dependenciesElement + .appendChild(dependency.getElement(document)); + } + } + + return pluginElement; + } + + public List getExecutions() { + return executions; + } + + /** + * Returns the Maven-style coordinates of this plugin + * + * @return a non-null set of coordinates + */ + public GAV getGAV() { + return gav; + } + + /** + * Returns this plugin's groupId. + * + * @return + */ + public String getGroupId() { + return gav.getGroupId(); + } + + /** + * @return a simple description, as would be used for console output + */ + public String getSimpleDescription() { + return gav.toString(); + } + + public String getVersion() { + return gav.getVersion(); + } + + @Override + public int hashCode() { + final int prime = 31; + final int result = prime * 1 + gav.hashCode(); + return prime * result + + (configuration == null ? 0 : configuration.hashCode()); + } + + /** + * Indicates whether the given {@link Plugin} has the same Maven coordinates + * as this one; this is not necessarily the same as calling + * {@link #equals(Object)}, which may compare more fields beyond the basic + * coordinates. + * + * @param plugin the plugin to check (can be null) + * @return false if any coordinates are different + */ + public boolean hasSameCoordinates(final Plugin dependency) { + return dependency != null && compareCoordinates(dependency) == 0; + } + + /** + * Compares this plugin's identifying coordinates (i.e. not the version) to + * those of the given plugin + * + * @param other the plugin being compared to (required) + * @return see {@link Comparable#compareTo(Object)} + */ + private int compareCoordinates(final Plugin other) { + Validate.notNull(other, "Plugin being compared to cannot be null"); + int result = getGroupId().compareTo(other.getGroupId()); + if (result == 0) { + result = getArtifactId().compareTo(other.getArtifactId()); + } + return result; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("gav", gav); + if (configuration != null) { + builder.append("configuration", configuration); + } + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/PomManagementService.java b/project/src/main/java/org/springframework/roo/project/PomManagementService.java new file mode 100644 index 000000000..9790eeff5 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/PomManagementService.java @@ -0,0 +1,82 @@ +package org.springframework.roo.project; + +import java.util.Collection; + +import org.springframework.roo.project.maven.Pom; + +/** + * Provides {@link Pom}-related methods to the "project" package. Code outside + * this package should use {@link ProjectOperations}. + * + * @author James Tyrrell + * @since 1.2.0 + */ +interface PomManagementService { + + /** + * Returns the {@link ProjectDescriptor} of the currently focussed module, + * or if no module has the focus, the root {@link ProjectDescriptor}. + * + * @return null if none of the above Poms exist + */ + Pom getFocusedModule(); + + /** + * Returns the name of the currently focussed module. + * + * @return an empty string if no module has the focus. + */ + String getFocusedModuleName(); + + /** + * @param fileIdentifier the canonical path to lookup + * @return the module name that represents the module the passed in file + * belongs to + */ + Pom getModuleForFileIdentifier(String fileIdentifier); + + /** + * Returns the names of the modules of this project + * + * @return a non-null collection + */ + Collection getModuleNames(); + + /** + * Returns the {@link ProjectDescriptor} for the module with the given name. + * + * @param moduleName the name of the module to look up (can be blank) + * @return null if there's no such module + */ + Pom getPomFromModuleName(String moduleName); + + /** + * Returns the {@link ProjectDescriptor} with the given canonical path + * + * @param canonicalPath the canonical path of the descriptor file + * @return null if there is no such file + */ + Pom getPomFromPath(String canonicalPath); + + /** + * Returns the known {@link ProjectDescriptor}s + * + * @return a non-null copy of this collection + */ + Collection getPoms(); + + /** + * Returns the {@link ProjectDescriptor} associated with the project's root + * descriptor file + * + * @return null if there's no such POM + */ + Pom getRootPom(); + + /** + * Focuses on the given module. + * + * @param module the module to focus upon (required) + */ + void setFocusedModule(Pom module); +} diff --git a/project/src/main/java/org/springframework/roo/project/PomManagementServiceImpl.java b/project/src/main/java/org/springframework/roo/project/PomManagementServiceImpl.java new file mode 100644 index 000000000..6aade5e8e --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/PomManagementServiceImpl.java @@ -0,0 +1,355 @@ +package org.springframework.roo.project; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.project.maven.PomFactory; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +@Component +@Service +public class PomManagementServiceImpl implements PomManagementService { + + private static class PomComparator implements Comparator { + private final Map pomMap; + + /** + * Constructor + * + * @param pomMap + */ + private PomComparator(final Map pomMap) { + this.pomMap = pomMap; + } + + public int compare(final String s1, final String s2) { + final String p1 = pomMap.get(s1).getRoot() + SEPARATOR; + final String p2 = pomMap.get(s2).getRoot() + SEPARATOR; + if (p1.startsWith(p2)) { + return -1; + } + else if (p2.startsWith(p1)) { + return 1; + } + return 0; + } + } + + private static final String SEPARATOR = File.separator; + private static final String DEFAULT_POM_NAME = "pom.xml"; + private static final String DEFAULT_RELATIVE_PATH = ".." + SEPARATOR + + DEFAULT_POM_NAME; + + @Reference FileManager fileManager; + @Reference FileMonitorService fileMonitorService; + @Reference MetadataDependencyRegistry metadataDependencyRegistry; + @Reference MetadataService metadataService; + @Reference PomFactory pomFactory; + @Reference Shell shell; + + private String focusedModulePath; + private final Map pomMap = new LinkedHashMap(); + private String projectRootDirectory; + private final Set toBeParsed = new HashSet(); + + protected void activate(final ComponentContext context) { + final File projectDirectory = new File(StringUtils.defaultIfEmpty( + OSGiUtils.getRooWorkingDirectory(context), + FileUtils.CURRENT_DIRECTORY)); + projectRootDirectory = FileUtils.getCanonicalPath(projectDirectory); + } + + /** + * For test cases to set up the state of this service + * + * @param pom the POM to add (required) + */ + void addPom(final Pom pom) { + pomMap.put(pom.getPath(), pom); + } + + private void findUnparsedPoms() { + for (final String change : fileMonitorService.getDirtyFiles(getClass() + .getName())) { + if (change.endsWith(DEFAULT_POM_NAME)) { + toBeParsed.add(change); + } + } + } + + public Pom getFocusedModule() { + updatePomCache(); + if (focusedModulePath == null && getRootPom() != null) { + focusedModulePath = getRootPom().getPath(); + } + return getPomFromPath(focusedModulePath); + } + + public String getFocusedModuleName() { + if (getFocusedModule() == null) { + return ""; + } + return getFocusedModule().getModuleName(); + } + + public Pom getModuleForFileIdentifier(final String fileIdentifier) { + updatePomCache(); + String startingPoint = FileUtils.getFirstDirectory(fileIdentifier); + String pomPath = FileUtils.ensureTrailingSeparator(startingPoint) + + DEFAULT_POM_NAME; + File pom = new File(pomPath); + while (!pom.exists()) { + if (startingPoint.equals(SEPARATOR)) { + break; + } + startingPoint = StringUtils.removeEnd(startingPoint, SEPARATOR); + + if (startingPoint.lastIndexOf(SEPARATOR) < 0) { + break; + } + startingPoint = startingPoint.substring(0, + startingPoint.lastIndexOf(SEPARATOR)); + startingPoint = StringUtils.removeEnd(startingPoint, SEPARATOR); + + pomPath = FileUtils.ensureTrailingSeparator(startingPoint) + + DEFAULT_POM_NAME; + pom = new File(pomPath); + } + return getPomFromPath(pomPath); + } + + private String getModuleName(final String pomDirectory) { + final String normalisedRootPath = FileUtils + .ensureTrailingSeparator(projectRootDirectory); + final String normalisedPomDirectory = FileUtils + .ensureTrailingSeparator(pomDirectory); + final String moduleName = StringUtils.removeStart( + normalisedPomDirectory, normalisedRootPath); + return StringUtils.stripEnd(moduleName, SEPARATOR); + } + + public Collection getModuleNames() { + final Set moduleNames = new HashSet(); + for (final Pom module : pomMap.values()) { + moduleNames.add(module.getModuleName()); + } + return moduleNames; + } + + public Pom getPomFromModuleName(final String moduleName) { + for (final Pom pom : getPoms()) { + if (pom.getModuleName().equals(moduleName)) { + return pom; + } + } + return null; + } + + public Pom getPomFromPath(final String pomPath) { + updatePomCache(); + return pomMap.get(pomPath); + } + + public Collection getPoms() { + updatePomCache(); + return new ArrayList(pomMap.values()); + } + + public Pom getRootPom() { + updatePomCache(); + return pomMap.get(projectRootDirectory + SEPARATOR + DEFAULT_POM_NAME); + } + + private Set parseUnparsedPoms() { + final Map pomModuleMap = new HashMap(); + final Set newPoms = new HashSet(); + for (final Iterator iter = toBeParsed.iterator(); iter + .hasNext();) { + final String pathToChangedPom = iter.next(); + if (new File(pathToChangedPom).exists()) { + String pomContents = ""; + try { + pomContents = org.apache.commons.io.FileUtils + .readFileToString(new File(pathToChangedPom)); + } + catch (IOException ignored) { + } + if (StringUtils.isNotBlank(pomContents)) { + final Element rootElement = XmlUtils + .stringToElement(pomContents); + resolvePoms(rootElement, pathToChangedPom, pomModuleMap); + final String moduleName = getModuleName(FileUtils + .getFirstDirectory(pathToChangedPom)); + final Pom pom = pomFactory.getInstance(rootElement, + pathToChangedPom, moduleName); + Validate.notNull(pom, + "POM is null for module '%s' and path '%s'", + moduleName, pathToChangedPom); + pomMap.put(pathToChangedPom, pom); + newPoms.add(pom); + iter.remove(); + } + } + } + return newPoms; + } + + private void resolveChildModulePoms(final Element pomRoot, + final String pomPath, final Map pomSet) { + for (final Element module : XmlUtils.findElements( + "/project/modules/module", pomRoot)) { + final String moduleName = module.getTextContent(); + if (StringUtils.isNotBlank(moduleName)) { + final String modulePath = resolveRelativePath(pomPath, + moduleName); + final boolean alreadyDiscovered = pomSet + .containsKey(modulePath); + pomSet.put(modulePath, moduleName); + if (!alreadyDiscovered) { + final Document pomDocument = XmlUtils.readXml(fileManager + .getInputStream(modulePath)); + final Element root = pomDocument.getDocumentElement(); + resolvePoms(root, modulePath, pomSet); + } + } + } + } + + private void resolveParentPom(final String pomPath, + final Map pomSet, final Element parentElement) { + final String relativePath = XmlUtils.getTextContent("/relativePath", + parentElement, DEFAULT_RELATIVE_PATH); + final String parentPomPath = resolveRelativePath(pomPath, relativePath); + final boolean alreadyDiscovered = pomSet.containsKey(parentPomPath); + if (!alreadyDiscovered) { + pomSet.put(parentPomPath, pomSet.get(parentPomPath)); + if (new File(parentPomPath).isFile()) { + final Document pomDocument = XmlUtils.readXml(fileManager + .getInputStream(parentPomPath)); + final Element root = pomDocument.getDocumentElement(); + resolvePoms(root, parentPomPath, pomSet); + } + } + } + + private void resolvePoms(final Element pomRoot, final String pomPath, + final Map pomSet) { + pomSet.put(pomPath, pomSet.get(pomPath)); // ensures this key exists + + final Element parentElement = XmlUtils.findFirstElement( + "/project/parent", pomRoot); + if (parentElement != null) { + resolveParentPom(pomPath, pomSet, parentElement); + } + + resolveChildModulePoms(pomRoot, pomPath, pomSet); + } + + private String resolveRelativePath(String relativeTo, + final String relativePath) { + if (relativeTo.endsWith(SEPARATOR)) { + relativeTo = relativeTo.substring(0, relativeTo.length() - 1); + } + while (new File(relativeTo).isFile()) { + relativeTo = relativeTo.substring(0, + relativeTo.lastIndexOf(SEPARATOR)); + } + final String[] relativePathSegments = relativePath.split(FileUtils + .getFileSeparatorAsRegex()); + + int backCount = 0; + for (final String relativePathSegment : relativePathSegments) { + if (relativePathSegment.equals("..")) { + backCount++; + } + else { + break; + } + } + final StringBuilder sb = new StringBuilder(); + for (int i = backCount; i < relativePathSegments.length; i++) { + sb.append(relativePathSegments[i]); + sb.append(SEPARATOR); + } + + while (backCount > 0) { + relativeTo = relativeTo.substring(0, + relativeTo.lastIndexOf(SEPARATOR)); + backCount--; + } + String path = relativeTo + SEPARATOR + sb.toString(); + if (new File(path).isDirectory()) { + path = path + DEFAULT_POM_NAME; + } + if (path.endsWith(SEPARATOR)) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + public void setFocusedModule(final Pom focusedModule) { + Validate.notNull(focusedModule, "Module required"); + if (focusedModule.getPath().equals(focusedModulePath)) { + return; + } + focusedModulePath = focusedModule.getPath(); + shell.setPromptPath(focusedModule.getModuleName()); + } + + private void sortPomMap() { + final List sortedPomPaths = new ArrayList( + pomMap.keySet()); + Collections.sort(sortedPomPaths, new PomComparator(pomMap)); + final Map sortedPomMap = new LinkedHashMap(); + for (final String pomPath : sortedPomPaths) { + sortedPomMap.put(pomPath, pomMap.get(pomPath)); + } + pomMap.clear(); + pomMap.putAll(sortedPomMap); + } + + private void updatePomCache() { + findUnparsedPoms(); + final Collection newPoms = parseUnparsedPoms(); + if (!newPoms.isEmpty()) { + sortPomMap(); + } + updateProjectMetadataForModules(newPoms); + } + + private void updateProjectMetadataForModules(final Iterable newPoms) { + for (final Pom pom : newPoms) { + final String projectMetadataId = ProjectMetadata + .getProjectIdentifier(pom.getModuleName()); + metadataService.evictAndGet(projectMetadataId); + metadataDependencyRegistry.notifyDownstream(projectMetadataId); + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/ProjectMetadata.java b/project/src/main/java/org/springframework/roo/project/ProjectMetadata.java new file mode 100644 index 000000000..be7aac6b0 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ProjectMetadata.java @@ -0,0 +1,84 @@ +package org.springframework.roo.project; + +import java.io.File; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.metadata.AbstractMetadataItem; +import org.springframework.roo.metadata.MetadataIdentificationUtils; +import org.springframework.roo.project.maven.Pom; + +/** + * The metadata for a module within the user's project. A simple project will + * have one instance of this class, whereas a multi-module project will have + * several. + * + * @since 1.0 + */ +public class ProjectMetadata extends AbstractMetadataItem { + + static final String MODULE_SEPARATOR = "?"; + static final String PROJECT_MID_PREFIX = MetadataIdentificationUtils + .create(ProjectMetadata.class.getName(), "the_project"); + + public static String getModuleName(final String metadataIdentificationString) { + if (metadataIdentificationString.contains(MODULE_SEPARATOR)) { + return StringUtils.substringAfterLast(metadataIdentificationString, + MODULE_SEPARATOR); + } + return ""; + } + + /** + * Returns the metadata ID for the project-level metadata of the given + * module. + * + * @param moduleName the fully-qualified module name, separated by + * {@link File#separator} and/or "/" if different; can be blank + * for the root or only module + * @return a non-blank MID + */ + public static String getProjectIdentifier(final String moduleName) { + final StringBuilder sb = new StringBuilder(PROJECT_MID_PREFIX); + if (StringUtils.isNotBlank(moduleName)) { + sb.append(MODULE_SEPARATOR).append( + moduleName.replace("/", File.separator)); + } + return sb.toString(); + } + + public static boolean isValid(final String metadataIdentificationString) { + return metadataIdentificationString.startsWith(PROJECT_MID_PREFIX); + } + + private final Pom pom; + + /** + * Constructor + * + * @param pom the POM for this module of the project (required) + */ + public ProjectMetadata(final Pom pom) { + super(getProjectIdentifier(pom.getModuleName())); + Validate.notNull(pom, "POM is required"); + this.pom = pom; + } + + public String getModuleName() { + return pom.getModuleName(); + } + + public Pom getPom() { + return pom; + } + + @Override + public final String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("identifier", getId()); + builder.append("valid", isValid()); + builder.append("pom", pom); + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/ProjectMetadataProvider.java b/project/src/main/java/org/springframework/roo/project/ProjectMetadataProvider.java new file mode 100644 index 000000000..779aef1eb --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ProjectMetadataProvider.java @@ -0,0 +1,319 @@ +package org.springframework.roo.project; + +import java.util.Collection; + +import org.springframework.roo.metadata.MetadataProvider; + +/** + * Provides mutability services for {@link ProjectMetadata}. + * + * @author Ben Alex + * @author Stefan Schmidt + * @author Alan Stewart + * @since 1.0 + */ +public interface ProjectMetadataProvider extends MetadataProvider { + + /** + * Attempts to add the specified build plugin. If the plugin already exists + * according to + * {@link ProjectMetadata#isBuildPluginRegistered(org.springframework.roo.project.Plugin)} + * , the method silently returns. Otherwise the plugin is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param plugin the plugin to add (required) + */ + void addBuildPlugin(Plugin plugin); + + /** + * Attempts to add the specified plugins. If all the plugins already exist + * according to {@link ProjectMetadata#isAllPluginRegistered(Plugin)}, the + * method silently returns. Otherwise each new dependency is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param plugins the plugins to add (required) + */ + void addBuildPlugins(Collection plugins); + + /** + * Attempts to add the specified dependencies. If all the dependencies + * already exist according to + * {@link ProjectMetadata#isAllDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise each new dependency is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param dependencies the dependencies to add (required) + */ + void addDependencies(Collection dependencies); + + /** + * Attempts to add the specified dependency. If the dependency already + * exists according to to + * {@link ProjectMetadata#isDependencyRegistered(org.springframework.roo.project.Dependency)} + * , the method silently returns. Otherwise the dependency is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param dependency the dependency to add (required) + */ + void addDependency(Dependency dependency); + + /** + * Attempts to add the specified filter. If the filter already exists + * according to + * {@link ProjectMetadata#isFilterRegistered(org.springframework.roo.project.Filter)} + * , the method silently returns. Otherwise the filter is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param filter the filter to add (required) + */ + void addFilter(Filter filter); + + /** + * Attempts to add the specified plugin repositories. If all the + * repositories already exists according to + * {@link ProjectMetadata#isPluginRepositoryRegistered(Repository)}, the + * method silently returns. Otherwise each new repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param repositories a list of plugin repositories to add (required) + */ + void addPluginRepositories(Collection repositories); + + /** + * Attempts to add the specified plugin repository. If the plugin repository + * already exists according to + * {@link ProjectMetadata#isPluginRepositoryRegistered(Repository)}, the + * method silently returns. Otherwise the repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param repository the plugin repository to add (required) + */ + void addPluginRepository(Repository repository); + + /** + * Attempts to add the specified property. If the property already exists + * according to + * {@link ProjectMetadata#isPropertyRegistered(org.springframework.roo.project.Property)} + * , the method silently returns. Otherwise the property is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param property the property to add (required) + */ + void addProperty(Property property); + + /** + * Attempts to add the specified repositories. If all the repositories + * already exists according to + * {@link ProjectMetadata#isRepositoryRegistered(Repository)}, the method + * silently returns. Otherwise each new repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param repositories a list of repositories to add (required) + */ + void addRepositories(Collection repositories); + + /** + * Attempts to add the specified repository. If the repository already + * exists according to + * {@link ProjectMetadata#isRepositoryRegistered(Repository)}, the method + * silently returns. Otherwise the repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param repository the repository to add (required) + */ + void addRepository(Repository repository); + + /** + * Attempts to add the specified resource. If the resource already exists + * according to {@link ProjectMetadata#isResourceRegistered(Resource)}, the + * method silently returns. Otherwise the resource is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param resource the resource to add (required) + */ + void addResource(Resource resource); + + /** + * Removes any plugins with the same groupId and artifactId as the given + * plugin. + * + * @param plugin the plugin to remove (can be null) + * @throws IllegalArgumentException if this method is called before the + * {@link ProjectMetadata} is available, or if the on-disk + * representation cannot be modified for any reason + */ + void removeBuildPlugin(Plugin plugin); + + /** + * Removes any plugins with the same groupId and artifactId as any of the + * given plugins. + * + * @param plugins the plugins to remove; can be null, any + * null elements will be quietly ignored + * @throws IllegalArgumentException if this method is called before the + * {@link ProjectMetadata} is available, or if the on-disk + * representation cannot be modified for any reason + */ + void removeBuildPlugins(Collection plugins); + + /** + * Attempts to remove the specified dependencies. If all the dependencies do + * not exist according to + * {@link ProjectMetadata#isDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise each located dependency is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param dependencies the dependencies to remove (required) + */ + void removeDependencies(Collection dependencies); + + /** + * Attempts to remove the specified dependency. If the dependency does not + * exist according to + * {@link ProjectMetadata#isDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise the located dependency is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param dependency the dependency to remove (required) + */ + void removeDependency(Dependency dependency); + + /** + * Attempts to remove the specified filter. If the filter does not exist + * according to + * {@link ProjectMetadata#isFilterRegistered(org.springframework.roo.project.Filter)} + * , the method silently returns. Otherwise the located filter is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param filter the filter to remove (required) + */ + void removeFilter(Filter filter); + + /** + * Attempts to remove the specified plugin repository. If the plugin + * repository does not exist according to + * {@link ProjectMetadata#isPluginRepositoryRegistered(Repository)}, the + * method silently returns. Otherwise the located plugin repository is + * removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param repository the plugin repository to remove (required) + */ + void removePluginRepository(Repository repository); + + /** + * Attempts to remove the specified property dependency. If the dependency + * does not exist according to + * {@link ProjectMetadata#isPropertyRegistered(Property)}, the method + * silently returns. Otherwise the located dependency is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param property the property to remove (required) + */ + void removeProperty(Property property); + + /** + * Attempts to remove the specified repository. If the repository does not + * exist according to + * {@link ProjectMetadata#isRepositoryRegistered(Repository)}, the method + * silently returns. Otherwise the located repository is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param repository the repository to remove (required) + */ + void removeRepository(Repository repository); + + /** + * Attempts to remove the specified resource. If the resource does not exist + * according to {@link ProjectMetadata#isResourceRegistered(Resource)}, the + * method silently returns. Otherwise the located resource is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param resource the resource to remove (required) + */ + void removeResource(Resource resource); + + /** + * Attempts to update the scope of the specified dependency. If the + * dependency does not exist according to + * {@link ProjectMetadata#isDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise the located dependency is updated. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param dependency the dependency to update (required) + * @param dependencyScope the dependency scope. May be null, in which case + * the element will be removed + */ + void updateDependencyScope(Dependency dependency, + DependencyScope dependencyScope); + + /** + * Attempts to update the project packaging type as defined via + * {@link ProjectType}. If the project packaging is not defined it will + * create a new definition. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param projectType the project type to update (required) + */ + void updateProjectType(ProjectType projectType); +} diff --git a/project/src/main/java/org/springframework/roo/project/ProjectOperations.java b/project/src/main/java/org/springframework/roo/project/ProjectOperations.java new file mode 100644 index 000000000..77064bebd --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ProjectOperations.java @@ -0,0 +1,632 @@ +package org.springframework.roo.project; + +import java.util.Collection; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.maven.Pom; + +/** + * Methods for various project-related operations. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ProjectOperations { + + /** + * Attempts to add the specified build plugin. If the plugin already exists + * according to + * {@link ProjectMetadata#isBuildPluginRegistered(org.springframework.roo.project.Plugin)} + * , the method silently returns. Otherwise the plugin is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param plugin the plugin to add (required) + */ + void addBuildPlugin(final String moduleName, Plugin plugin); + + /** + * Attempts to add the specified plugins. If all the plugins already exist + * according to {@link ProjectMetadata#isAllPluginRegistered(Plugin)}, the + * method silently returns. Otherwise each new dependency is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param plugins the plugins to add (required) + */ + void addBuildPlugins(final String moduleName, + Collection plugins); + + /** + * Attempts to add the specified dependencies. If all the dependencies + * already exist according to + * {@link ProjectMetadata#isAllDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise each new dependency is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param dependencies the dependencies to add (required) + */ + void addDependencies(final String moduleName, + Collection dependencies); + + /** + * Attempts to add the specified dependency. If the dependency already + * exists according to to + * {@link ProjectMetadata#isDependencyRegistered(org.springframework.roo.project.Dependency)} + * , the method silently returns. Otherwise the dependency is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param dependency the dependency to add (required) + */ + void addDependency(final String moduleName, Dependency dependency); + + /** + * Allows addition of a JAR dependency to the POM. + *

    + * Provides a convenient way for third parties to instruct end users how to + * use the CLI to add support for their projects without requiring the user + * to manually edit a pom.xml or write an add-on. + * + * @param moduleName the name of the module to act upon (required) + * @param groupId the group id of the dependency (required) + * @param artifactId the artifact id of the dependency (required) + * @param version the version of the dependency (required) + */ + void addDependency(final String moduleName, String groupId, + String artifactId, String version); + + /** + * Allows addition of a JAR dependency to the POM. + *

    + * Provides a convenient way for third parties to instruct end users how to + * use the CLI to add support for their projects without requiring the user + * to manually edit a pom.xml or write an add-on. + * + * @param moduleName the name of the module to act upon (required) + * @param groupId the group id of the dependency (required) + * @param artifactId the artifact id of the dependency (required) + * @param version the version of the dependency (required) + * @param scope the scope of the dependency + */ + void addDependency(final String moduleName, String groupId, + String artifactId, String version, DependencyScope scope); + + /** + * Allows addition of a JAR dependency to the POM. + *

    + * Provides a convenient way for third parties to instruct end users how to + * use the CLI to add support for their projects without requiring the user + * to manually edit a pom.xml or write an add-on. + * + * @param moduleName the name of the module to act upon (required) + * @param groupId the group id of the dependency (required) + * @param artifactId the artifact id of the dependency (required) + * @param version the version of the dependency (required) + * @param scope the scope of the dependency + * @param classifier the classifier of the dependency + */ + void addDependency(final String moduleName, String groupId, + String artifactId, String version, DependencyScope scope, + String classifier); + + /** + * Attempts to add the specified filter. If the filter already exists + * according to + * {@link ProjectMetadata#isFilterRegistered(org.springframework.roo.project.Filter)} + * , the method silently returns. Otherwise the filter is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param filter the filter to add (required) + */ + void addFilter(final String moduleName, Filter filter); + + /** + * Adds the given module as a dependency of the currently focused module. + * + * @param moduleName the name of the module to act upon (required) + */ + void addModuleDependency(String moduleName); + + /** + * Attempts to add the specified plugin repositories. If all the + * repositories already exists according to + * {@link ProjectMetadata#isPluginRepositoryRegistered(Repository)}, the + * method silently returns. Otherwise each new repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param repositories a list of plugin repositories to add (required) + */ + void addPluginRepositories(final String moduleName, + Collection repositories); + + /** + * Attempts to add the specified plugin repository. If the plugin repository + * already exists according to + * {@link ProjectMetadata#isPluginRepositoryRegistered(Repository)}, the + * method silently returns. Otherwise the repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param repository the plugin repository to add (required) + */ + void addPluginRepository(final String moduleName, Repository repository); + + /** + * Attempts to add the specified property. If the property already exists + * according to + * {@link ProjectMetadata#isPropertyRegistered(org.springframework.roo.project.Property)} + * , the method silently returns. Otherwise the property is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param property the property to add (required) + */ + void addProperty(final String moduleName, Property property); + + /** + * Attempts to add the specified repositories. If all the repositories + * already exists according to + * {@link ProjectMetadata#isRepositoryRegistered(Repository)}, the method + * silently returns. Otherwise each new repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param repositories a list of repositories to add (required) + */ + void addRepositories(final String moduleName, + Collection repositories); + + /** + * Attempts to add the specified repository. If the repository already + * exists according to + * {@link ProjectMetadata#isRepositoryRegistered(Repository)}, the method + * silently returns. Otherwise the repository is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param repository the repository to add (required) + */ + void addRepository(final String moduleName, Repository repository); + + /** + * Attempts to add the specified resource. If the resource already exists + * according to {@link ProjectMetadata#isResourceRegistered(Resource)}, the + * method silently returns. Otherwise the resource is added. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param resource the resource to add (required) + */ + void addResource(final String moduleName, Resource resource); + + /** + * Verifies if the specified build plugin is present. If it is present, + * silently returns. If it is not present, removes any build plugin which + * matches {@link ProjectMetadata#getBuildPluginsExcludingVersion(Plugin)}. + * Always adds the presented plugin. + *

    + * This method is deprecated - use {@link #updateBuildPlugin(Plugin)} + * instead. + * + * @param moduleName the name of the module to act upon (required) + * @param plugin the build plugin to update (required) + */ + @Deprecated + void buildPluginUpdate(final String moduleName, Plugin plugin); + + /** + * Returns the {@link Pom} of the currently focussed module, or if no module + * has the focus, the root {@link Pom}. + * + * @return null if none of the above descriptors exist + */ + Pom getFocusedModule(); + + /** + * Returns the name of the currently focussed module. + * + * @return an empty string if no module has the focus, otherwise a + * fully-qualified name separated by {@link java.io.File#separator} + */ + String getFocusedModuleName(); + + /** + * Returns the metadata for the currently focussed module. + * + * @return null if no project metadata is available + */ + ProjectMetadata getFocusedProjectMetadata(); + + /** + * @return + */ + String getFocusedProjectName(); + + /** + * @return + */ + JavaPackage getFocusedTopLevelPackage(); + + /** + * Returns the module to which the given file belongs + * + * @param fileIdentifier the canonical path to look up + * @return see above + */ + Pom getModuleForFileIdentifier(String fileIdentifier); + + /** + * Returns the names of each module in the user's project + * + * @return a non-null list + */ + Collection getModuleNames(); + + /** + * Convenience method to return the {@link PathResolver} from the project's + * {@link ProjectMetadata}. + * + * @return the {@link PathResolver}, or null if the project is unavailable + */ + PathResolver getPathResolver(); + + /** + * Returns the given module's {@link Pom} + * + * @param moduleName the fully-qualified name of the module (required) + * @return + */ + Pom getPomFromModuleName(String moduleName); + + /** + * Returns the {@link Pom}s for all modules of the user's project + * + * @return a non-null collection + */ + Collection getPoms(); + + /** + * Returns the {@link ProjectMetadata} for the given module. + * + * @param moduleName the module whose metadata is being requested (can be + * empty to signify the root or only module) + * @return null if the metadata is not available + */ + ProjectMetadata getProjectMetadata(String moduleName); + + /** + * @param moduleName the name of the module to act upon (required) + * @return + */ + String getProjectName(String moduleName); + + /** + * @param moduleName the name of the module to act upon (required) + * @return + */ + JavaPackage getTopLevelPackage(String moduleName); + + /** + * Indicates whether the supplied feature is installed in any module of a + * project. + * + * @param featureName the name of the feature (see {@link FeatureNames} for + * available features) + * @return true if the feature is installed in any module, otherwise false + */ + boolean isFeatureInstalled(String featureName); + + /** + * Indicates whether the supplied feature is installed in the module with + * the supplied name. + * + * @param featureName the name of the feature (see {@link FeatureNames} for + * available features) + * @param moduleName the name of the module to be checked + * @return true if the feature is installed the module, otherwise false + */ + boolean isFeatureInstalledInModule(String featureName, String moduleName); + + /** + * Indicates whether any of the supplied features are installed in the + * focused module. + * + * @param featureNames the names of the features (see {@link FeatureNames} + * for available features) + * @return true if any of the supplied features are installed in the focused + * module, otherwise false + */ + boolean isFeatureInstalledInFocusedModule(String... featureNames); + + /** + * Indicates whether the module whose name has the focus, if any, is + * available. + * + * @return see above + */ + boolean isFocusedProjectAvailable(); + + /** + * Indicates whether the user can create a new project module + * + * @return see above + */ + boolean isModuleCreationAllowed(); + + /** + * Indicates whether the user can change the focused module + * + * @return see above + */ + boolean isModuleFocusAllowed(); + + /** + * Indicates whether a module with the given name is available. + * + * @param moduleName the name of the module to act upon (can be blank) + * @return see above + */ + boolean isProjectAvailable(String moduleName); + + /** + * Removes any plugins with the same groupId and artifactId as the given + * plugin. + * + * @param moduleName the name of the module to act upon (required) + * @param plugin the plugin to remove (can be null) + * @throws IllegalArgumentException if this method is called before the + * {@link ProjectMetadata} is available, or if the on-disk + * representation cannot be modified for any reason + */ + void removeBuildPlugin(final String moduleName, Plugin plugin); + + /** + * Removes any plugins with the same groupId and artifactId as the given + * plugin and immediately writes the pom to the file system. + * + * @param moduleName the name of the module to act upon (required) + * @param plugin the plugin to remove (can be null) + * @throws IllegalArgumentException if this method is called before the + * {@link ProjectMetadata} is available, or if the on-disk + * representation cannot be modified for any reason + */ + void removeBuildPluginImmediately(String moduleName, Plugin plugin); + + /** + * Removes any plugins with the same groupId and artifactId as any of the + * given plugins. + * + * @param moduleName the name of the module to act upon (required) + * @param plugins the plugins to remove; can be null, any + * null elements will be quietly ignored + * @throws IllegalArgumentException if this method is called before the + * {@link ProjectMetadata} is available, or if the on-disk + * representation cannot be modified for any reason + */ + void removeBuildPlugins(final String moduleName, + Collection plugins); + + /** + * Attempts to remove the specified dependencies. If all the dependencies do + * not exist according to + * {@link ProjectMetadata#isDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise each located dependency is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param dependencies the dependencies to remove (required) + */ + void removeDependencies(final String moduleName, + Collection dependencies); + + /** + * Attempts to remove the specified dependency. If the dependency does not + * exist according to + * {@link ProjectMetadata#isDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise the located dependency is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param dependency the dependency to remove (required) + */ + void removeDependency(final String moduleName, Dependency dependency); + + /** + * Allows remove of an existing JAR dependency from the POM. + *

    + * Provides a convenient way for third parties to instruct end users how to + * use the CLI to remove an unwanted dependency from their projects without + * requiring the user to manually edit a pom.xml or write an add-on. + * + * @param moduleName the name of the module to act upon (required) + * @param groupId the group id of the dependency (required) + * @param artifactId the artifact id of the dependency (required) + * @param version the version of the dependency (required) + */ + void removeDependency(final String moduleName, String groupId, + String artifactId, String version); + + /** + * Allows remove of an existing JAR dependency from the POM. + *

    + * Provides a convenient way for third parties to instruct end users how to + * use the CLI to remove an unwanted dependency from their projects without + * requiring the user to manually edit a pom.xml or write an add-on. + * + * @param moduleName the name of the module to act upon (required) + * @param groupId the group id of the dependency (required) + * @param artifactId the artifact id of the dependency (required) + * @param version the version of the dependency (required) + * @param classifier the classifier of the dependency + */ + void removeDependency(final String moduleName, String groupId, + String artifactId, String version, String classifier); + + /** + * Attempts to remove the specified filter. If the filter does not exist + * according to + * {@link ProjectMetadata#isFilterRegistered(org.springframework.roo.project.Filter)} + * , the method silently returns. Otherwise the located filter is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param filter the filter to remove (required) + */ + void removeFilter(final String moduleName, Filter filter); + + /** + * Attempts to remove the specified plugin repository. If the plugin + * repository does not exist according to + * {@link ProjectMetadata#isPluginRepositoryRegistered(Repository)}, the + * method silently returns. Otherwise the located plugin repository is + * removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param repository the plugin repository to remove (required) + */ + void removePluginRepository(final String moduleName, Repository repository); + + /** + * Attempts to remove the specified property dependency. If the dependency + * does not exist according to + * {@link ProjectMetadata#isPropertyRegistered(Property)}, the method + * silently returns. Otherwise the located dependency is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param property the property to remove (required) + */ + void removeProperty(final String moduleName, Property property); + + /** + * Attempts to remove the specified repository. If the repository does not + * exist according to + * {@link ProjectMetadata#isRepositoryRegistered(Repository)}, the method + * silently returns. Otherwise the located repository is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param repository the repository to remove (required) + */ + void removeRepository(final String moduleName, Repository repository); + + /** + * Attempts to remove the specified resource. If the resource does not exist + * according to {@link ProjectMetadata#isResourceRegistered(Resource)}, the + * method silently returns. Otherwise the located resource is removed. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param resource the resource to remove (required) + */ + void removeResource(String moduleName, Resource resource); + + /** + * Sets the currently focused module + * + * @param module the module to focus upon (required) + */ + void setModule(Pom module); + + /** + * Verifies if the specified build plugin is present. If it is present, + * silently returns. If it is not present, removes any build plugin which + * matches {@link ProjectMetadata#getBuildPluginsExcludingVersion(Plugin)}. + * Always adds the presented plugin. + * + * @param moduleName the name of the module to act upon (required) + * @param plugin the build plugin to update (required) + */ + void updateBuildPlugin(final String moduleName, Plugin plugin); + + /** + * Attempts to update the scope of the specified dependency. If the + * dependency does not exist according to + * {@link ProjectMetadata#isDependencyRegistered(Dependency)}, the method + * silently returns. Otherwise the located dependency is updated. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param dependency the dependency to update (required) + * @param dependencyScope the dependency scope. May be null, in which case + * the element will be removed + */ + void updateDependencyScope(final String moduleName, Dependency dependency, + DependencyScope dependencyScope); + + /** + * Attempts to update the project packaging type as defined via + * {@link ProjectType}. If the project packaging is not defined it will + * create a new definition. + *

    + * An exception is thrown if this method is called before there is + * {@link ProjectMetadata} available, or if the on-disk representation + * cannot be modified for any reason. + * + * @param moduleName the name of the module to act upon (required) + * @param projectType the project type to update (required) + */ + void updateProjectType(final String moduleName, ProjectType projectType); +} diff --git a/project/src/main/java/org/springframework/roo/project/ProjectPathMonitoringInitializer.java b/project/src/main/java/org/springframework/roo/project/ProjectPathMonitoringInitializer.java new file mode 100644 index 000000000..e540f532e --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ProjectPathMonitoringInitializer.java @@ -0,0 +1,89 @@ +package org.springframework.roo.project; + +import static org.springframework.roo.file.monitor.event.FileOperation.CREATED; +import static org.springframework.roo.file.monitor.event.FileOperation.DELETED; +import static org.springframework.roo.file.monitor.event.FileOperation.MONITORING_FINISH; +import static org.springframework.roo.file.monitor.event.FileOperation.MONITORING_START; +import static org.springframework.roo.file.monitor.event.FileOperation.RENAMED; +import static org.springframework.roo.file.monitor.event.FileOperation.UPDATED; + +import java.io.File; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.monitor.DirectoryMonitoringRequest; +import org.springframework.roo.file.monitor.MonitoringRequest; +import org.springframework.roo.file.monitor.NotifiableFileMonitorService; +import org.springframework.roo.file.monitor.event.FileOperation; +import org.springframework.roo.file.undo.UndoManager; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataNotificationListener; + +@Component +@Service +public class ProjectPathMonitoringInitializer implements + MetadataNotificationListener { + + private static final FileOperation[] MONITORED_OPERATIONS = { + MONITORING_START, MONITORING_FINISH, CREATED, RENAMED, UPDATED, + DELETED }; + + @Reference private NotifiableFileMonitorService fileMonitorService; + @Reference private MetadataDependencyRegistry metadataDependencyRegistry; + @Reference private PathResolver pathResolver; + private boolean pathsRegistered; + @Reference private UndoManager undoManager; + + protected void activate(final ComponentContext context) { + metadataDependencyRegistry.addNotificationListener(this); + } + + protected void deactivate(final ComponentContext context) { + metadataDependencyRegistry.removeNotificationListener(this); + } + + private void monitorPathIfExists(final LogicalPath logicalPath) { + final String canonicalPath = pathResolver.getRoot(logicalPath); + // The path can be blank if a sub-folder contains a POM that doesn't + // belong to a module + if (StringUtils.isNotBlank(canonicalPath)) { + final File directory = new File(canonicalPath); + if (directory.isDirectory()) { + final MonitoringRequest request = new DirectoryMonitoringRequest( + directory, true, MONITORED_OPERATIONS); + new UndoableMonitoringRequest(undoManager, fileMonitorService, + request, true); + } + } + } + + private void monitorProjectPaths() { + for (final LogicalPath logicalPath : pathResolver.getPaths()) { + if (requiresMonitoring(logicalPath)) { + monitorPathIfExists(logicalPath); + } + } + } + + public void notify(final String upstreamDependency, + final String downstreamDependency) { + if (pathsRegistered) { + return; + } + monitorProjectPaths(); + pathsRegistered = true; + } + + private boolean requiresMonitoring(final LogicalPath logicalPath) { + if (logicalPath.isProjectRoot()) { + return false; // already monitored by ProcessManager + } + if (StringUtils.isBlank(logicalPath.getModule())) { + return true; // non-root path within root module + } + return logicalPath.isModuleRoot(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/ProjectType.java b/project/src/main/java/org/springframework/roo/project/ProjectType.java new file mode 100644 index 000000000..563d3b9a9 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/ProjectType.java @@ -0,0 +1,51 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.project.packaging.PackagingProvider; + +/** + * Provides available project types for the project. Currently only war and jar + * types are supported, but other types can be added in future. TODO check how + * this type can/should be replaced by {@link PackagingProvider} + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class ProjectType { + + public static final ProjectType JAR = new ProjectType("jar"); + public static final ProjectType WAR = new ProjectType("war"); + + private final String name; + + /** + * Constructor + * + * @param name the name of this type of project (required) + */ + public ProjectType(final String name) { + Validate.notBlank(name, "Name required"); + this.name = name; + } + + /** + * Returns the name of this type of project + * + * @return a non-blank name + * @since 1.2.0 + */ + public String getName() { + return name; + } + + /** + * Returns the name of this type of project + * + * @return a non-blank name + * @deprecated use {@link #getName()} instead + */ + @Deprecated + public String getType() { + return getName(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Property.java b/project/src/main/java/org/springframework/roo/project/Property.java new file mode 100644 index 000000000..828dd080a --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Property.java @@ -0,0 +1,129 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.w3c.dom.Element; + +/** + * Simplified immutable representation of a property. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Property implements Comparable { + + private final String name; + private final String value; + + /** + * Convenience constructor for creating a property instance from a XML + * Element + * + * @param element containing the property definition (required) + */ + public Property(final Element element) { + Validate.notNull(element, "Element required"); + name = element.getNodeName(); + value = element.getTextContent(); + } + + /** + * Convenience constructor creating a property instance + * + * @param name the property name (required) + */ + public Property(final String name) { + this.name = name; + value = ""; + } + + /** + * Convenience constructor creating a property instance + * + * @param name the property name (required) + * @param value the property value (required) + */ + public Property(final String name, final String value) { + Validate.notBlank(name, "Name required"); + Validate.notNull(value, "Value required"); + this.name = name; + this.value = value; + } + + public int compareTo(final Property o) { + if (o == null) { + throw new NullPointerException(); + } + int result = name.compareTo(o.name); + if (result == 0) { + result = value.compareTo(o.value); + } + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Property other = (Property) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + if (value == null) { + if (other.value != null) { + return false; + } + } + else if (!value.equals(other.value)) { + return false; + } + return true; + } + + /** + * The name of a property + * + * @return the name of the property (never null) + */ + public String getName() { + return name; + } + + /** + * The value of a property + * + * @return the value + */ + public String getValue() { + return value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + result = prime * result + (value == null ? 0 : value.hashCode()); + return result; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("name", name); + builder.append("value", value); + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Repository.java b/project/src/main/java/org/springframework/roo/project/Repository.java new file mode 100644 index 000000000..91d0af627 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Repository.java @@ -0,0 +1,169 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Simplified immutable representation of a repository. + *

    + * Structured after the model used by Maven and Ivy. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class Repository implements Comparable { + + private final boolean enableSnapshots; + private final String id; + private final String name; + private final String url; + + /** + * Convenience constructor for creating a repository instance from an XML + * Element + * + * @param element containing the repository definition (required) + */ + public Repository(final Element element) { + Validate.notNull(element, "Element required"); + final Element name = XmlUtils.findFirstElement("name", element); + final Element snapshotsElement = XmlUtils.findFirstElement("snapshots", + element); + enableSnapshots = snapshotsElement == null ? false : Boolean + .valueOf(XmlUtils.findRequiredElement("enabled", + snapshotsElement).getTextContent()); + id = XmlUtils.findRequiredElement("id", element).getTextContent(); + this.name = name == null ? null : name.getTextContent(); + url = XmlUtils.findRequiredElement("url", element).getTextContent(); + } + + /** + * Constructor for snapshots disabled + * + * @param id the repository id (required) + * @param name the repository name (optional) + * @param url the repository url (required) + */ + public Repository(final String id, final String name, final String url) { + this(id, name, url, false); + } + + /** + * Constructor for snapshots optionally enabled + * + * @param id the repository id (required) + * @param name the repository name (optional) + * @param url the repository url (required) + * @param enableSnapshots true if snapshots are allowed, otherwise false + */ + public Repository(final String id, final String name, final String url, + final boolean enableSnapshots) { + Validate.notBlank(id, "ID required"); + Validate.notBlank(url, "URL required"); + this.enableSnapshots = enableSnapshots; + this.id = id; + this.name = StringUtils.trimToNull(name); + this.url = url; + } + + public int compareTo(final Repository o) { + if (o == null) { + throw new NullPointerException(); + } + int result = id.compareTo(o.id); + if (result == 0) { + result = url.compareTo(o.url); + } + return result; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Repository && compareTo((Repository) obj) == 0; + } + + /** + * Returns the XML element for this repository + * + * @param document the document in which to create the element (required) + * @param tagName the name of the element to create (required) + * @return a non-null element + * @since 1.2.0 + */ + public Element getElement(final Document document, final String tagName) { + final Element repositoryElement = new XmlElementBuilder(tagName, + document) + .addChild( + new XmlElementBuilder("id", document).setText(id) + .build()) + .addChild( + new XmlElementBuilder("url", document).setText(url) + .build()).build(); + if (name != null) { + repositoryElement.appendChild(new XmlElementBuilder("name", + document).setText(name).build()); + } + if (enableSnapshots) { + repositoryElement.appendChild(new XmlElementBuilder("snapshots", + document).addChild( + new XmlElementBuilder("enabled", document).setText("true") + .build()).build()); + } + return repositoryElement; + } + + /** + * The id of the repository + * + * @return the id (never null) + */ + public String getId() { + return id; + } + + /** + * The name of the repository + * + * @return the name of the repository (null if not exists) + */ + public String getName() { + return name; + } + + /** + * The url of the repository + * + * @return the url (never null) + */ + public String getUrl() { + return url; + } + + @Override + public int hashCode() { + return 11 * id.hashCode() * url.hashCode(); + } + + /** + * Indicates if snapshots are enabled + * + * @return enableSnapshots + */ + public boolean isEnableSnapshots() { + return enableSnapshots; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("id", id); + builder.append("name", name); + builder.append("url", url); + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/Resource.java b/project/src/main/java/org/springframework/roo/project/Resource.java new file mode 100644 index 000000000..b78382b47 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/Resource.java @@ -0,0 +1,168 @@ +package org.springframework.roo.project; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Simplified immutable representation of a Maven resource. + *

    + * Structured after the model used by Maven. + * + * @author Alan Stewart + * @since 1.1 + */ +public class Resource implements Comparable { + + private final String directory; + private final Boolean filtering; + private final List includes = new ArrayList(); + + /** + * Convenience constructor when an XML element is available that represents + * a Maven . + * + * @param resource to parse (required) + */ + public Resource(final Element resource) { + final Element directoryElement = XmlUtils.findFirstElement("directory", + resource); + Validate.notNull(directoryElement, "directory element required"); + directory = directoryElement.getTextContent(); + + final Element filteringElement = XmlUtils.findFirstElement("filtering", + resource); + filtering = filteringElement == null ? null : Boolean + .valueOf(filteringElement.getTextContent()); + + // Parsing for includes + for (final Element include : XmlUtils.findElements("includes/include", + resource)) { + includes.add(include.getTextContent()); + } + } + + /** + * Creates an immutable {@link Resource} with no "includes". + * + * @param directory the {@link Path directory} (required) + * @param filtering whether filtering should occur + */ + public Resource(final String directory, final Boolean filtering) { + this(directory, filtering, null); + } + + /** + * Creates an immutable {@link Resource} with optional "includes". + * + * @param directory the {@link Path directory} (required) + * @param filtering whether filtering should occur + * @param includes the list of includes; can be null + */ + public Resource(final String directory, final Boolean filtering, + final Collection includes) { + Validate.notNull(directory, "Directory required"); + this.directory = directory; + this.filtering = filtering; + if (includes != null) { + this.includes.addAll(includes); + } + } + + public int compareTo(final Resource o) { + if (o == null) { + throw new NullPointerException(); + } + return getSimpleDescription().compareTo(o.getSimpleDescription()); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Resource other = (Resource) obj; + return getSimpleDescription().equals(other.getSimpleDescription()); + } + + public String getDirectory() { + return directory; + } + + /** + * Returns the Maven POM element for this resource + * + * @param document the POM document (required) + * @return a non-null element + */ + public Element getElement(final Document document) { + final Element resourceElement = document.createElement("resource"); + resourceElement.appendChild(XmlUtils.createTextElement(document, + "directory", directory)); + + if (filtering != null) { + resourceElement.appendChild(XmlUtils.createTextElement(document, + "filtering", filtering.toString())); + } + + if (!includes.isEmpty()) { + final Element includes = DomUtils.createChildElement("includes", + resourceElement, document); + for (final String include : this.includes) { + includes.appendChild(XmlUtils.createTextElement(document, + "include", include)); + } + } + + return resourceElement; + } + + public Boolean getFiltering() { + return filtering; + } + + public List getIncludes() { + return includes; + } + + public String getSimpleDescription() { + final StringBuilder builder = new StringBuilder(); + builder.append("directory ").append(directory); + if (filtering != null) { + builder.append(", filtering ").append(filtering.toString()); + } + if (!includes.isEmpty()) { + builder.append(", includes ").append( + StringUtils.join(includes, ",")); + } + return builder.toString(); + } + + @Override + public int hashCode() { + return getSimpleDescription().hashCode(); + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("directory", directory); + builder.append("filtering", filtering); + builder.append("includes", includes); + return builder.toString(); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/UndoableMonitoringRequest.java b/project/src/main/java/org/springframework/roo/project/UndoableMonitoringRequest.java new file mode 100644 index 000000000..35cbf6085 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/UndoableMonitoringRequest.java @@ -0,0 +1,63 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.file.monitor.MonitoringRequest; +import org.springframework.roo.file.undo.UndoManager; +import org.springframework.roo.file.undo.UndoableOperation; + +/** + * Allows {@link org.springframework.roo.file.monitor.MonitoringRequest}s to be + * applied as {@link org.springframework.roo.file.undo.UndoableOperation}s. + * + * @author Ben Alex + * @since 1.0 + */ +public class UndoableMonitoringRequest implements UndoableOperation { + + private final boolean add; + private final FileMonitorService fileMonitorService; + private final MonitoringRequest monitoringRequest; + private boolean resetRequired; + + public UndoableMonitoringRequest(final UndoManager undoManager, + final FileMonitorService fileMonitorService, + final MonitoringRequest monitoringRequest, final boolean add) { + Validate.notNull(undoManager, "Undo manager required"); + Validate.notNull(fileMonitorService, "File monitor service required"); + Validate.notNull(monitoringRequest, "Request required"); + this.fileMonitorService = fileMonitorService; + this.monitoringRequest = monitoringRequest; + this.add = add; + + if (add) { + resetRequired = fileMonitorService.add(monitoringRequest); + } + else { + resetRequired = fileMonitorService.remove(monitoringRequest); + } + + undoManager.add(this); + } + + public void reset() { + } + + public boolean undo() { + if (!resetRequired) { + return true; + } + try { + if (add) { + fileMonitorService.remove(monitoringRequest); + } + else { + fileMonitorService.add(monitoringRequest); + } + return true; + } + catch (final RuntimeException e) { + return false; + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/converter/GAVConverter.java b/project/src/main/java/org/springframework/roo/project/converter/GAVConverter.java new file mode 100644 index 000000000..98522deb3 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/converter/GAVConverter.java @@ -0,0 +1,42 @@ +package org.springframework.roo.project.converter; + +import java.util.List; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.GAV; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * A {@link Converter} for {@link GAV}s + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class GAVConverter implements Converter { + + public GAV convertFromText(final String value, final Class targetType, + final String optionContext) { + return GAV.getInstance(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class targetType, final String existingData, + final String optionContext, final MethodTarget target) { + // Currently (i.e. with no multi-module support), we can't offer any + // completions as we don't know what GAVs are valid for the user and + // the "this" alias (representing the current project or module) isn't + // implemented yet either. + // TODO offer the GAVs of the project, its modules, and any parent POMs + // of either + return true; + } + + public boolean supports(final Class type, final String optionContext) { + return GAV.class.isAssignableFrom(type); + } +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/converter/PomConverter.java b/project/src/main/java/org/springframework/roo/project/converter/PomConverter.java new file mode 100644 index 000000000..67577da37 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/converter/PomConverter.java @@ -0,0 +1,70 @@ +package org.springframework.roo.project.converter; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +@Component +@Service +public class PomConverter implements Converter { + + /** + * An option context value indicating that the currently focused module + * should be included when this {@link Converter} generates completions. + */ + public static final String INCLUDE_CURRENT_MODULE = "includeCurrent"; + + static final String ROOT_MODULE_SYMBOL = "~"; + + @Reference ProjectOperations projectOperations; + + private void addCompletion(final String moduleName, + final List completions) { + final String nonEmptyModuleName = StringUtils.defaultIfEmpty( + moduleName, ROOT_MODULE_SYMBOL); + completions.add(new Completion(nonEmptyModuleName)); + } + + public Pom convertFromText(final String value, final Class targetType, + final String optionContext) { + final String moduleName; + if (ROOT_MODULE_SYMBOL.equals(value)) { + moduleName = ""; + } + else { + moduleName = value; + } + return projectOperations.getPomFromModuleName(moduleName); + } + + public boolean getAllPossibleValues(final List completions, + final Class targetType, final String existingData, + final String optionContext, final MethodTarget target) { + final String focusedModuleName = projectOperations + .getFocusedModuleName(); + for (final String moduleName : projectOperations.getModuleNames()) { + if (isModuleRelevant(moduleName, focusedModuleName, optionContext)) { + addCompletion(moduleName, completions); + } + } + return true; + } + + private boolean isModuleRelevant(final String moduleName, + final String focusedModuleName, final String optionContext) { + return StringUtils.contains(optionContext, INCLUDE_CURRENT_MODULE) + || !moduleName.equals(focusedModuleName); + } + + public boolean supports(final Class type, final String optionContext) { + return Pom.class.isAssignableFrom(type); + } +} diff --git a/project/src/main/java/org/springframework/roo/project/maven/Module.java b/project/src/main/java/org/springframework/roo/project/maven/Module.java new file mode 100644 index 000000000..0b10b57e3 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/maven/Module.java @@ -0,0 +1,37 @@ +package org.springframework.roo.project.maven; + +import org.apache.commons.lang3.Validate; + +/** + * A module of a Maven multi-module project. + * + * @author James Tyrrell + * @since 1.2.0 + */ +public class Module { + + private final String name; + private final String pomPath; + + /** + * Constructor + * + * @param name the module's name (can't be blank) + * @param pomPath the canonical path of the module's POM file (can't be + * blank) + */ + public Module(final String name, final String pomPath) { + Validate.notBlank(name, "Invalid module name '%s'", name); + Validate.notBlank(pomPath, "Invalid path '%s'", pomPath); + this.name = name; + this.pomPath = pomPath; + } + + public String getName() { + return name; + } + + public String getPomPath() { + return pomPath; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/maven/Parent.java b/project/src/main/java/org/springframework/roo/project/maven/Parent.java new file mode 100644 index 000000000..0c7b19816 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/maven/Parent.java @@ -0,0 +1,46 @@ +package org.springframework.roo.project.maven; + +/** + * The parent declaration within a Maven POM. + * + * @author James Tyrrell + * @since 1.2.0 + */ +public class Parent { + + private final String artifactId; + private final String groupId; + private final String pomPath; + private final String relativePath; + private final String version; + + public Parent(final String groupId, final String artifactId, + final String version, final String relativePath, + final String pomPath) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.relativePath = relativePath; + this.pomPath = pomPath; + } + + public String getArtifactId() { + return artifactId; + } + + public String getGroupId() { + return groupId; + } + + public String getPomPath() { + return pomPath; + } + + public String getRelativePath() { + return relativePath; + } + + public String getVersion() { + return version; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/maven/ParentBuilder.java b/project/src/main/java/org/springframework/roo/project/maven/ParentBuilder.java new file mode 100644 index 000000000..2aaf9c1a8 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/maven/ParentBuilder.java @@ -0,0 +1,46 @@ +package org.springframework.roo.project.maven; + +import org.springframework.roo.model.Builder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +public class ParentBuilder implements Builder { + + private final String artifactId; + private final String groupId; + private final String pomPath; + private final String relativePath; + private final String version; + + public ParentBuilder(final Element parentElement, final String pomPath) { + groupId = XmlUtils.getTextContent("/project/groupId", parentElement); + artifactId = XmlUtils.getTextContent("/project/artifactId", parentElement); + version = XmlUtils.getTextContent("/project/version", parentElement); + relativePath = XmlUtils.getTextContent("/project/relativePath", parentElement); + this.pomPath = pomPath; + } + + public Parent build() { + return new Parent(groupId, artifactId, version, relativePath, pomPath); + } + + public String getArtifactId() { + return artifactId; + } + + public String getGroupId() { + return groupId; + } + + public String getPomPath() { + return pomPath; + } + + public String getRelativePath() { + return relativePath; + } + + public String getVersion() { + return version; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/maven/Pom.java b/project/src/main/java/org/springframework/roo/project/maven/Pom.java new file mode 100644 index 000000000..fbfcb7e14 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/maven/Pom.java @@ -0,0 +1,648 @@ +package org.springframework.roo.project.maven; + +import static org.springframework.roo.project.Path.ROOT; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.DependencyScope; +import org.springframework.roo.project.DependencyType; +import org.springframework.roo.project.Filter; +import org.springframework.roo.project.GAV; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PhysicalPath; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.Property; +import org.springframework.roo.project.Repository; +import org.springframework.roo.project.Resource; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.FileUtils; + +/** + * A Maven project object model (POM). + * + * @author James Tyrrell + * @author Andrew Swan + * @since 1.2.0 + */ +public class Pom { + + static final String DEFAULT_PACKAGING = "jar"; // Maven behaviour + + private final Set buildPlugins = new LinkedHashSet(); + private final Set dependencies = new LinkedHashSet(); + private final Set filters = new LinkedHashSet(); + private final GAV gav; + private final String moduleName; + private final Set modules = new LinkedHashSet(); + private final String name; + private final String packaging; + private final Parent parent; + private final String path; + private final Map pathLocations = new LinkedHashMap(); + private final Set pluginRepositories = new LinkedHashSet(); + private final Set pomProperties = new LinkedHashSet(); + private final Set repositories = new LinkedHashSet(); + private final Set resources = new LinkedHashSet(); + private final String sourceDirectory; // TODO use pathCache instead + private final String testSourceDirectory; // TODO use pathCache instead + + /** + * Constructor + * + * @param groupId the Maven groupId, explicit or inherited (required) + * @param artifactId the Maven artifactId (required) + * @param version the version of the artifact being built (required) + * @param packaging the Maven packaging (required) + * @param dependencies (can be null for none) + * @param parent the POM's parent declaration (can be null for + * none) + * @param modules the modules defined by this POM (only applies when + * packaging is "pom"; can be null for none) + * @param pomProperties any properties defined in the POM (can be + * null for none) + * @param name the Maven name of the artifact being built (can be blank) + * @param repositories any repositories defined in the POM (can be + * null for none) + * @param pluginRepositories any plugin repositories defined in the POM (can + * be null for none) + * @param sourceDirectory the directory relative to the POM that contains + * production code (can be blank for the Maven default) + * @param testSourceDirectory the directory relative to the POM that + * contains test code (can be blank for the Maven default) + * @param filters any filters defined in the POM (can be null + * for none) + * @param buildPlugins any plugins defined in the POM (can be + * null for none) + * @param resources any build resources defined in the POM (can be + * null for none) + * @param path the canonical path of this POM (required) + * @param moduleName the Maven name of this module (blank for the project's + * root or only POM) + * @param paths the {@link Path}s required for this module, in addition to + * the root (can be null) + */ + public Pom(final String groupId, final String artifactId, + final String version, final String packaging, + final Collection dependencies, + final Parent parent, final Collection modules, + final Collection pomProperties, + final String name, + final Collection repositories, + final Collection pluginRepositories, + final String sourceDirectory, final String testSourceDirectory, + final Collection filters, + final Collection buildPlugins, + final Collection resources, final String path, + final String moduleName, final Collection paths) { + Validate.notBlank(packaging, "Invalid packaging '%s'", packaging); + Validate.notBlank(path, "Invalid path '%s'", path); + + //gav = new GAV(groupId, artifactId, version); + this.moduleName = StringUtils.stripToEmpty(moduleName); + this.name = StringUtils.stripToEmpty(name); + this.packaging = packaging; + this.parent = parent; + + if(version == null && parent.getVersion() != null) { + gav = new GAV(groupId, artifactId, parent.getVersion()); + }else { + gav = new GAV(groupId, artifactId, version); + } + + this.path = path; + this.sourceDirectory = StringUtils.defaultIfEmpty(sourceDirectory, + Path.SRC_MAIN_JAVA.getDefaultLocation()); + this.testSourceDirectory = StringUtils.defaultIfEmpty( + testSourceDirectory, Path.SRC_TEST_JAVA.getDefaultLocation()); + + CollectionUtils.populate(this.buildPlugins, buildPlugins); + CollectionUtils.populate(this.dependencies, dependencies); + CollectionUtils.populate(this.filters, filters); + CollectionUtils.populate(this.modules, modules); + CollectionUtils.populate(this.pluginRepositories, pluginRepositories); + CollectionUtils.populate(this.pomProperties, pomProperties); + CollectionUtils.populate(this.repositories, repositories); + CollectionUtils.populate(this.resources, resources); + + cachePhysicalPaths(paths); + } + + /** + * Returns this module as a Dependency with the given scope + * + * @return a non-null instance + */ + public Dependency asDependency(final DependencyScope scope) { + return new Dependency(gav, DependencyType.valueOfTypeCode(packaging), + scope); + } + + private void cachePhysicalPaths(final Collection paths) { + final Collection pathsToCache = CollectionUtils.populate( + new HashSet(), paths); + if (!pathsToCache.contains(ROOT)) { + pathsToCache.add(ROOT); + } + for (final Path path : pathsToCache) { + pathLocations.put(path, path.getModulePath(this)); + } + } + + /** + * Indicates whether it's valid to add the given {@link Dependency} to this + * POM. + * + * @param newDependency the {@link Dependency} to check (can be + * null) + * @return see above + * @since 1.2.1 + */ + public boolean canAddDependency(final Dependency newDependency) { + return newDependency != null + && !isDependencyRegistered(newDependency) + && !Dependency.isHigherLevel( + newDependency.getType().toString(), packaging); + } + + /** + * Returns the ID of the artifact created by this module or project + * + * @return a non-blank ID + */ + public String getArtifactId() { + return gav.getArtifactId(); + } + + /** + * Returns any registered build plugins + * + * @return a non-null collection + */ + public Set getBuildPlugins() { + return buildPlugins; + } + + /** + * Returns any build plugins with the same groupId and artifactId as the + * given plugin. This is useful for upgrade cases. + * + * @param plugin to locate (required; note the version number is ignored in + * comparisons) + * @return any matching plugins (never returns null, but may return an empty + * {@link Set}) + */ + public Set getBuildPluginsExcludingVersion(final Plugin plugin) { + Validate.notNull(plugin, "Plugin to locate is required"); + final Set result = new HashSet(); + for (final Plugin p : buildPlugins) { + if (plugin.getArtifactId().equals(p.getArtifactId()) + && plugin.getGroupId().equals(p.getGroupId())) { + result.add(p); + } + } + return result; + } + + public Set getDependencies() { + return dependencies; + } + + /** + * Locates any dependencies which match the presented dependency, excluding + * the version number. This is useful for upgrade use cases, where it is + * necessary to remove any dependencies with the same group id, artifact id, + * and type as the dependency being upgraded to. + * + * @param dependency to locate (can be null) + * @return any matching dependencies (never returns null, but may return an + * empty {@link Set}) + */ + public Set getDependenciesExcludingVersion( + final Dependency dependency) { + final Set result = new HashSet(); + for (final Dependency d : dependencies) { + if (dependency != null + && dependency.getArtifactId().equals(d.getArtifactId()) + && dependency.getGroupId().equals(d.getGroupId()) + && dependency.getType().equals(d.getType())) { + result.add(d); + } + } + return result; + } + + /** + * Returns the display name of this module of the user project + * + * @return a non-blank name + */ + public String getDisplayName() { + return name; + } + + public Set getFilters() { + return filters; + } + + /** + * Returns the ID of the organisation or group that owns this module or + * project + * + * @return a non-blank ID + */ + public String getGroupId() { + return gav.getGroupId(); + } + + /** + * Returns the programmatic name of this module of the user project + * + * @return an empty string for the root or only module + */ + public String getModuleName() { + return moduleName; + } + + public Set getModules() { + return modules; + } + + /** + * Returns the display name of this module of the user project + * + * @return a non-blank name + * @deprecated use {@link #getDisplayName()} instead + */ + @Deprecated + public String getName() { + return getDisplayName(); + } + + public String getPackaging() { + return packaging; + } + + public Parent getParent() { + return parent; + } + + /** + * Returns this descriptor's canonical path on the file system + * + * @return a valid canonical path + */ + public String getPath() { + return path; + } + + /** + * Returns the canonical path of the given {@link Path} within this module, + * plus a trailing separator if found + * + * @param path the path for which to get the canonical location (required) + * @return null if this module has no such path + */ + public String getPathLocation(final Path path) { + final PhysicalPath modulePath = getPhysicalPath(path); + if (modulePath == null) { + return null; + } + return FileUtils.ensureTrailingSeparator(modulePath.getLocationPath()); + } + + /** + * Returns the {@link PhysicalPath} for the given {@link Path} of this + * module + * + * @param path the sub-path for which to return the {@link PhysicalPath} + * @return null if this module has no such sub-path + */ + public PhysicalPath getPhysicalPath(final Path path) { + return pathLocations.get(path); + } + + public List getPhysicalPaths() { + return new ArrayList(pathLocations.values()); + } + + public Set getPluginRepositories() { + return pluginRepositories; + } + + public Set getPomProperties() { + return pomProperties; + } + + /** + * Locates any properties which match the presented property, excluding the + * value. This is useful for upgrade use cases, where it is necessary to + * locate any properties with the name so that they can be removed. + * + * @param property to locate (required; note the value is ignored in + * comparisons) + * @return any matching properties (never returns null, but may return an + * empty {@link Set}) + */ + public Set getPropertiesExcludingValue(final Property property) { + Validate.notNull(property, "Property to locate is required"); + final Set result = new HashSet(); + for (final Property p : pomProperties) { + if (property.getName().equals(p.getName())) { + result.add(p); + } + } + return result; + } + + /** + * Locates the first occurrence of a property for a given name and returns + * it. + * + * @param name the property name (required) + * @return the property if found otherwise null + */ + public Property getProperty(final String name) { + Validate.notBlank(name, "Property name to locate is required"); + for (final Property p : pomProperties) { + if (name.equals(p.getName())) { + return p; + } + } + return null; + } + + public Set getRepositories() { + return repositories; + } + + public Set getResources() { + return resources; + } + + /** + * Returns the canonical path of this module's root directory, plus a + * trailing separator + * + * @return a valid canonical path + */ + public String getRoot() { + return getPathLocation(Path.ROOT); + } + + public String getSourceDirectory() { + return sourceDirectory; + } + + public String getTestSourceDirectory() { + return testSourceDirectory; + } + + /** + * Returns the version number of this module or project + * + * @return a non-blank version number + */ + public String getVersion() { + return gav.getVersion(); + } + + /** + * Indicates whether this {@link Pom} has the given {@link Dependency}, + * ignoring the version number. + * + * @param dependency the {@link Dependency} to check for (can be + * null) + * @return false if a null dependency is given + * @since 1.2.1 + */ + public boolean hasDependencyExcludingVersion(final Dependency dependency) { + return !getDependenciesExcludingVersion(dependency).isEmpty(); + } + + /** + * Indicates whether all of the given dependencies are registered, by + * calling {@link #isDependencyRegistered(Dependency)} for each one, + * ignoring any null elements. + * + * @param dependencies the dependencies to check (can be null + * or contain null elements) + * @return true if a null or empty collection is given + */ + public boolean isAllDependenciesRegistered( + final Collection dependencies) { + if (dependencies != null) { + for (final Dependency dependency : dependencies) { + if (dependency != null && !isDependencyRegistered(dependency)) { + return false; + } + } + } + return true; + } + + /** + * Indicates whether all the given plugin repositories are registered, by + * calling {@link #isPluginRepositoryRegistered(Repository)} for each one, + * ignoring any null elements. + * + * @param repositories the plugin repositories to check (can be + * null) + * @return true if a null collection is given + */ + public boolean isAllPluginRepositoriesRegistered( + final Collection repositories) { + if (repositories != null) { + for (final Repository repository : repositories) { + if (repository != null + && !isPluginRepositoryRegistered(repository)) { + return false; + } + } + } + return true; + } + + /** + * Indicates whether all of the given plugins are registered, based on their + * groupId, artifactId, and version. + * + * @param plugins the plugins to check (required) + * @return false if any of them are not registered + */ + public boolean isAllPluginsRegistered( + final Collection plugins) { + Validate.notNull(plugins, "Plugins to check is required"); + for (final Plugin plugin : plugins) { + if (plugin != null && !isBuildPluginRegistered(plugin)) { + return false; + } + } + return true; + } + + /** + * Indicates whether all the given repositories are registered. Equivalent + * to calling {@link #isRepositoryRegistered(Repository)} for each one, + * ignoring any null elements. + * + * @param repositories the repositories to check (can be null) + * @return true if a null collection is given + */ + public boolean isAllRepositoriesRegistered( + final Collection repositories) { + if (repositories != null) { + for (final Repository repository : repositories) { + if (repository != null && !isRepositoryRegistered(repository)) { + return false; + } + } + } + return true; + } + + /** + * Indicates whether any of the given dependencies are registered, by + * calling {@link #isDependencyRegistered(Dependency)} for each one. + * + * @param dependencies the dependencies to check (can be null) + * @return see above + */ + public boolean isAnyDependenciesRegistered( + final Collection dependencies) { + if (dependencies != null) { + for (final Dependency dependency : dependencies) { + if (isDependencyRegistered(dependency)) { + return true; + } + } + } + return false; + } + + /** + * Indicates whether any of the given plugins are registered, by calling + * {@link #isBuildPluginRegistered(Plugin)} for each one. + * + * @param plugins the plugins to check (required) + * @return whether any of the plugins are currently registered or not + */ + public boolean isAnyPluginsRegistered( + final Collection plugins) { + Validate.notNull(plugins, "Plugins to check is required"); + for (final Plugin plugin : plugins) { + if (isBuildPluginRegistered(plugin)) { + return true; + } + } + return false; + } + + /** + * Indicates whether the given build plugin is registered, based on its + * groupId, artifactId, and version. + * + * @param plugin to check (required) + * @return whether the build plugin is currently registered or not + * @deprecated use {@link #isPluginRegistered(GAV)} instead + */ + @Deprecated + public boolean isBuildPluginRegistered(final Plugin plugin) { + return plugin != null && isPluginRegistered(plugin.getGAV()); + } + + /** + * Indicates whether the given dependency is registered, by checking the + * result of {@link Dependency#equals(Object)}. + * + * @param dependency the dependency to check (can be null) + * @return false if a null dependency is given + */ + public boolean isDependencyRegistered(final Dependency dependency) { + return dependency != null && dependencies.contains(dependency); + } + + /** + * Indicates whether the given filter is registered. + * + * @param filter to check (required) + * @return whether the filter is currently registered or not + */ + public boolean isFilterRegistered(final Filter filter) { + Validate.notNull(filter, "Filter to check is required"); + return filters.contains(filter); + } + + /** + * Indicates whether a plugin with the given coordinates is registered + * + * @param coordinates the coordinates to match upon; can be + * null + * @return false if null coordinates are given + */ + public boolean isPluginRegistered(final GAV gav) { + for (final Plugin existingPlugin : buildPlugins) { + if (existingPlugin.getGAV().equals(gav)) { + return true; + } + } + return false; + } + + /** + * Indicates whether the given plugin repository is registered. + * + * @param repository repository to check (can be null) + * @return false if a null repository is given + */ + public boolean isPluginRepositoryRegistered(final Repository repository) { + return pluginRepositories.contains(repository); + } + + /** + * Indicates whether the given build property is registered. + * + * @param property to check (required) + * @return whether the property is currently registered or not + */ + public boolean isPropertyRegistered(final Property property) { + Validate.notNull(property, "Property to check is required"); + return pomProperties.contains(property); + } + + /** + * Indicates whether the given repository is registered. + * + * @param repository to check (can be null) + * @return false if a null repository is given + */ + public boolean isRepositoryRegistered(final Repository repository) { + return repositories.contains(repository); + } + + /** + * Indicates whether the given resource is registered. + * + * @param resource to check (required) + * @return whether the resource is currently registered or not + */ + public boolean isResourceRegistered(final Resource resource) { + Validate.notNull(resource, "Resource to check is required"); + return resources.contains(resource); + } + + @Override + public String toString() { + // For debugging + return gav + " at " + path; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/maven/PomFactory.java b/project/src/main/java/org/springframework/roo/project/maven/PomFactory.java new file mode 100644 index 000000000..c6d4ec934 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/maven/PomFactory.java @@ -0,0 +1,23 @@ +package org.springframework.roo.project.maven; + +import org.w3c.dom.Element; + +/** + * A Factory for {@link Pom}s. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface PomFactory { + + /** + * Creates a {@link Pom} by reading a pom.xml file + * + * @param root the root element of the XML file (required) + * @param pomPath the canonical path of the XML file (required) + * @param moduleName the name of the module to which the POM belongs (blank + * means the root or only POM) + * @return a non-null instance + */ + Pom getInstance(Element root, String pomPath, String moduleName); +} diff --git a/project/src/main/java/org/springframework/roo/project/maven/PomFactoryImpl.java b/project/src/main/java/org/springframework/roo/project/maven/PomFactoryImpl.java new file mode 100644 index 000000000..dac6e0f22 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/maven/PomFactoryImpl.java @@ -0,0 +1,217 @@ +package org.springframework.roo.project.maven; + +import static org.springframework.roo.project.maven.Pom.DEFAULT_PACKAGING; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.Filter; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.Plugin; +import org.springframework.roo.project.Property; +import org.springframework.roo.project.Repository; +import org.springframework.roo.project.Resource; +import org.springframework.roo.project.packaging.PackagingProvider; +import org.springframework.roo.project.packaging.PackagingProviderRegistry; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Element; + +@Component +@Service +public class PomFactoryImpl implements PomFactory { + + private static final String ARTIFACT_ID_XPATH = "/project/artifactId"; + private static final String DEFAULT_RELATIVE_PATH = "../pom.xml"; + private static final String DEPENDENCY_XPATH = "/project/dependencies/dependency"; + private static final String FILTER_XPATH = "/project/build/filters/filter"; + private static final String GROUP_ID_XPATH = "/project/groupId"; + private static final String MODULE_XPATH = "/project/modules/module"; + private static final String NAME_XPATH = "/project/name"; + private static final String PACKAGING_PROVIDER_PROPERTY_XPATH = "/project/properties/roo.packaging.provider"; + private static final String PACKAGING_XPATH = "/project/packaging"; + private static final String PARENT_GROUP_ID_XPATH = "/project/parent/groupId"; + private static final String PARENT_XPATH = "/project/parent"; + private static final String PLUGIN_REPOSITORY_XPATH = "/project/pluginRepositories/pluginRepository"; + private static final String PLUGIN_XPATH = "/project/build/plugins/plugin"; + private static final String PROPERTY_XPATH = "/project/properties/*"; + private static final String REPOSITORY_XPATH = "/project/repositories/repository"; + private static final String RESOURCE_XPATH = "/project/build/resources/resource"; + private static final String SOURCE_DIRECTORY_XPATH = "/project/build/sourceDirectory"; + private static final String TEST_SOURCE_DIRECTORY_XPATH = "/project/build/testSourceDirectory"; + private static final String VERSION_XPATH = "/project/version"; + + @Reference PackagingProviderRegistry packagingProviderRegistry; + + /** + * Returns the groupId defined in the given POM + * + * @param root the POM's root element (required) + * @return a non-blank groupId + */ + private String getGroupId(final Element root) { + final String projectGroupId = XmlUtils.getTextContent(GROUP_ID_XPATH, + root); + if (StringUtils.isNotBlank(projectGroupId)) { + return projectGroupId; + } + // Fall back to a group ID assumed to be the same as any possible + // (ROO-1193) + return XmlUtils.getTextContent(PARENT_GROUP_ID_XPATH, root); + } + + public Pom getInstance(final Element root, final String pomPath, + final String moduleName) { + Validate.notBlank(pomPath, "POM's canonical path is required"); + final String artifactId = XmlUtils.getTextContent(ARTIFACT_ID_XPATH, + root); + final String groupId = getGroupId(root); + final String name = XmlUtils.getTextContent(NAME_XPATH, root); + final String packaging = XmlUtils.getTextContent(PACKAGING_XPATH, root, + DEFAULT_PACKAGING); + String version = XmlUtils.getTextContent(VERSION_XPATH, root); + final String sourceDirectory = XmlUtils.getTextContent( + SOURCE_DIRECTORY_XPATH, root); + final String testSourceDirectory = XmlUtils.getTextContent( + TEST_SOURCE_DIRECTORY_XPATH, root); + final List dependencies = parseElements(Dependency.class, + DEPENDENCY_XPATH, root); + final List filters = parseElements(Filter.class, FILTER_XPATH, + root); + final List modules = getModules(root, pomPath, packaging); + final List plugins = parseElements(Plugin.class, PLUGIN_XPATH, + root); + final List pomProperties = parseElements(Property.class, + PROPERTY_XPATH, root); + final List pluginRepositories = parseElements( + Repository.class, PLUGIN_REPOSITORY_XPATH, root); + final List repositories = parseElements(Repository.class, + REPOSITORY_XPATH, root); + final List resources = parseElements(Resource.class, + RESOURCE_XPATH, root); + final String projectParentVersion = XmlUtils.getTextContent("/project/parent/version", root); + final Parent parent = getParent(pomPath, root); + if(version == null) { + version = projectParentVersion; + } + final Collection paths = getPaths(root, packaging); + return new Pom(groupId, artifactId, version, packaging, dependencies, + parent, modules, pomProperties, name, repositories, + pluginRepositories, sourceDirectory, testSourceDirectory, + filters, plugins, resources, pomPath, moduleName, paths); + } + + private List getModules(final Element root, final String pomPath, + final String packaging) { + if (!"pom".equalsIgnoreCase(packaging)) { + return null; + } + final List modules = new ArrayList(); + for (final Element module : XmlUtils.findElements(MODULE_XPATH, root)) { + final String moduleName = module.getTextContent(); + if (StringUtils.isNotBlank(moduleName)) { + final String modulePath = resolveRelativePath(pomPath, + moduleName); + modules.add(new Module(moduleName, modulePath)); + } + } + return modules; + } + + private Parent getParent(final String pomPath, final Element root) { + final Element parentElement = XmlUtils.findFirstElement(PARENT_XPATH, + root); + if (parentElement == null) { + return null; + } + final String relativePath = XmlUtils.getTextContent("/relativePath", + parentElement, DEFAULT_RELATIVE_PATH); + final String parentPomPath = resolveRelativePath(pomPath, relativePath); + return new ParentBuilder(parentElement, parentPomPath).build(); + } + + private Collection getPaths(final Element root, final String packaging) { + final String packagingProviderId = XmlUtils.getTextContent( + PACKAGING_PROVIDER_PROPERTY_XPATH, root, packaging); + final PackagingProvider packagingProvider = packagingProviderRegistry + .getPackagingProvider(packagingProviderId); + Validate.notNull(packagingProvider, + "No PackagingProvider found with the ID '%s'", + packagingProviderId); + return packagingProvider.getPaths(); + } + + /** + * Parses any elements matching the given XPath expression into instances of + * the given type. + * + * @param the type of object to parse + * @param type the type of object to parse; must have a constructor that + * accepts an {@link Element} as its sole argument + * @param xPath the XPath expression to apply (required) + * @param root the root of the XML document being searched (required) + * @return a non-null list + */ + private List parseElements(final Class type, final String xPath, + final Element root) { + final List results = new ArrayList(); + for (final Element element : XmlUtils.findElements(xPath, root)) { + try { + results.add(type.getConstructor(Element.class).newInstance( + element)); + } + catch (final RuntimeException e) { + throw e; + } + catch (final Exception e) { + throw new RuntimeException(e); + } + } + return results; + } + + private String resolveRelativePath(String relativeTo, + final String relativePath) { + relativeTo = StringUtils.removeEnd(relativeTo, File.separator); + while (new File(relativeTo).isFile()) { + relativeTo = relativeTo.substring(0, + relativeTo.lastIndexOf(File.separator)); + } + final String[] relativePathSegments = relativePath.split(FileUtils + .getFileSeparatorAsRegex()); + + int backCount = 0; + for (final String relativePathSegment : relativePathSegments) { + if (relativePathSegment.equals("..")) { + backCount++; + } + else { + break; + } + } + final StringBuilder sb = new StringBuilder(); + for (int i = backCount; i < relativePathSegments.length; i++) { + sb.append(relativePathSegments[i]); + sb.append(File.separator); + } + + while (backCount > 0) { + relativeTo = relativeTo.substring(0, + relativeTo.lastIndexOf(File.separatorChar)); + backCount--; + } + String path = relativeTo + File.separator + sb.toString(); + if (new File(path).isDirectory()) { + path = path + "pom.xml"; + } + return path; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/AbstractCorePackagingProvider.java b/project/src/main/java/org/springframework/roo/project/packaging/AbstractCorePackagingProvider.java new file mode 100644 index 000000000..f9bd383b0 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/AbstractCorePackagingProvider.java @@ -0,0 +1,47 @@ +package org.springframework.roo.project.packaging; + +import org.w3c.dom.Document; + +/** + * A {@link PackagingProvider} provided by Spring Roo, i.e. not by a third-party + * addon. + * + * @author Andrew Swan + * @since 1.2.0 + */ +abstract class AbstractCorePackagingProvider extends AbstractPackagingProvider + implements CorePackagingProvider { + + /** + * Constructor + * + * @param name the name of this type of packaging as used in the POM + * (required) + * @param pomTemplate the path of this packaging type's POM template, + * relative to its own package, as per + * {@link Class#getResourceAsStream(String)}; this template + * should contain a "parent" element with its own groupId, + * artifactId, and version elements; this parent element will be + * removed if not required + */ + protected AbstractCorePackagingProvider(final String name, + final String pomTemplate) { + /* + * Core instances use the Maven packaging name as the ID so that the + * user sees intuitively-named packaging options on the command line. If + * they implement their own packaging types, they can name them with any + * other name that makes sense to them. + */ + super(name, name, pomTemplate); + } + + public boolean isDefault() { + return false; + } + + @Override + protected final void setPackagingProviderId(final Document pom) { + // Not needed, as the core providers use the Maven packaging name as + // their IDs. + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/AbstractPackagingProvider.java b/project/src/main/java/org/springframework/roo/project/packaging/AbstractPackagingProvider.java new file mode 100644 index 000000000..5ed1c5ed4 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/AbstractPackagingProvider.java @@ -0,0 +1,445 @@ +package org.springframework.roo.project.packaging; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.ApplicationContextOperations; +import org.springframework.roo.project.GAV; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PathResolver; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.DomUtils; +import org.springframework.roo.support.util.FileUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; +import org.osgi.service.component.ComponentContext; + +/** + * Convenient superclass for core or third-party addons to implement a + * {@link PackagingProvider}. Uses the "Template Method" GoF pattern. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component(componentAbstract = true) +public abstract class AbstractPackagingProvider implements PackagingProvider { + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + private static final String DEFAULT_VERSION = "0.1.0.BUILD-SNAPSHOT"; + private static final String JAVA_VERSION_PLACEHOLDER = "JAVA_VERSION"; + protected static final Logger LOGGER = HandlerUtils + .getLogger(PackagingProvider.class); + /** + * The name of the POM property that stores the packaging provider's ID. + */ + public static final String ROO_PACKAGING_PROVIDER_PROPERTY = "roo.packaging.provider"; + + private static final String VERSION_ELEMENT = "version"; + + protected ApplicationContextOperations applicationContextOperations; + protected FileManager fileManager; + protected PathResolver pathResolver; + private final String id; + private final String name; + private final String pomTemplate; + + /** + * Constructor + * + * @param id the unique ID of this packaging type, see + * {@link PackagingProvider#getId()} + * @param name the name of this type of packaging as used in the POM + * (required) + * @param pomTemplate the path of this packaging type's POM template, + * relative to its own package, as per + * {@link Class#getResourceAsStream(String)}; this template + * should contain a "parent" element with its own groupId, + * artifactId, and version elements; this parent element will be + * removed if not required + */ + protected AbstractPackagingProvider(final String id, final String name, + final String pomTemplate) { + Validate.notBlank(id, "ID is required"); + Validate.notBlank(name, "Name is required"); + Validate.notBlank(pomTemplate, "POM template path is required"); + this.id = id; + this.name = name; + this.pomTemplate = pomTemplate; + } + + public String createArtifacts(final JavaPackage topLevelPackage, + final String nullableProjectName, final String javaVersion, + final GAV parentPom, final String module, + final ProjectOperations projectOperations) { + final String pomPath = createPom(topLevelPackage, nullableProjectName, + javaVersion, parentPom, module, projectOperations); + createOtherArtifacts(topLevelPackage, module, projectOperations); + return pomPath; + } + + /** + * Subclasses can override this method to create any other required files or + * directories (apart from the POM, which has previously been generated by + * {@link #createPom}). + *

    + * This implementation sets up the Log4j configuration file for the root + * module. + * + * @param topLevelPackage + * @param module the unqualified name of the module being created (empty + * means the root or only module) + * @param projectOperations can't be injected as it would create a circular + * dependency + */ + protected void createOtherArtifacts(final JavaPackage topLevelPackage, + final String module, final ProjectOperations projectOperations) { + if (StringUtils.isBlank(module)) { + setUpLog4jConfiguration(); + } + } + + /** + * Creates the Maven POM using the subclass' POM template as follows: + *

      + *
    • sets the parent POM to the given parent (if any)
    • + *
    • sets the groupId to the result of {@link #getGroupId}, omitting this + * element if it's the same as the parent's groupId (as per Maven best + * practice)
    • + *
    • sets the artifactId to the result of {@link #getArtifactId}
    • + *
    • sets the packaging to the result of {@link #getName()}
    • + *
    • sets the project name to the result of {@link #getProjectName}
    • + *
    • replaces all occurrences of {@link #JAVA_VERSION_PLACEHOLDER} with + * the given Java version
    • + *
    + * This method makes as few assumptions about the POM template as possible, + * to make life easier for anyone writing a {@link PackagingProvider}. + * + * @param topLevelPackage the new project or module's top-level Java package + * (required) + * @param projectName the project name provided by the user (can be blank) + * @param javaVersion the Java version to substitute into the POM (required) + * @param parentPom the Maven coordinates of the parent POM (can be + * null) + * @param module the unqualified name of the Maven module to which the new + * POM belongs + * @param projectOperations cannot be injected otherwise it's a circular + * dependency + * @return the path of the newly created POM + */ + protected String createPom(final JavaPackage topLevelPackage, + final String projectName, final String javaVersion, + final GAV parentPom, final String module, + final ProjectOperations projectOperations) { + Validate.notBlank(javaVersion, "Java version required"); + Validate.notNull(topLevelPackage, "Top level package required"); + + // Read the POM template from the classpath + final Document pom = XmlUtils.readXml(FileUtils.getInputStream( + getClass(), pomTemplate)); + final Element root = pom.getDocumentElement(); + + // name + final String mavenName = getProjectName(projectName, module, + topLevelPackage); + if (StringUtils.isNotBlank(mavenName)) { + // If the user wants this element in the traditional place, ensure + // the template already contains it + DomUtils.createChildIfNotExists("name", root, pom).setTextContent( + mavenName.trim()); + } + else { + DomUtils.removeElements("name", root); + } + + // groupId and parent + setGroupIdAndParent(getGroupId(topLevelPackage), parentPom, root, pom); + + // artifactId + final String artifactId = getArtifactId(projectName, module, + topLevelPackage); + Validate.notBlank(artifactId, "Maven artifactIds cannot be blank"); + DomUtils.createChildIfNotExists("artifactId", root, pom) + .setTextContent(artifactId.trim()); + + // version + final Element existingVersionElement = DomUtils + .getChildElementByTagName(root, VERSION_ELEMENT); + if (existingVersionElement == null) { + DomUtils.createChildElement(VERSION_ELEMENT, root, pom) + .setTextContent(DEFAULT_VERSION); + } + + // packaging + DomUtils.createChildIfNotExists("packaging", root, pom).setTextContent( + name); + setPackagingProviderId(pom); + + // Java versions + final List versionElements = XmlUtils.findElements("//*[.='" + + JAVA_VERSION_PLACEHOLDER + "']", root); + for (final Element versionElement : versionElements) { + versionElement.setTextContent(javaVersion); + } + + // Write the new POM to disk + final String pomPath = getPathResolver().getIdentifier( + Path.ROOT.getModulePathId(module), "pom.xml"); + getFileManager().createOrUpdateTextFileIfRequired(pomPath, + XmlUtils.nodeToString(pom), true); + return pomPath; + } + + /** + * Returns the text to be inserted into the POM's + * <artifactId> element. This implementation simply + * delegates to {@link #getProjectName}. Subclasses can override this method + * to use a different strategy. + * + * @param nullableProjectName the project name entered by the user (can be + * blank) + * @param module the name of the module being created (blank for the root + * module) + * @param topLevelPackage the project or module's top level Java package + * (required) + * @return a non-blank artifactId + */ + protected String getArtifactId(final String nullableProjectName, + final String module, final JavaPackage topLevelPackage) { + return getProjectName(nullableProjectName, module, topLevelPackage); + } + + /** + * Returns the fully-qualified name of the given module, relative to the + * currently focused module. + * + * @param moduleName can be blank for the root or only module + * @param projectOperations + * @return + */ + protected final String getFullyQualifiedModuleName(final String moduleName, + final ProjectOperations projectOperations) { + if (StringUtils.isBlank(moduleName)) { + return ""; + } + final String focusedModuleName = projectOperations + .getFocusedModuleName(); + if (StringUtils.isBlank(focusedModuleName)) { + return moduleName; + } + return focusedModuleName + File.separator + moduleName; + } + + /** + * Returns the groupId of the project or module being created. This + * implementation simply uses the fully-qualified name of the given Java + * package. Subclasses can override this method to use a different strategy. + * + * @param topLevelPackage the new project or module's top-level Java package + * (required) + * @return + */ + protected String getGroupId(final JavaPackage topLevelPackage) { + return topLevelPackage.getFullyQualifiedPackageName(); + } + + public final String getId() { + return id; + } + + /** + * Returns the package-relative path to this {@link PackagingProvider}'s POM + * template. + * + * @return a non-blank path + */ + String getPomTemplate() { + return pomTemplate; + } + + /** + * Returns the text to be inserted into the POM's <name> + * element. This implementation uses the given project name if not blank, + * otherwise the last element of the given Java package. Subclasses can + * override this method to use a different strategy. + * + * @param nullableProjectName the project name entered by the user (can be + * blank) + * @param module the name of the module being created (blank for the root + * module) + * @param topLevelPackage the project or module's top level Java package + * (required) + * @return a blank name if none is required + */ + protected String getProjectName(final String nullableProjectName, + final String module, final JavaPackage topLevelPackage) { + String packageName = StringUtils.defaultIfEmpty(nullableProjectName, + module); + return StringUtils.defaultIfEmpty(packageName, + topLevelPackage.getLastElement()); + } + + /** + * Sets the Maven groupIds of the parent and/or project as necessary + * + * @param projectGroupId the project's groupId (required) + * @param parentPom the Maven coordinates of the parent POM (can be + * null) + * @param root the root element of the POM document (required) + * @param pom the POM document (required) + */ + protected void setGroupIdAndParent(final String projectGroupId, + final GAV parentPom, final Element root, final Document pom) { + final Element parentPomElement = DomUtils.createChildIfNotExists( + "parent", root, pom); + final Element projectGroupIdElement = DomUtils.createChildIfNotExists( + "groupId", root, pom); + if (parentPom == null) { + // No parent POM was specified; remove the parent element + root.removeChild(parentPomElement); + DomUtils.removeTextNodes(root); + projectGroupIdElement.setTextContent(projectGroupId); + } + else { + // Parent's groupId, artifactId, and version + DomUtils.createChildIfNotExists("groupId", parentPomElement, pom) + .setTextContent(parentPom.getGroupId()); + DomUtils.createChildIfNotExists("artifactId", parentPomElement, pom) + .setTextContent(parentPom.getArtifactId()); + DomUtils.createChildIfNotExists(VERSION_ELEMENT, parentPomElement, + pom).setTextContent(parentPom.getVersion()); + + // Project groupId (if necessary) + if (projectGroupId.equals(parentPom.getGroupId())) { + // Maven best practice is to inherit the groupId from the parent + root.removeChild(projectGroupIdElement); + DomUtils.removeTextNodes(root); + } + else { + // Project has its own groupId => needs to be explicit + projectGroupIdElement.setTextContent(projectGroupId); + } + } + } + + /** + * Stores the ID of this {@link PackagingProvider} as a POM property called + * {@value #ROO_PACKAGING_PROVIDER_PROPERTY}. Subclasses can override this + * method, but be aware that Roo needs some way of working out from a given + * pom.xml file which {@link PackagingProvider} should be used. + * + * @param pom the DOM document for the POM being created + */ + protected void setPackagingProviderId(final Document pom) { + final Node propertiesElement = DomUtils.createChildIfNotExists( + "properties", pom.getDocumentElement(), pom); + DomUtils.createChildIfNotExists(ROO_PACKAGING_PROVIDER_PROPERTY, + propertiesElement, pom).setTextContent(getId()); + } + + private void setUpLog4jConfiguration() { + final String log4jConfigFile = getPathResolver().getFocusedIdentifier( + Path.SRC_MAIN_RESOURCES, "log4j.properties"); + final InputStream templateInputStream = FileUtils.getInputStream( + getClass(), "log4j.properties-template"); + OutputStream outputStream = null; + try { + outputStream = getFileManager().createFile(log4jConfigFile) + .getOutputStream(); + IOUtils.copy(templateInputStream, outputStream); + } + catch (final IOException e) { + LOGGER.warning("Unable to install log4j logging configuration"); + } + finally { + IOUtils.closeQuietly(templateInputStream); + IOUtils.closeQuietly(outputStream); + } + } + + public FileManager getFileManager(){ + if(fileManager == null){ + // Get all Services implement FileManager interface + try { + ServiceReference[] references = context.getAllServiceReferences(FileManager.class.getName(), null); + + for(ServiceReference ref : references){ + return (FileManager) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load FileManager on AbstractPackagingProvider."); + return null; + } + }else{ + return fileManager; + } + } + + public PathResolver getPathResolver(){ + if(pathResolver == null){ + // Get all Services implement PathResolver interface + try { + ServiceReference[] references = context.getAllServiceReferences(PathResolver.class.getName(), null); + + for(ServiceReference ref : references){ + return (PathResolver) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load PathResolver on AbstractPackagingProvider."); + return null; + } + }else{ + return pathResolver; + } + } + + public ApplicationContextOperations getApplicationContextOperations(){ + if(applicationContextOperations == null){ + // Get all Services implement ApplicationContextOperations interface + try { + ServiceReference[] references = context.getAllServiceReferences(ApplicationContextOperations.class.getName(), null); + + for(ServiceReference ref : references){ + return (ApplicationContextOperations) context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load ApplicationContextOperations on AbstractPackagingProvider."); + return null; + } + }else{ + return applicationContextOperations; + } + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/BundlePackaging.java b/project/src/main/java/org/springframework/roo/project/packaging/BundlePackaging.java new file mode 100644 index 000000000..3e3367d50 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/BundlePackaging.java @@ -0,0 +1,47 @@ +package org.springframework.roo.project.packaging; + +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; +import static org.springframework.roo.project.Path.SRC_MAIN_RESOURCES; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.GAV; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * The {@link PackagingProvider} that creates an OSGi bundle. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class BundlePackaging implements CorePackagingProvider { + + public String createArtifacts(final JavaPackage topLevelPackage, + final String nullableProjectName, final String javaVersion, + final GAV parentPom, final String module, + final ProjectOperations projectOperations) { + // Already created by the creator addon + return projectOperations.getPathResolver().getIdentifier( + LogicalPath.getInstance(Path.ROOT, ""), "pom.xml"); + } + + public String getId() { + return "bundle"; + } + + public Collection getPaths() { + return Arrays.asList(SRC_MAIN_JAVA, SRC_MAIN_RESOURCES); + } + + public boolean isDefault() { + return false; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/CorePackagingProvider.java b/project/src/main/java/org/springframework/roo/project/packaging/CorePackagingProvider.java new file mode 100644 index 000000000..5afba2e72 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/CorePackagingProvider.java @@ -0,0 +1,10 @@ +package org.springframework.roo.project.packaging; + +/** + * Marker interface for a {@link PackagingProvider} built into Spring Roo. + * + * @author Andrew Swan + * @since 1.2.0 + */ +interface CorePackagingProvider extends PackagingProvider { +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/packaging/EarPackaging.java b/project/src/main/java/org/springframework/roo/project/packaging/EarPackaging.java new file mode 100644 index 000000000..f38816d1c --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/EarPackaging.java @@ -0,0 +1,32 @@ +package org.springframework.roo.project.packaging; + +import java.util.Collection; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * The Maven "pom" {@link PackagingProvider} + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class EarPackaging extends AbstractCorePackagingProvider { + + /** + * Constructor + */ + public EarPackaging() { + // ear-pom-template.xml doesn't exist because we won't allow ear packaging projects + super("ear", "ear-pom-template.xml"); + } + + public Collection getPaths() { + return null; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/JarPackaging.java b/project/src/main/java/org/springframework/roo/project/packaging/JarPackaging.java new file mode 100644 index 000000000..d89e30052 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/JarPackaging.java @@ -0,0 +1,70 @@ +package org.springframework.roo.project.packaging; + +import static org.springframework.roo.project.Path.SPRING_CONFIG_ROOT; +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; +import static org.springframework.roo.project.Path.SRC_MAIN_RESOURCES; +import static org.springframework.roo.project.Path.SRC_TEST_JAVA; +import static org.springframework.roo.project.Path.SRC_TEST_RESOURCES; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.GAV; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * The {@link PackagingProvider} that creates a JAR file. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class JarPackaging extends AbstractCorePackagingProvider { + + public static final String NAME = "jar"; + + /** + * Constructor invoked by the OSGi container + */ + public JarPackaging() { + super(NAME, "jar-pom-template.xml"); + } + + @Override + protected void createOtherArtifacts(final JavaPackage topLevelPackage, + final String module, final ProjectOperations projectOperations) { + + super.createOtherArtifacts(topLevelPackage, module, projectOperations); + final String fullyQualifiedModuleName = getFullyQualifiedModuleName( + module, projectOperations); + getApplicationContextOperations().createMiddleTierApplicationContext( + topLevelPackage, fullyQualifiedModuleName); + } + + @Override + protected String createPom(final JavaPackage topLevelPackage, + final String nullableProjectName, final String javaVersion, + final GAV parentPom, final String moduleName, + final ProjectOperations projectOperations) { + + final String pomPath = super.createPom(topLevelPackage, + nullableProjectName, javaVersion, parentPom, moduleName, + projectOperations); + return pomPath; + } + + public Collection getPaths() { + return Arrays.asList(SRC_MAIN_JAVA, SRC_MAIN_RESOURCES, SRC_TEST_JAVA, + SRC_TEST_RESOURCES, SPRING_CONFIG_ROOT); + } + + @Override + public boolean isDefault() { + return true; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/PackagingProvider.java b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProvider.java new file mode 100644 index 000000000..f01d9b823 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProvider.java @@ -0,0 +1,74 @@ +package org.springframework.roo.project.packaging; + +import java.util.Collection; + +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.GAV; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * Creates the initial set of artifacts for a given Maven packaging type. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface PackagingProvider { + + /** + * Creates the initial set of artifacts (files and directories) for a module + * with this type of packaging; this includes setting the POM's + * /project/packaging element to the desired value. + * + * @param topLevelPackage the top-level Java package for the new project or + * module (required) + * @param nullableProjectName the project name provided by the user (can be + * blank) + * @param javaVersion the Java version for this project or module (required) + * @param parentPom the Maven coordinates of the parent POM (can be + * null for none) + * @param moduleName the name of the module being created (blank for the + * root or only module) + * @param projectOperations in case it's required (never null) + * @return the path of the newly created POM + */ + String createArtifacts(JavaPackage topLevelPackage, String projectName, + String javaVersion, GAV parentPom, String moduleName, + ProjectOperations projectOperations); + + /** + * Returns the unique identifier of this {@link PackagingProvider}, for use + * in the Roo user interface. + *

    + * The intent of this method is to allow third-party addons to provide + * alternative behaviour for a given Maven packaging type. For example, the + * core Roo WAR packaging type will have an ID of "war". If the user wants + * to customise how WAR modules are generated, they can implement their own + * {@link PackagingProvider} with an ID of (say) "custom-war". Then when the + * user adds a new module to their project, the shell will offer them the + * choice of "WAR" and "CUSTOM-WAR" for the packaging type. + * + * @return a non-blank ID, unique when case is ignored + */ + String getId(); + + /** + * Returns the {@link Path}s to be created for this module, in addition to + * {@link Path#ROOT}. + * + * @return + */ + Collection getPaths(); + + /** + * Indicates whether this type of packaging should be used for new projects + * and modules by default, i.e. when the user doesn't specify the packaging. + *

    + * If the user defines their own {@link PackagingProvider}s, they should + * ensure that at most one of them returns true from this + * method. + * + * @return see above + */ + boolean isDefault(); +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderConverter.java b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderConverter.java new file mode 100644 index 000000000..5f93cfde8 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderConverter.java @@ -0,0 +1,48 @@ +package org.springframework.roo.project.packaging; + +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * A {@link Converter} for {@link PackagingProvider}s + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class PackagingProviderConverter implements Converter { + + @Reference PackagingProviderRegistry packagingProviderRegistry; + + public PackagingProvider convertFromText(final String value, + final Class targetType, final String optionContext) { + final PackagingProvider packagingProvider = packagingProviderRegistry + .getPackagingProvider(value); + Validate.notNull(packagingProvider, "Unsupported packaging id '%s'", + value); + return packagingProvider; + } + + public boolean getAllPossibleValues(final List completions, + final Class targetType, final String existingData, + final String optionContext, final MethodTarget target) { + for (final PackagingProvider packagingProvider : packagingProviderRegistry + .getAllPackagingProviders()) { + completions.add(new Completion(packagingProvider.getId() + .toUpperCase())); + } + return true; + } + + public boolean supports(final Class type, final String optionContext) { + return PackagingProvider.class.isAssignableFrom(type); + } +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderRegistry.java b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderRegistry.java new file mode 100644 index 000000000..f80663dac --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderRegistry.java @@ -0,0 +1,35 @@ +package org.springframework.roo.project.packaging; + +import java.util.Collection; + +/** + * A registry for {@link PackagingProvider}s. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface PackagingProviderRegistry { + + /** + * Returns all known {@link PackagingProvider}s + * + * @return a non-null list (might be empty) + */ + Collection getAllPackagingProviders(); + + /** + * Returns the {@link PackagingProvider} to be used when the user doesn't + * specify one. + * + * @return a non-null instance + */ + PackagingProvider getDefaultPackagingProvider(); + + /** + * Returns the {@link PackagingProvider} with the given ID. + * + * @param id the ID to look for; see {@link PackagingProvider#getId()} + * @return null if there's no such instance + */ + PackagingProvider getPackagingProvider(String id); +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderRegistryImpl.java b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderRegistryImpl.java new file mode 100644 index 000000000..c8b927a63 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/PackagingProviderRegistryImpl.java @@ -0,0 +1,81 @@ +package org.springframework.roo.project.packaging; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.Service; + +/** + * The {@link PackagingProviderRegistry} implementation. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +@Reference(name = "packagingProvider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = PackagingProvider.class, cardinality = ReferenceCardinality.MANDATORY_MULTIPLE) +public class PackagingProviderRegistryImpl implements PackagingProviderRegistry { + + private final Object mutex = new Object(); + // Using a map avoids each PackagingProvider having to implement equals() + // properly (when removing) + private final Map packagingProviders = new HashMap(); + + protected void bindPackagingProvider( + final PackagingProvider packagingProvider) { + synchronized (mutex) { + final PackagingProvider previousPackagingProvider = packagingProviders + .put(packagingProvider.getId(), packagingProvider); + Validate.isTrue(previousPackagingProvider == null, + "More than one PackagingProvider with ID = '%s'", + packagingProvider.getId()); + } + } + + public Collection getAllPackagingProviders() { + return new ArrayList(packagingProviders.values()); + } + + public PackagingProvider getDefaultPackagingProvider() { + PackagingProvider defaultCoreProvider = null; + for (final PackagingProvider packagingProvider : packagingProviders + .values()) { + if (packagingProvider.isDefault()) { + if (packagingProvider instanceof CorePackagingProvider) { + defaultCoreProvider = packagingProvider; + } + else { + return packagingProvider; + } + } + } + Validate.validState(defaultCoreProvider != null, + "Should have found a default core PackagingProvider"); + return defaultCoreProvider; + } + + public PackagingProvider getPackagingProvider(final String id) { + for (final PackagingProvider packagingProvider : packagingProviders + .values()) { + if (packagingProvider.getId().equalsIgnoreCase(id)) { + return packagingProvider; + } + } + return null; + } + + protected void unbindPackagingProvider( + final PackagingProvider packagingProvider) { + synchronized (mutex) { + packagingProviders.remove(packagingProvider.getId()); + } + } +} \ No newline at end of file diff --git a/project/src/main/java/org/springframework/roo/project/packaging/PomPackaging.java b/project/src/main/java/org/springframework/roo/project/packaging/PomPackaging.java new file mode 100644 index 000000000..939df9900 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/PomPackaging.java @@ -0,0 +1,37 @@ +package org.springframework.roo.project.packaging; + +import java.util.Collection; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * The Maven "pom" {@link PackagingProvider} + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class PomPackaging extends AbstractCorePackagingProvider { + + /** + * Constructor + */ + public PomPackaging() { + super("pom", "parent-pom-template.xml"); + } + + @Override + protected void createOtherArtifacts(final JavaPackage topLevelPackage, + final String module, final ProjectOperations projectOperations) { + // No artifacts are applicable for POM modules + } + + public Collection getPaths() { + return null; + } +} diff --git a/project/src/main/java/org/springframework/roo/project/packaging/WarPackaging.java b/project/src/main/java/org/springframework/roo/project/packaging/WarPackaging.java new file mode 100644 index 000000000..6c93d4aa9 --- /dev/null +++ b/project/src/main/java/org/springframework/roo/project/packaging/WarPackaging.java @@ -0,0 +1,50 @@ +package org.springframework.roo.project.packaging; + +import static org.springframework.roo.project.Path.SPRING_CONFIG_ROOT; +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; +import static org.springframework.roo.project.Path.SRC_MAIN_WEBAPP; +import static org.springframework.roo.project.Path.SRC_TEST_JAVA; +import static org.springframework.roo.project.Path.SRC_TEST_RESOURCES; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.model.JavaPackage; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.ProjectOperations; + +/** + * The core {@link PackagingProvider} for web modules. + * + * @author Andrew Swan + * @since 1.2.0 + */ +@Component +@Service +public class WarPackaging extends AbstractCorePackagingProvider { + + public WarPackaging() { + super("war", "war-pom-template.xml"); + } + + @Override + protected void createOtherArtifacts(final JavaPackage topLevelPackage, + final String module, final ProjectOperations projectOperations) { + super.createOtherArtifacts(topLevelPackage, module, projectOperations); + if (StringUtils.isBlank(module)) { + // This is a single-module web project + final String fullyQualifiedModuleName = getFullyQualifiedModuleName( + module, projectOperations); + getApplicationContextOperations().createMiddleTierApplicationContext( + topLevelPackage, fullyQualifiedModuleName); + } + } + + public Collection getPaths() { + return Arrays.asList(SRC_MAIN_JAVA, SRC_TEST_JAVA, SRC_TEST_RESOURCES, + SPRING_CONFIG_ROOT, SRC_MAIN_WEBAPP); + } +} diff --git a/project/src/main/resources/org/springframework/roo/project/applicationContext-template.xml b/project/src/main/resources/org/springframework/roo/project/applicationContext-template.xml new file mode 100644 index 000000000..b5a563d3f --- /dev/null +++ b/project/src/main/resources/org/springframework/roo/project/applicationContext-template.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + diff --git a/project/src/main/resources/org/springframework/roo/project/packaging/jar-pom-template.xml b/project/src/main/resources/org/springframework/roo/project/packaging/jar-pom-template.xml new file mode 100644 index 000000000..e99a00fac --- /dev/null +++ b/project/src/main/resources/org/springframework/roo/project/packaging/jar-pom-template.xml @@ -0,0 +1,304 @@ + + + + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + + 4.0.0 + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + 0.1.0.BUILD-SNAPSHOT + TO_BE_CHANGED_BY_LISTENER + + 1.8.1 + JAVA_VERSION + UTF-8 + 2.0.0.BUILD-SNAPSHOT + 1.7.5 + 3.2.8.RELEASE + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-maven-milestone + Spring Maven Milestone Repository + http://maven.springframework.org/milestone + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-maven-milestone + Spring Maven Milestone Repository + http://maven.springframework.org/milestone + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + + junit + junit + 4.11 + test + + + log4j + log4j + 1.2.17 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + javax.servlet + servlet-api + 2.5 + provided + + + net.sf.flexjson + flexjson + 2.1 + + + org.apache.commons + commons-lang3 + 3.1 + + + + org.springframework.roo + org.springframework.roo.annotations + ${roo.version} + provided + + + + org.springframework + spring-core + ${spring.version} + + + commons-logging + commons-logging + + + + + org.springframework + spring-test + ${spring.version} + test + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-aop + ${spring.version} + + + org.springframework + spring-aspects + ${spring.version} + + + + + + org.apache.maven.plugins + maven-war-plugin + 2.2 + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.codehaus.mojo + aspectj-maven-plugin + 1.4 + + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + process-sources + + compile + test-compile + + + + + true + + + org.springframework + spring-aspects + + + ${java.version} + ${java.version} + + false + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + false + true + + **/*_Roo_* + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.3 + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.7 + + true + false + 2.0 + + + org.eclipse.ajdt.core.ajbuilder + + org.springframework.aspects + + + + org.springframework.ide.eclipse.core.springbuilder + + + + org.eclipse.ajdt.ui.ajnature + com.springsource.sts.roo.core.nature + org.springframework.ide.eclipse.core.springnature + + + + + org.apache.maven.plugins + maven-idea-plugin + 2.2 + + true + true + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + + org.mortbay.jetty + jetty-maven-plugin + 8.1.4.v20120524 + + + /${project.name} + + + + + + diff --git a/project/src/main/resources/org/springframework/roo/project/packaging/log4j.properties-template b/project/src/main/resources/org/springframework/roo/project/packaging/log4j.properties-template new file mode 100644 index 000000000..5014b358b --- /dev/null +++ b/project/src/main/resources/org/springframework/roo/project/packaging/log4j.properties-template @@ -0,0 +1,17 @@ +log4j.rootLogger=error, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +log4j.appender.R=org.apache.log4j.RollingFileAppender +log4j.appender.R.File=application.log + +log4j.appender.R.MaxFileSize=100KB +# Keep one backup file +log4j.appender.R.MaxBackupIndex=1 + +log4j.appender.R.layout=org.apache.log4j.PatternLayout +log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n diff --git a/project/src/main/resources/org/springframework/roo/project/packaging/parent-pom-template.xml b/project/src/main/resources/org/springframework/roo/project/packaging/parent-pom-template.xml new file mode 100644 index 000000000..f852f3ef3 --- /dev/null +++ b/project/src/main/resources/org/springframework/roo/project/packaging/parent-pom-template.xml @@ -0,0 +1,320 @@ + + + + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + + 4.0.0 + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + 0.1.0.BUILD-SNAPSHOT + TO_BE_CHANGED_BY_LISTENER + + 1.8.1 + JAVA_VERSION + UTF-8 + 2.0.0.BUILD-SNAPSHOT + 1.7.5 + 3.2.8.RELEASE + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-maven-milestone + Spring Maven Milestone Repository + http://maven.springframework.org/milestone + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-maven-milestone + Spring Maven Milestone Repository + http://maven.springframework.org/milestone + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + org.springframework.roo + org.springframework.roo.annotations + + + + + + + junit + junit + 4.11 + test + + + log4j + log4j + 1.2.17 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + javax.servlet + servlet-api + 2.5 + provided + + + net.sf.flexjson + flexjson + 2.1 + + + org.apache.commons + commons-lang3 + 3.1 + + + + org.springframework.roo + org.springframework.roo.annotations + ${roo.version} + provided + + + + org.springframework + spring-core + ${spring.version} + + + commons-logging + commons-logging + + + + + org.springframework + spring-test + ${spring.version} + test + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-aop + ${spring.version} + + + org.springframework + spring-aspects + ${spring.version} + + + org.springframework + spring-tx + ${spring.version} + + + + + + + + org.apache.maven.plugins + maven-war-plugin + 2.3 + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.codehaus.mojo + aspectj-maven-plugin + 1.4 + + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + process-sources + + compile + test-compile + + + + + true + + + org.springframework + spring-aspects + + + ${java.version} + ${java.version} + + false + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + ${project.build.sourceEncoding} + ${project.build.outputDirectory} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + false + true + + **/*_Roo_* + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.3 + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.7 + + true + false + 2.0 + + + org.eclipse.ajdt.core.ajbuilder + + org.springframework.aspects + + + + org.springframework.ide.eclipse.core.springbuilder + + + + org.eclipse.ajdt.ui.ajnature + com.springsource.sts.roo.core.nature + org.springframework.ide.eclipse.core.springnature + + + + + org.apache.maven.plugins + maven-idea-plugin + 2.2 + + true + true + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + + org.mortbay.jetty + jetty-maven-plugin + 8.1.4.v20120524 + + + /${project.name} + + + + + + + diff --git a/project/src/main/resources/org/springframework/roo/project/packaging/war-pom-template.xml b/project/src/main/resources/org/springframework/roo/project/packaging/war-pom-template.xml new file mode 100644 index 000000000..252198486 --- /dev/null +++ b/project/src/main/resources/org/springframework/roo/project/packaging/war-pom-template.xml @@ -0,0 +1,304 @@ + + + + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + + 4.0.0 + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + TO_BE_CHANGED_BY_LISTENER + 0.1.0.BUILD-SNAPSHOT + TO_BE_CHANGED_BY_LISTENER + + 1.8.1 + JAVA_VERSION + UTF-8 + 2.0.0.BUILD-SNAPSHOT + 1.7.5 + 3.2.8.RELEASE + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-maven-milestone + Spring Maven Milestone Repository + http://maven.springframework.org/milestone + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + spring-maven-release + Spring Maven Release Repository + http://maven.springframework.org/release + + + spring-maven-milestone + Spring Maven Milestone Repository + http://maven.springframework.org/milestone + + + spring-roo-repository + Spring Roo Repository + http://spring-roo-repository.springsource.org/release + + + + + + junit + junit + 4.11 + test + + + log4j + log4j + 1.2.17 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + javax.servlet + servlet-api + 2.5 + provided + + + net.sf.flexjson + flexjson + 2.1 + + + org.apache.commons + commons-lang3 + 3.1 + + + + org.springframework.roo + org.springframework.roo.annotations + ${roo.version} + provided + + + + org.springframework + spring-core + ${spring.version} + + + commons-logging + commons-logging + + + + + org.springframework + spring-test + ${spring.version} + test + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-aop + ${spring.version} + + + org.springframework + spring-aspects + ${spring.version} + + + + + + org.apache.maven.plugins + maven-war-plugin + 2.3 + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.codehaus.mojo + aspectj-maven-plugin + 1.4 + + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + process-sources + + compile + test-compile + + + + + true + + + org.springframework + spring-aspects + + + ${java.version} + ${java.version} + + false + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + false + true + + **/*_Roo_* + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.3 + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + true + false + 2.0 + + + org.eclipse.ajdt.core.ajbuilder + + org.springframework.aspects + + + + org.springframework.ide.eclipse.core.springbuilder + + + + org.eclipse.ajdt.ui.ajnature + com.springsource.sts.roo.core.nature + org.springframework.ide.eclipse.core.springnature + + + + + org.apache.maven.plugins + maven-idea-plugin + 2.2 + + true + true + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + + org.mortbay.jetty + jetty-maven-plugin + 8.1.4.v20120524 + + + /${project.name} + + + + + + diff --git a/project/src/test/java/org/springframework/roo/project/ConfigurationTest.java b/project/src/test/java/org/springframework/roo/project/ConfigurationTest.java new file mode 100644 index 000000000..6b0534d53 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/ConfigurationTest.java @@ -0,0 +1,21 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +import org.junit.Test; +import org.w3c.dom.Element; + +/** + * Unit test of the {@link Configuration} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ConfigurationTest { + + @Test + public void testInstanceDoesNotEqualNull() { + assertFalse(new Configuration(mock(Element.class)).equals(null)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/DefaultPathResolvingStrategyTest.java b/project/src/test/java/org/springframework/roo/project/DefaultPathResolvingStrategyTest.java new file mode 100644 index 000000000..9282caffa --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/DefaultPathResolvingStrategyTest.java @@ -0,0 +1,107 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.AbstractPathResolvingStrategy.ROOT_MODULE; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.FileUtils; + +/** + * Unit test of {@link DefaultPathResolvingStrategy} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DefaultPathResolvingStrategyTest { + + private static final String WORKING_DIRECTORY; + + static { + try { + WORKING_DIRECTORY = new File(".").getCanonicalPath(); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + // Fixture + private DefaultPathResolvingStrategy pathResolvingStrategy; + + /** + * Creates a mock {@link ComponentContext} in which the + * {@link OSGiUtils#ROO_WORKING_DIRECTORY_PROPERTY} has the given value + * + * @param rooWorkingDirectory the desired property value (can be blank) + * @return a non-null mock + */ + private ComponentContext getMockComponentContext( + final String rooWorkingDirectory) { + final BundleContext mockBundleContext = mock(BundleContext.class); + when( + mockBundleContext + .getProperty(OSGiUtils.ROO_WORKING_DIRECTORY_PROPERTY)) + .thenReturn(rooWorkingDirectory); + final ComponentContext mockComponentContext = mock(ComponentContext.class); + when(mockComponentContext.getBundleContext()).thenReturn( + mockBundleContext); + return mockComponentContext; + } + + @Before + public void setUp() { + // Object under test + pathResolvingStrategy = new DefaultPathResolvingStrategy(); + } + + @Test + public void testGetModulePaths() { + // Set up + pathResolvingStrategy.activate(getMockComponentContext(null)); + + // Invoke + final List modulePaths = pathResolvingStrategy + .getPhysicalPaths(); + + // Check + assertEquals(Path.values().length, modulePaths.size()); + for (int i = 0; i < modulePaths.size(); i++) { + final PhysicalPath modulePath = modulePaths.get(i); + final LogicalPath modulePathId = modulePath.getLogicalPath(); + final Path subPath = Path.values()[i]; + assertEquals(ROOT_MODULE, modulePathId.getModule()); + assertEquals(subPath, modulePathId.getPath()); + assertEquals( + new File(WORKING_DIRECTORY, subPath.getDefaultLocation()), + modulePath.getLocation()); + } + } + + @Test + public void testGetRootOfPath() { + // Set up + pathResolvingStrategy.activate(getMockComponentContext(null)); + final LogicalPath mockContextualPath = mock(LogicalPath.class); + when(mockContextualPath.getPath()).thenReturn(Path.SRC_MAIN_JAVA); + + // Invoke + final String root = pathResolvingStrategy.getRoot(mockContextualPath); + + // Check + final String srcMainJava = FileUtils.getSystemDependentPath("src", + "main", "java"); + assertTrue("Expected the root to end with '" + srcMainJava + + "', but was '" + root + "'", root.endsWith(srcMainJava)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/DependencyTest.java b/project/src/test/java/org/springframework/roo/project/DependencyTest.java new file mode 100644 index 000000000..8d86dac50 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/DependencyTest.java @@ -0,0 +1,203 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.DependencyScope.PROVIDED; +import static org.springframework.roo.project.DependencyType.ZIP; + +import org.junit.Test; +import org.w3c.dom.Element; + +/** + * Unit test of the {@link Dependency} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DependencyTest extends XmlTestCase { + + private static final String DEPENDENCY_ARTIFACT_ID = "foo-api"; + private static final String DEPENDENCY_GROUP_ID = "com.bar"; + private static final String DEPENDENCY_VERSION = "6.6.6"; + + private static final String EXCLUSION_ARTIFACT_ID = "ugly-api"; + private static final String EXCLUSION_GROUP_ID = "com.ugliness"; + + private static final String EXPECTED_ELEMENT_FOR_MINIMAL_DEPENDENCY = "\n" + + "\n" + + " " + + DEPENDENCY_GROUP_ID + + "\n" + + " " + + DEPENDENCY_ARTIFACT_ID + + "\n" + + " " + + DEPENDENCY_VERSION + + "\n" + ""; + + @Test + public void testAddExclusion() { + // Set up + final Dependency dependency = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + final int originalExclusionCount = dependency.getExclusions().size(); + + // Invoke + dependency.addExclusion(EXCLUSION_GROUP_ID, EXCLUSION_ARTIFACT_ID); + + // Check + assertEquals(originalExclusionCount + 1, dependency.getExclusions() + .size()); + } + + @Test + public void testConstructFromGav() { + // Set up + final GAV mockGav = mock(GAV.class); + when(mockGav.getGroupId()).thenReturn(DEPENDENCY_GROUP_ID); + when(mockGav.getArtifactId()).thenReturn(DEPENDENCY_ARTIFACT_ID); + when(mockGav.getVersion()).thenReturn(DEPENDENCY_VERSION); + + // Invoke + final Dependency dependency = new Dependency(mockGav, + DependencyType.ZIP, DependencyScope.SYSTEM); + + // Check + assertEquals(DEPENDENCY_GROUP_ID, dependency.getGroupId()); + assertEquals(DEPENDENCY_ARTIFACT_ID, dependency.getArtifactId()); + assertEquals(DEPENDENCY_VERSION, dependency.getVersion()); + assertEquals(DependencyScope.SYSTEM, dependency.getScope()); + assertEquals(DependencyType.ZIP, dependency.getType()); + } + + @Test + public void testConstructWithCustomTypeAndScope() { + // Set up + final Dependency dependency = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION, ZIP, PROVIDED); + + // Invoke and check + assertEquals(ZIP, dependency.getType()); + assertEquals(PROVIDED, dependency.getScope()); + } + + @Test + public void testDependenciesWithDifferentVersionsAreNotEqual() { + // Set up + final Dependency dependency1 = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + final Dependency dependency2 = new Dependency(dependency1.getGroupId(), + dependency1.getArtifactId(), dependency1.getVersion() + "x"); + + // Invoke + final boolean equal = dependency1.equals(dependency2); + + // Check + assertFalse(equal); + } + + @Test + public void testDependenciesWithDifferentVersionsHaveSameCoordinates() { + // Set up + final Dependency dependency1 = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + final Dependency dependency2 = new Dependency(dependency1.getGroupId(), + dependency1.getArtifactId(), dependency1.getVersion() + "x"); + + // Invoke + final boolean same = dependency1.hasSameCoordinates(dependency2); + + // Check + assertTrue(same); + } + + @Test + public void testDependenciesWithSameVersionAreEqual() { + // Set up + final Dependency dependency1 = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + final Dependency dependency2 = new Dependency(dependency1.getGroupId(), + dependency1.getArtifactId(), dependency1.getVersion()); + + // Invoke + final boolean equal = dependency1.equals(dependency2); + + // Check + assertTrue(equal); + } + + @Test + public void testDependenciesWithSameVersionHaveSameCoordinates() { + // Set up + final Dependency dependency1 = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + final Dependency dependency2 = new Dependency(dependency1.getGroupId(), + dependency1.getArtifactId(), dependency1.getVersion()); + + // Invoke + final boolean same = dependency1.hasSameCoordinates(dependency2); + + // Check + assertTrue(same); + } + + @Test + public void testEarIsHigherThanJar() { + assertTrue(Dependency.isHigherLevel("ear", "jar")); + } + + @Test + public void testEarIsHigherThanWar() { + assertTrue(Dependency.isHigherLevel("ear", "war")); + } + + @Test + public void testGetElementForMinimalDependency() { + // Set up + final Dependency dependency = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + + // Invoke + final Element element = dependency.getElement(DOCUMENT_BUILDER + .newDocument()); + + // Check + assertXmlEquals(EXPECTED_ELEMENT_FOR_MINIMAL_DEPENDENCY, element); + } + + @Test + public void testJarIsNotHigherThanItself() { + assertFalse(Dependency.isHigherLevel("jar", "jar")); + } + + @Test + public void testJarIsNotHigherThanWar() { + assertFalse(Dependency.isHigherLevel("jar", "war")); + } + + @Test + public void testNullDependencyDoesNotHaveSameCoordinates() { + // Set up + final Dependency dependency = new Dependency(DEPENDENCY_GROUP_ID, + DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + + // Invoke + final boolean same = dependency.hasSameCoordinates(null); + + // Check + assertFalse(same); + } + + @Test + public void testPomIsHigherThanWar() { + assertTrue(Dependency.isHigherLevel("ear", "war")); + } + + @Test + public void testWarIsHigherThanJar() { + assertTrue(Dependency.isHigherLevel("war", "jar")); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/DependencyTypeTest.java b/project/src/test/java/org/springframework/roo/project/DependencyTypeTest.java new file mode 100644 index 000000000..d3745f1a3 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/DependencyTypeTest.java @@ -0,0 +1,39 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Unit test of the {@link DependencyType} enum. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DependencyTypeTest { + + @Test + public void testValueOfEmptyCode() { + assertEquals(DependencyType.JAR, DependencyType.valueOfTypeCode("")); + } + + @Test + public void testValueOfKnownCodes() { + for (final DependencyType dependencyType : DependencyType.values()) { + assertEquals(dependencyType, + DependencyType.valueOfTypeCode(dependencyType.name() + .toLowerCase())); + } + } + + @Test + public void testValueOfNullCode() { + assertEquals(DependencyType.JAR, DependencyType.valueOfTypeCode(null)); + } + + @Test + public void testValueOfUnknownCode() { + assertEquals(DependencyType.OTHER, + DependencyType.valueOfTypeCode("guff")); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/ExecutionTest.java b/project/src/test/java/org/springframework/roo/project/ExecutionTest.java new file mode 100644 index 000000000..db6bfa11c --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/ExecutionTest.java @@ -0,0 +1,98 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Unit test of the {@link Execution} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ExecutionTest extends XmlTestCase { + + private static final String EXECUTION_CONFIGURATION_XML = " \n" + + " \n" + + " src/main/groovy\n" + + " \n" + " \n"; + private static final String GOAL_1 = "lock"; + private static final String GOAL_2 = "load"; + private static final String[] GOALS = { GOAL_1, GOAL_2 }; + private static final String ID = "some-id"; + private static final String PHASE = "test"; + private static final String EXECUTION_XML = "\n" + + "\n" + + " " + + ID + + "\n" + + " " + + PHASE + + "\n" + + " \n" + + " " + + GOAL_1 + + "\n" + + " " + + GOAL_2 + + "\n" + + " \n" + EXECUTION_CONFIGURATION_XML + ""; + + // Fixture + @Mock private Configuration mockConfiguration; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testExecutionWithConfigurationDoesNotEqualOneWithout() { + // Set up + final Execution execution1 = new Execution(ID, PHASE, GOALS); + final Execution execution2 = new Execution(ID, PHASE, + mockConfiguration, GOALS); + + // Invoke + assertFalse(execution1.equals(execution2)); + assertFalse(execution2.equals(execution1)); + } + + @Test + public void testGetElementForMinimalExecution() throws Exception { + // Set up + final Document document = DOCUMENT_BUILDER.newDocument(); + final Configuration mockConfiguration = mock(Configuration.class); + when(mockConfiguration.getConfiguration()).thenReturn( + XmlUtils.stringToElement(EXECUTION_CONFIGURATION_XML)); + final Execution execution = new Execution(ID, PHASE, mockConfiguration, + GOALS); + + // Invoke + final Element element = execution.getElement(document); + + // Check + assertXmlEquals(EXECUTION_XML, element); + } + + @Test + public void testIdenticalExecutionsAreEqual() { + assertEquals(new Execution(ID, PHASE, mockConfiguration, GOALS), + new Execution(ID, PHASE, mockConfiguration, GOALS)); + } + + @Test + public void testIdenticalExecutionsWithNoConfigurationAreEqual() { + assertEquals(new Execution(ID, PHASE, GOALS), new Execution(ID, PHASE, + GOALS)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/GAVTest.java b/project/src/test/java/org/springframework/roo/project/GAVTest.java new file mode 100644 index 000000000..5710b8c7e --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/GAVTest.java @@ -0,0 +1,80 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +/** + * Unit test of {@link GAV} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class GAVTest { + + private static final String GROUP_ID = "org.apache.maven"; + private static final String VERSION = "5.6"; + private static final String ARTIFACT_ID = "maven-surefire-plugin"; + private static final GAV GAV_1A = new GAV(GROUP_ID, ARTIFACT_ID, VERSION); + private static final GAV GAV_1B = new GAV(GROUP_ID, ARTIFACT_ID, VERSION); + private static final GAV GAV_2 = new GAV(GROUP_ID, ARTIFACT_ID, VERSION + + ".1"); + + @Test + public void testConstructorAndGetters() { + // Set up + + // Invoke + final GAV gav = new GAV(GROUP_ID, ARTIFACT_ID, VERSION); + + // Check + assertEquals(GROUP_ID, gav.getGroupId()); + assertEquals(ARTIFACT_ID, gav.getArtifactId()); + assertEquals(VERSION, gav.getVersion()); + } + + @Test + public void testGetInstance() { + // Set up + final String coordinates = StringUtils.join(new String[] { GROUP_ID, + ARTIFACT_ID, VERSION }, MavenUtils.COORDINATE_SEPARATOR); + + // Invoke + final GAV gav = GAV.getInstance(coordinates); + + // Check + assertEquals(GROUP_ID, gav.getGroupId()); + assertEquals(ARTIFACT_ID, gav.getArtifactId()); + assertEquals(VERSION, gav.getVersion()); + } + + @Test + public void testInstancesWithDifferentVersionsAreNotEqual() { + assertFalse(GAV_1A.equals(GAV_2)); + assertFalse(GAV_2.equals(GAV_1A)); + } + + @Test + public void testInstancesWithDifferentVersionsCompareUnequally() { + assertFalse(GAV_1A.compareTo(GAV_2) == 0); + } + + @Test + public void testInstancesWithSameCoordinatesAreEqual() { + assertTrue(GAV_1A.equals(GAV_1B)); + assertTrue(GAV_1B.equals(GAV_1A)); + } + + @Test + public void testInstancesWithSameCoordinatesCompareEqually() { + assertEquals(0, GAV_1A.compareTo(GAV_1B)); + } + + @Test + public void testInstancesWithSameCoordinatesHaveSameHashCode() { + assertEquals(GAV_1A.hashCode(), GAV_1B.hashCode()); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/LogicalPathTest.java b/project/src/test/java/org/springframework/roo/project/LogicalPathTest.java new file mode 100644 index 000000000..90ede6ab8 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/LogicalPathTest.java @@ -0,0 +1,183 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.roo.project.LogicalPath.MODULE_PATH_SEPARATOR; + +import org.junit.Test; + +/** + * Unit test of {@link LogicalPath} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class LogicalPathTest { + + private static final Path PATH = Path.SRC_TEST_JAVA; // arbitrary; can't be + private static final String MODULE_NAME = "web"; + private static final String MODULE_PLUS_PATH = MODULE_NAME + + MODULE_PATH_SEPARATOR + PATH.name(); + + /** + * Asserts that the given instance has the expected values + * + * @param instance the instance to check (required) + * @param expectedInstanceName + * @param expectedModuleName + */ + private void assertContextualPath(final LogicalPath instance, + final String expectedInstanceName, final String expectedModuleName) { + assertEquals(expectedInstanceName, instance.getName()); + assertEquals(expectedModuleName, instance.getModule()); + assertEquals(PATH, instance.getPath()); + assertEquals(instance.getName(), instance.toString()); + } + + /** + * Asserts that calling {@link LogicalPath#getInstance(Path, String)} with + * the given module name results in the expected behaviour + * + * @param inputModuleName + * @param expectedModuleName + * @param expectedInstanceName + */ + private void assertGetInstance(final String inputModuleName, + final String expectedModuleName, final String expectedInstanceName) { + // Set up + + // Invoke + final LogicalPath instance = LogicalPath.getInstance(PATH, + inputModuleName); + + // Check + assertContextualPath(instance, expectedInstanceName, expectedModuleName); + } + + @Test(expected = NullPointerException.class) + public void testCompareToNull() { + LogicalPath.getInstance(PATH, MODULE_NAME).compareTo(null); + } + + @Test + public void testDoesNotEqualOtherType() { + assertFalse(LogicalPath.getInstance(PATH, MODULE_NAME).equals(PATH)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetInstanceFromBlankString() { + LogicalPath.getInstance(" "); + } + + @Test + public void testGetInstanceFromCombinedPathAndModuleName() { + // Invoke + final LogicalPath instance = LogicalPath.getInstance(MODULE_PLUS_PATH); + + // Check + assertContextualPath(instance, MODULE_PLUS_PATH, MODULE_NAME); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetInstanceFromEmptyString() { + LogicalPath.getInstance(""); + } + + @Test(expected = NullPointerException.class) + public void testGetInstanceFromNullString() { + LogicalPath.getInstance((String) null); + } + + @Test + public void testGetInstanceFromPathNameOnly() { + // Invoke + final LogicalPath instance = LogicalPath.getInstance(PATH.name()); + + // Check + assertContextualPath(instance, PATH.name(), ""); + } + + @Test + public void testGetInstanceWithBlankModuleName() { + assertGetInstance(" ", "", PATH.toString()); + } + + @Test + public void testGetInstanceWithEmptyModuleName() { + assertGetInstance("", "", PATH.toString()); + } + + @Test + public void testGetInstanceWithNonBlankModuleName() { + assertGetInstance(MODULE_NAME, MODULE_NAME, MODULE_NAME + + MODULE_PATH_SEPARATOR + PATH.toString()); + } + + @Test + public void testGetInstanceWithNullModuleName() { + assertGetInstance(null, "", PATH.toString()); + } + + @Test + public void testModuleRootIsNotProjectRoot() { + assertFalse(LogicalPath.getInstance(Path.ROOT, "web").isProjectRoot()); + } + + @Test + public void testNonRootPathIsNotModuleRoot() { + assertFalse(LogicalPath.getInstance(Path.SRC_MAIN_JAVA, "") + .isModuleRoot()); + } + + @Test + public void testNonRootPathIsNotProjectRoot() { + assertFalse(LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, null) + .isProjectRoot()); + } + + @Test + public void testProjectRootIsModuleRoot() { + assertTrue(LogicalPath.getInstance(Path.ROOT, "").isModuleRoot()); + } + + @Test + public void testProjectRootIsProjectRoot() { + assertTrue(LogicalPath.getInstance(Path.ROOT, null).isProjectRoot()); + } + + @Test + public void testSamePathsInDifferentModulesAreNotEqual() { + // Set up + final LogicalPath instance1 = LogicalPath.getInstance(PATH, "module1"); + final LogicalPath instance2 = LogicalPath.getInstance(PATH, "module2"); + + // Invoke + final boolean equal = instance1.equals(instance2) + || instance2.equals(instance1); + + // Check + assertFalse(equal); + } + + @Test + public void testSamePathsInSameModuleAreEqual() { + // Set up + final LogicalPath instance1 = LogicalPath + .getInstance(PATH, MODULE_NAME); + final LogicalPath instance2 = LogicalPath + .getInstance(PATH, MODULE_NAME); + + // Invoke + final boolean equal = instance1.equals(instance2) + && instance2.equals(instance1); + + // Check + assertTrue(equal); + } + + @Test + public void testSubModuleRootIsModuleRoot() { + assertTrue(LogicalPath.getInstance(Path.ROOT, "foo").isModuleRoot()); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/MavenOperationsImplTest.java b/project/src/test/java/org/springframework/roo/project/MavenOperationsImplTest.java new file mode 100644 index 000000000..42ea6990e --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/MavenOperationsImplTest.java @@ -0,0 +1,173 @@ +package org.springframework.roo.project; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.maven.Pom; + +/** + * Unit test of {@link MavenOperationsImpl} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MavenOperationsImplTest { + + private static final String ARTIFACT_ID = "foo-lib"; + private static final String CLASSIFIER = "exe"; + private static final String GROUP_ID = "com.example"; + private static final String VERSION = "1.0.Final"; + private static final String POM_AFTER_DEPENDENCY_REMOVED = "\n" + + " \n" + + " \n" + + " com.example\n" + + " test-lib\n" + + " 2.0.Final\n" + + " test\n" + + " \n" + + " \n" + "\n"; + private static final String POM_BEFORE_DEPENDENCY_REMOVED = "" + + " " + " " + + " " + GROUP_ID + "" + + " " + ARTIFACT_ID + "" + + " " + VERSION + "" + + " provided" + + " " + CLASSIFIER + "" + + " " + " " + + " com.example" + + " test-lib" + + " 2.0.Final" + + " test" + " " + + " " + ""; + private static final String POM_PATH = "/any/old/path"; + private static final String SIMPLE_DESCRIPTION = "Foo Library"; + + @Mock private FileManager mockFileManager; + @Mock private MetadataService mockMetadataService; + @Mock private PathResolver mockPathResolver; + @Mock private PomManagementService mockPomManagementService; + @Mock private ProjectMetadata mockProjectMetadata; + + // Fixture + private MavenOperationsImpl projectOperations; + + private void assertModuleFocusAllowed(final boolean expectedResult, + final String... moduleNames) { + // Set up + when(mockPomManagementService.getModuleNames()).thenReturn( + Arrays.asList(moduleNames)); + + // Invoke and check + assertEquals(expectedResult, projectOperations.isModuleFocusAllowed()); + } + + @Before + public void setUp() { + // Mocks + MockitoAnnotations.initMocks(this); + + when( + mockPathResolver.getIdentifier(Path.ROOT.getModulePathId(""), + MavenProjectMetadataProvider.POM_RELATIVE_PATH)) + .thenReturn(POM_PATH); + + // Object under test + projectOperations = new MavenOperationsImpl(); + projectOperations.fileManager = mockFileManager; + projectOperations.metadataService = mockMetadataService; + projectOperations.pathResolver = mockPathResolver; + projectOperations.pomManagementService = mockPomManagementService; + } + + @Test + public void testCannotFocusModuleWhenMoreThanOneModuleExists() { + assertModuleFocusAllowed(true, "", "core"); + } + + @Test + public void testCannotFocusModuleWhenOneOrLessModulesExist() { + assertModuleFocusAllowed(false, ""); + } + + @Test + public void testGetFocusedModuleWhenChildModuleHasFocus() { + // Set up + when(mockPomManagementService.getFocusedModuleName()).thenReturn( + "child"); + final Pom mockChildPom = mock(Pom.class); + final ProjectMetadata mockChildMetadata = mock(ProjectMetadata.class); + when(mockChildMetadata.getPom()).thenReturn(mockChildPom); + when( + mockMetadataService.get(ProjectMetadata + .getProjectIdentifier("child"))).thenReturn( + mockChildMetadata); + + // Invoke and check + assertEquals(mockChildPom, projectOperations.getFocusedModule()); + } + + @Test + public void testGetFocusedModuleWhenNoModulesExist() { + // Set up + when(mockPomManagementService.getFocusedModuleName()).thenReturn(""); + when(mockMetadataService.get(ProjectMetadata.getProjectIdentifier(""))) + .thenReturn(null); + + // Invoke and check + assertNull(projectOperations.getFocusedModule()); + } + + @Test + public void testRemoveDependencyTwiceWhenItExistsOnce() { + // Set up + when(mockFileManager.getInputStream(POM_PATH)).thenReturn( + new ByteArrayInputStream(POM_BEFORE_DEPENDENCY_REMOVED + .getBytes())); + + // -- Dependency to remove + final Dependency mockDependency = mock(Dependency.class); + when(mockDependency.getArtifactId()).thenReturn(ARTIFACT_ID); + when(mockDependency.getClassifier()).thenReturn(CLASSIFIER); + when(mockDependency.getGroupId()).thenReturn(GROUP_ID); + when(mockDependency.getSimpleDescription()).thenReturn( + SIMPLE_DESCRIPTION); + when(mockDependency.getType()).thenReturn(DependencyType.JAR); + when(mockDependency.getVersion()).thenReturn(VERSION); + when(mockMetadataService.get(ProjectMetadata.getProjectIdentifier(""))) + .thenReturn(mockProjectMetadata); + + final Pom pom = mock(Pom.class); + when(pom.getPath()).thenReturn(POM_PATH); + when(mockProjectMetadata.getPom()).thenReturn(pom); + + final Collection dependencies = Arrays.asList( + mockDependency, mockDependency); + when(pom.isAnyDependenciesRegistered(dependencies)).thenReturn(true); + when(pom.isDependencyRegistered(mockDependency)).thenReturn(true); + + // Invoke + projectOperations.removeDependencies("", dependencies); + + // Check + final String expectedPom = POM_AFTER_DEPENDENCY_REMOVED.replace("\n", + LINE_SEPARATOR); + verify(mockFileManager).createOrUpdateTextFileIfRequired(eq(POM_PATH), + eq(expectedPom), (String) any(), eq(false)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/MavenPathResolvingStrategyTest.java b/project/src/test/java/org/springframework/roo/project/MavenPathResolvingStrategyTest.java new file mode 100644 index 000000000..92559bae7 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/MavenPathResolvingStrategyTest.java @@ -0,0 +1,158 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.support.util.FileUtils; + +/** + * Unit test of {@link MavenPathResolvingStrategy} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MavenPathResolvingStrategyTest { + + private static final String NEW_MODULE = "new"; + private static final String PATH_RELATIVE_TO_POM = FileUtils + .getSystemDependentPath("src", "main", "java"); + private static final String POM_PATH = File.separator + + FileUtils.getSystemDependentPath("path", "to", "the", "pom"); + private static final String ROOT_MODULE = ""; + + @Mock private LogicalPath mockNonSourcePath; + @Mock private PomManagementService mockPomManagementService; + @Mock private LogicalPath mockSourcePath; + // Fixture + private MavenPathResolvingStrategy strategy; + + /** + * Asserts that calling + * {@link MavenPathResolvingStrategy#getIdentifier(LogicalPath, String)} + * with the given parameters results in the given expected identifier. + * + * @param pom the POM that the mock {@link PomManagementService} should + * return for the given module name (can be null) + * @param module the module to be returned by the {@link LogicalPath} + * @param relativePath cannot be null + * @param expectedIdentifier + */ + private void assertIdentifier(final Pom pom, final String module, + final String relativePath, final String expectedIdentifier) { + // Set up + final LogicalPath mockContextualPath = getMockContextualPath(module, + pom); + when(mockPomManagementService.getPomFromModuleName(module)).thenReturn( + pom); + + // Invoke + final String identifier = strategy.getIdentifier(mockContextualPath, + relativePath); + + // Check + assertEquals(expectedIdentifier, identifier.replaceFirst("[A-Z]:", "")); + } + + private LogicalPath getMockContextualPath(final String module, final Pom pom) { + final LogicalPath mockContextualPath = mock(LogicalPath.class); + when(mockContextualPath.getModule()).thenReturn(module); + when(mockContextualPath.getPathRelativeToPom(pom)).thenReturn( + PATH_RELATIVE_TO_POM); + return mockContextualPath; + } + + private PhysicalPath getMockModulePath(final boolean isSource, + final LogicalPath logicalPath) { + final PhysicalPath mockModulePath = mock(PhysicalPath.class); + when(mockModulePath.isSource()).thenReturn(isSource); + when(mockModulePath.getLogicalPath()).thenReturn(logicalPath); + return mockModulePath; + } + + private Pom getMockPom(final String rootPath) { + final Pom mockPom = mock(Pom.class); + when(mockPom.getRoot()).thenReturn(rootPath); + return mockPom; + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + strategy = new MavenPathResolvingStrategy(); + strategy.pomManagementService = mockPomManagementService; + } + + private void setUpModulePaths() { + final PhysicalPath mockModuleSourcePath = getMockModulePath(true, + mockSourcePath); + final PhysicalPath mockModuleNonSourcePath = getMockModulePath(false, + mockNonSourcePath); + final Pom mockPom = mock(Pom.class); + when(mockPom.getPhysicalPaths()).thenReturn( + Arrays.asList(mockModuleSourcePath, mockModuleNonSourcePath)); + when(mockPomManagementService.getPoms()).thenReturn( + Arrays.asList(mockPom)); + } + + @Test + public void testGetAllPaths() { + // Set up + setUpModulePaths(); + + // Invoke + final Object modulePathIds = strategy.getPaths(); + + // Check + assertEquals(Arrays.asList(mockSourcePath, mockNonSourcePath), + modulePathIds); + } + + @Test + public void testGetIdentifierForNewModuleWithEmptyRelativePath() { + final Pom mockParentPom = getMockPom(POM_PATH); + when(mockPomManagementService.getFocusedModule()).thenReturn( + mockParentPom); + final String expectedIdentifier = FileUtils.getSystemDependentPath( + POM_PATH, NEW_MODULE, PATH_RELATIVE_TO_POM) + File.separator; + assertIdentifier(null, NEW_MODULE, "", expectedIdentifier); + } + + @Test + public void testGetIdentifierForRootModuleWithEmptyRelativePath() { + final String expectedIdentifier = FileUtils.getSystemDependentPath( + POM_PATH, PATH_RELATIVE_TO_POM) + File.separator; + assertIdentifier(getMockPom(POM_PATH), ROOT_MODULE, "", + expectedIdentifier); + } + + @Test + public void testGetIdentifierForRootModuleWithNonEmptyRelativePath() { + final String relativePath = FileUtils.getSystemDependentPath("com", + "example", "domain", "PersonTest.java"); + final String expectedIdentifier = FileUtils.getSystemDependentPath( + POM_PATH, PATH_RELATIVE_TO_POM, relativePath); + assertIdentifier(getMockPom(POM_PATH), ROOT_MODULE, relativePath, + expectedIdentifier); + } + + @Test + public void testGetSourcePaths() { + // Set up + setUpModulePaths(); + + // Invoke + final Object modulePathIds = strategy.getSourcePaths(); + + // Check + assertEquals(Arrays.asList(mockSourcePath), modulePathIds); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/MavenUtilsTest.java b/project/src/test/java/org/springframework/roo/project/MavenUtilsTest.java new file mode 100644 index 000000000..47fe28199 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/MavenUtilsTest.java @@ -0,0 +1,35 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Unit test of {@link MavenUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MavenUtilsTest { + + @Test + public void testEmptyStringIsNotAValidId() { + assertFalse(MavenUtils.isValidMavenId("")); + } + + @Test + public void testNullIsNotAValidId() { + assertFalse(MavenUtils.isValidMavenId(null)); + } + + @Test + public void testValidArtifactIdIsAValidId() { + assertTrue(MavenUtils.isValidMavenId("spring-core")); + } + + @Test + public void testValidGroupIdIsAValidId() { + assertTrue(MavenUtils.isValidMavenId("org.springframework")); + } +} \ No newline at end of file diff --git a/project/src/test/java/org/springframework/roo/project/PathTest.java b/project/src/test/java/org/springframework/roo/project/PathTest.java new file mode 100644 index 000000000..816f72766 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/PathTest.java @@ -0,0 +1,31 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; + +import java.util.Collection; +import java.util.HashSet; + +import org.junit.Test; + +/** + * Unit test of the {@link Path} enum. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PathTest { + + @Test + public void testDefaultLocationsAreUnique() { + // Set up + final Collection distinctLocations = new HashSet(); + + // Invoke + for (final Path path : Path.values()) { + distinctLocations.add(path.getDefaultLocation()); + } + + // Check + assertEquals(Path.values().length, distinctLocations.size()); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/PluginTest.java b/project/src/test/java/org/springframework/roo/project/PluginTest.java new file mode 100644 index 000000000..00a0914e8 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/PluginTest.java @@ -0,0 +1,313 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.support.util.XmlUtils.stringToElement; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Unit test of the {@link Plugin} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PluginTest extends XmlTestCase { + + private static final String DEPENDENCY_ARTIFACT_ID = "huge-thing"; + private static final String DEPENDENCY_GROUP_ID = "com.acme"; + + private static final String DEPENDENCY_VERSION = "1.1"; + private static final String DEPENDENCY_XML = " \n" + + " " + DEPENDENCY_GROUP_ID + "\n" + + " " + DEPENDENCY_ARTIFACT_ID + + "\n" + " " + DEPENDENCY_VERSION + + "\n" + " \n"; + private static final String EXECUTION_CONFIGURATION_XML = " \n" + + " \n" + + " src/main/groovy\n" + + " \n" + " \n"; + + private static final String EXECUTION_GOAL = "add-tests"; + private static final String EXECUTION_ID = "build-it"; + private static final String EXECUTION_PHASE = "test"; + + private static final String EXECUTION_XML = " \n" + + " " + EXECUTION_ID + "\n" + + " " + EXECUTION_PHASE + "\n" + + " \n" + " " + + EXECUTION_GOAL + "\n" + " \n" + + EXECUTION_CONFIGURATION_XML + " \n"; + + private static final List NO_DEPENDENCIES = Collections + .emptyList(); + + private static final List NO_EXECUTIONS = Collections + .emptyList(); + + private static final String PLUGIN_ARTIFACT_ID = "bar"; + + private static final String PLUGIN_CONFIGURATION_XML = " \n" + + " \n" + + " classes\n" + + " \n" + " \n"; + + private static final String PLUGIN_GROUP_ID = "com.foo"; + + private static final String PLUGIN_VERSION = "1.2.3"; + + private static final String PLUGIN_XML_AV = "" + + "foo-plugin" + "1.6" + + ""; + + private static final String PLUGIN_XML_GAV = "" + + "org.codehaus.mojo" + + "build-helper-maven-plugin" + + "1.5" + ""; + + private static final String PLUGIN_XML_WITH_DEPENDENCY = "" + + "com.example" + + "ball-of-mud" + "1.4" + + "" + "" + "" + + DEPENDENCY_GROUP_ID + "" + "" + + DEPENDENCY_ARTIFACT_ID + "" + "" + + DEPENDENCY_VERSION + "" + "" + + "" + ""; + + private static final String PLUGIN_WITHOUT_VERSION_WITH_DEPENDENCY = "" + + "com.example" + + "ball-of-mud" + + "" + "" + "" + + DEPENDENCY_GROUP_ID + "" + "" + + DEPENDENCY_ARTIFACT_ID + "" + "" + + DEPENDENCY_VERSION + "" + "" + + "" + ""; + + private static final String PLUGIN_XML_WITH_EXECUTION = "" + + "tv.reality" + + "view-plugin" + "2.5" + + "" + "" + "" + EXECUTION_ID + "" + + "" + EXECUTION_PHASE + "" + "" + "" + + EXECUTION_GOAL + "" + "" + + EXECUTION_CONFIGURATION_XML + "" + "" + + ""; + + private static final String MAXIMAL_PLUGIN_XML = "\n" + + "\n" + + " " + + PLUGIN_GROUP_ID + + "\n" + + " " + + PLUGIN_ARTIFACT_ID + + "\n" + + " " + + PLUGIN_VERSION + + "\n" + + PLUGIN_CONFIGURATION_XML + + " \n" + + EXECUTION_XML + + " \n" + + " \n" + + DEPENDENCY_XML + + " \n" + ""; + + private static final String MININAL_PLUGIN_XML = "\n" + + "\n" + + " " + + PLUGIN_GROUP_ID + + "\n" + + " " + + PLUGIN_ARTIFACT_ID + + "\n" + + " " + PLUGIN_VERSION + "\n" + ""; + + /** + * Asserts that the given plugin returns the expected XML for its POM + * element + * + * @param plugin the plugin for which to check the XML (required) + * @param document the document in which to create the element (required) + * @param expectedXml the expected XML element; can be blank + */ + private void assertElement(final Plugin plugin, final Document document, + final String expectedXml) { + // Invoke + final Element element = plugin.getElement(document); + + // Check + assertXmlEquals(expectedXml, element); + } + + /** + * Asserts that the given {@link Plugin} has the given expected values + * + * @param groupId + * @param artifactId + * @param version + * @param configuration + * @param dependencies + * @param executions + * @param actual + */ + private void assertPluginEquals(final String groupId, + final String artifactId, final String version, + final Configuration configuration, + final List dependencies, + final List executions, final Plugin actual) { + assertEquals(artifactId, actual.getArtifactId()); + assertEquals(configuration, actual.getConfiguration()); + assertEquals(dependencies, actual.getDependencies()); + assertEquals(executions, actual.getExecutions()); + assertEquals(groupId, actual.getGroupId()); + assertEquals(groupId + ":" + artifactId + ":" + version, + actual.getSimpleDescription()); + assertEquals(version, actual.getVersion()); + } + + /** + * Asserts that constructing a {@link Plugin} from the given XML gives rise + * to the given properties + * + * @param xml the XML from which to construct the {@link Plugin} + * @param expectedGroupId + * @param expectedArtifactId + * @param expectedVersion + * @param expectedConfiguration + * @param expectedDependencies + * @param expectedExecutions + * @throws Exception + */ + private void assertPluginFromXml(final String xml, + final String expectedGroupId, final String expectedArtifactId, + final String expectedVersion, + final Configuration expectedConfiguration, + final List expectedDependencies, + final List expectedExecutions) { + // Set up + final Element pluginElement = stringToElement(xml); + + // Invoke + final Plugin plugin = new Plugin(pluginElement); + + // Check + assertPluginEquals(expectedGroupId, expectedArtifactId, + expectedVersion, expectedConfiguration, expectedDependencies, + expectedExecutions, plugin); + } + + @Test + public void testFullConstructor() { + // Set up + final Configuration mockConfiguration = mock(Configuration.class); + final List mockDependencies = Arrays.asList( + mock(Dependency.class), mock(Dependency.class)); + final List mockExecutions = Arrays.asList( + mock(Execution.class), mock(Execution.class)); + + // Invoke + final Plugin plugin = new Plugin(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID, + PLUGIN_VERSION, mockConfiguration, mockDependencies, + mockExecutions); + + // Check + assertPluginEquals(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID, PLUGIN_VERSION, + mockConfiguration, mockDependencies, mockExecutions, plugin); + } + + @Test + public void testGetElementForMaximalPlugin() throws Exception { + // Set up + final Configuration mockPluginConfiguration = mock(Configuration.class); + when(mockPluginConfiguration.getConfiguration()).thenReturn( + stringToElement(PLUGIN_CONFIGURATION_XML)); + + final Document document = DOCUMENT_BUILDER.newDocument(); + + final Dependency mockDependency = mock(Dependency.class); + final Element dependencyElement = (Element) document.importNode( + stringToElement(DEPENDENCY_XML), true); + when(mockDependency.getElement(document)).thenReturn(dependencyElement); + + final Execution mockExecution = mock(Execution.class); + final Element executionElement = (Element) document.importNode( + stringToElement(EXECUTION_XML), true); + when(mockExecution.getElement(document)).thenReturn(executionElement); + + final Plugin plugin = new Plugin(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID, + PLUGIN_VERSION, mockPluginConfiguration, + Arrays.asList(mockDependency), Arrays.asList(mockExecution)); + + // Invoke and check + assertElement(plugin, document, MAXIMAL_PLUGIN_XML); + } + + @Test + public void testGetElementForMinimalPlugin() { + assertElement(new Plugin(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID, + PLUGIN_VERSION), DOCUMENT_BUILDER.newDocument(), + MININAL_PLUGIN_XML); + } + + @Test + public void testGroupArtifactVersionConstructor() { + // Invoke + final Plugin plugin = new Plugin(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID, + PLUGIN_VERSION); + + // Check + assertPluginEquals(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID, PLUGIN_VERSION, + null, NO_DEPENDENCIES, NO_EXECUTIONS, plugin); + } + + @Test + public void testXmlElementConstructorWithArtifactAndVersion() + throws Exception { + // In this case we expect the default groupId to be used + assertPluginFromXml(PLUGIN_XML_AV, Plugin.DEFAULT_GROUP_ID, + "foo-plugin", "1.6", null, NO_DEPENDENCIES, NO_EXECUTIONS); + } + + @Test + public void testXmlElementConstructorWithGroupArtifactAndVersion() + throws Exception { + assertPluginFromXml(PLUGIN_XML_GAV, "org.codehaus.mojo", + "build-helper-maven-plugin", "1.5", null, NO_DEPENDENCIES, + NO_EXECUTIONS); + } + + @Test + public void testXmlElementConstructorWithOneDependency() throws Exception { + final Dependency expectedDependency = new Dependency( + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); + assertPluginFromXml(PLUGIN_XML_WITH_DEPENDENCY, "com.example", + "ball-of-mud", "1.4", null, Arrays.asList(expectedDependency), + NO_EXECUTIONS); + } + +// @Test(expected = IllegalArgumentException.class) +// public void testXmlElementConstructorWithoutVersionWithOneDependency() throws Exception { +// final Dependency expectedDependency = new Dependency( +// DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, DEPENDENCY_VERSION); +// assertPluginFromXml(PLUGIN_WITHOUT_VERSION_WITH_DEPENDENCY, "com.example", +// "ball-of-mud", "", null, Arrays.asList(expectedDependency), +// NO_EXECUTIONS); +// } + + @Test + public void testXmlElementConstructorWithOneExecution() throws Exception { + final Configuration executionConfiguration = new Configuration( + stringToElement(EXECUTION_CONFIGURATION_XML)); + final Execution execution = new Execution(EXECUTION_ID, + EXECUTION_PHASE, executionConfiguration, EXECUTION_GOAL); + assertPluginFromXml(PLUGIN_XML_WITH_EXECUTION, "tv.reality", + "view-plugin", "2.5", null, NO_DEPENDENCIES, + Arrays.asList(execution)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/PomManagementServiceImplTest.java b/project/src/test/java/org/springframework/roo/project/PomManagementServiceImplTest.java new file mode 100644 index 000000000..905edf30d --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/PomManagementServiceImplTest.java @@ -0,0 +1,275 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.file.monitor.FileMonitorService; +import org.springframework.roo.metadata.MetadataDependencyRegistry; +import org.springframework.roo.metadata.MetadataService; +import org.springframework.roo.process.manager.FileManager; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.project.maven.PomFactory; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.support.util.FileUtils; +import org.w3c.dom.Element; + +/** + * Unit test of {@link PomManagementServiceImpl} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PomManagementServiceImplTest { + + private static final String ROOT_MODULE_NAME = ""; + + @Mock private FileManager mockFileManager; + @Mock private FileMonitorService mockFileMonitorService; + @Mock private MetadataDependencyRegistry mockMetadataDependencyRegistry; + @Mock private MetadataService mockMetadataService; + @Mock private PomFactory mockPomFactory; + @Mock private Shell mockShell; + // Fixture + private PomManagementServiceImpl service; + + private String getCanonicalPath(final String relativePath) { + final String systemDependentPath = relativePath.replace("/", + File.separator); + final URL resource = getClass().getResource(systemDependentPath); + assertNotNull("Can't find '" + systemDependentPath + + "' on the classpath of " + getClass().getName(), resource); + try { + return new File(resource.toURI()).getCanonicalPath(); + } + catch (final Exception e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + private Pom getMockPom(final String moduleName, final String canonicalPath) { + final Pom mockPom = mock(Pom.class); + when(mockPom.getModuleName()).thenReturn(moduleName); + when(mockPom.getPath()).thenReturn(canonicalPath); + when( + mockPomFactory.getInstance(any(Element.class), + eq(canonicalPath), eq(moduleName))).thenReturn(mockPom); + return mockPom; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + service = new PomManagementServiceImpl(); + service.fileManager = mockFileManager; + service.fileMonitorService = mockFileMonitorService; + service.metadataDependencyRegistry = mockMetadataDependencyRegistry; + service.metadataService = mockMetadataService; + service.pomFactory = mockPomFactory; + service.shell = mockShell; + } + + /** + * Sets the working directory for the service under test + * + * @param relativePath the desired working directory relative to the + * "o.s.r.project" package + * @throws IOException + */ + private void setUpWorkingDirectory(final String relativePath) + throws IOException { + final ComponentContext mockComponentContext = mock(ComponentContext.class); + final BundleContext mockBundleContext = mock(BundleContext.class); + when(mockComponentContext.getBundleContext()).thenReturn( + mockBundleContext); + final File workingDirectory = new File( + "target/test-classes/org/springframework/roo/project", + relativePath); + when( + mockBundleContext + .getProperty(OSGiUtils.ROO_WORKING_DIRECTORY_PROPERTY)) + .thenReturn(workingDirectory.getCanonicalPath()); + service.activate(mockComponentContext); + } + + @Test + public void testGetPomOfSingleModuleProjectWhenParentHasNoRelativePath() + throws Exception { + // Set up + setUpWorkingDirectory("single"); + final String canonicalPath = getCanonicalPath("single/pom.xml"); + when( + mockFileMonitorService + .getDirtyFiles(PomManagementServiceImpl.class.getName())) + .thenReturn(Arrays.asList(canonicalPath)); + final Pom mockPom = getMockPom(ROOT_MODULE_NAME, canonicalPath); + + // Invoke + final Collection poms = service.getPoms(); + + // Check + assertEquals(1, poms.size()); + assertEquals(mockPom, poms.iterator().next()); + verifyProjectMetadataNotification(ROOT_MODULE_NAME); + } + + @Test + public void testGetPomsOfMultiModuleProjectWhenChildIsDirty() + throws Exception { + // Set up + setUpWorkingDirectory("multi"); + final String rootPom = FileUtils.getSystemDependentPath("multi", + "pom.xml"); + final String rootPomCanonicalPath = getCanonicalPath(rootPom); + final String childPom = FileUtils.getSystemDependentPath("multi", + "foo-child", "pom.xml"); + final String childPomCanonicalPath = getCanonicalPath(childPom); + final Collection dirtyFiles = Arrays + .asList(childPomCanonicalPath); + when( + mockFileMonitorService + .getDirtyFiles(PomManagementServiceImpl.class.getName())) + .thenReturn(dirtyFiles); + + final Pom mockRootPom = getMockPom(ROOT_MODULE_NAME, + rootPomCanonicalPath); + final String childModuleName = "foo-child"; + final Pom mockChildPom = getMockPom(childModuleName, + childPomCanonicalPath); + + when(mockFileManager.getInputStream(rootPomCanonicalPath)).thenReturn( + getClass().getResourceAsStream(rootPom)); + when(mockFileManager.getInputStream(childPomCanonicalPath)).thenReturn( + getClass().getResourceAsStream(childPom)); + + service.addPom(mockRootPom); + + // Invoke + final Collection poms = service.getPoms(); + + // Check + final Collection expectedPoms = Arrays.asList(mockRootPom, + mockChildPom); + assertEquals(expectedPoms.size(), poms.size()); + assertTrue(poms.containsAll(expectedPoms)); + verifyProjectMetadataNotification(childModuleName); + } + + @Test + public void testGetPomsOfMultiModuleProjectWhenParentAndChildAreDirty() + throws Exception { + // Set up + setUpWorkingDirectory("multi"); + final String rootPom = "multi/pom.xml"; + final String rootPomCanonicalPath = getCanonicalPath(rootPom); + final String childPom = "multi/foo-child/pom.xml"; + final String childPomCanonicalPath = getCanonicalPath(childPom); + final Collection dirtyFiles = Arrays.asList( + rootPomCanonicalPath, childPomCanonicalPath); + when( + mockFileMonitorService + .getDirtyFiles(PomManagementServiceImpl.class.getName())) + .thenReturn(dirtyFiles); + + final Pom mockRootPom = getMockPom(ROOT_MODULE_NAME, + rootPomCanonicalPath); + final String childModuleName = "foo-child"; + final Pom mockChildPom = getMockPom(childModuleName, + childPomCanonicalPath); + + when(mockFileManager.getInputStream(rootPomCanonicalPath)).thenReturn( + getClass().getResourceAsStream(rootPom)); + when(mockFileManager.getInputStream(childPomCanonicalPath)).thenReturn( + getClass().getResourceAsStream(childPom)); + + // Invoke + final Collection poms = service.getPoms(); + + // Check + final Collection expectedPoms = Arrays.asList(mockRootPom, + mockChildPom); + assertEquals(expectedPoms.size(), poms.size()); + assertTrue(poms.containsAll(expectedPoms)); + verifyProjectMetadataNotification(ROOT_MODULE_NAME, childModuleName); + } + + @Test + public void testGetPomsWhenNoPomsAreDirty() { + // Set up + final Collection dirtyFiles = Arrays.asList("not-a-pom.txt"); + when( + mockFileMonitorService + .getDirtyFiles(PomManagementServiceImpl.class.getName())) + .thenReturn(dirtyFiles); + + // Invoke + final Collection poms = service.getPoms(); + + // Check + assertEquals(0, poms.size()); + } + + @Test + public void testGetPomsWhenOneEmptyPomIsDirty() throws Exception { + // Set up + final Collection dirtyFiles = Arrays + .asList(getCanonicalPath("empty/pom.xml")); + when( + mockFileMonitorService + .getDirtyFiles(PomManagementServiceImpl.class.getName())) + .thenReturn(dirtyFiles); + + // Invoke + final Collection poms = service.getPoms(); + + // Check + assertEquals(0, poms.size()); + } + + @Test + public void testGetPomsWhenOneNonExistantPomIsDirty() { + // Set up + final Collection dirtyFiles = Arrays + .asList("/users/jbloggs/clinic/pom.xml"); + when( + mockFileMonitorService + .getDirtyFiles(PomManagementServiceImpl.class.getName())) + .thenReturn(dirtyFiles); + + // Invoke + final Collection poms = service.getPoms(); + + // Check + assertEquals(0, poms.size()); + } + + private void verifyProjectMetadataNotification(final String... moduleNames) { + for (final String moduleName : moduleNames) { + final String projectMetadataId = ProjectMetadata + .getProjectIdentifier(moduleName); + verify(mockMetadataService).evictAndGet(projectMetadataId); + verify(mockMetadataDependencyRegistry).notifyDownstream( + projectMetadataId); + } + } +} diff --git a/project/src/test/java/org/springframework/roo/project/ProjectMetadataTest.java b/project/src/test/java/org/springframework/roo/project/ProjectMetadataTest.java new file mode 100644 index 000000000..f260e53e0 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/ProjectMetadataTest.java @@ -0,0 +1,93 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.ProjectMetadata.MODULE_SEPARATOR; +import static org.springframework.roo.project.ProjectMetadata.PROJECT_MID_PREFIX; + +import org.junit.Test; +import org.springframework.roo.project.maven.Pom; + +/** + * Unit test of {@link ProjectMetadata} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ProjectMetadataTest { + + private static final String LEVEL_ONE_MODULE = "core"; + private static final String LEVEL_ONE_MID = PROJECT_MID_PREFIX + + MODULE_SEPARATOR + LEVEL_ONE_MODULE; + private static final String LEVEL_TWO_MODULE = LEVEL_ONE_MODULE + + MODULE_SEPARATOR + "sub"; + private static final String LEVEL_TWO_MID = PROJECT_MID_PREFIX + + MODULE_SEPARATOR + LEVEL_TWO_MODULE; + private static final String ROOT_MID = PROJECT_MID_PREFIX; + + @Test + public void testConstructorForLevelTwoModule() { + // Set up + final Pom mockPom = mock(Pom.class); + when(mockPom.getModuleName()).thenReturn(LEVEL_TWO_MODULE); + + // Invoke + final ProjectMetadata projectMetadata = new ProjectMetadata(mockPom); + + // Check + assertEquals(mockPom, projectMetadata.getPom()); + assertEquals(LEVEL_TWO_MODULE, projectMetadata.getModuleName()); + assertEquals(LEVEL_TWO_MID, projectMetadata.getId()); + } + + @Test + public void testGetModuleNameFromLevelOneModuleMID() { + assertEquals(LEVEL_ONE_MODULE, + ProjectMetadata.getModuleName(LEVEL_ONE_MID)); + } + + @Test + public void testGetModuleNameFromRootModuleMID() { + assertEquals("", ProjectMetadata.getModuleName(ROOT_MID)); + } + + @Test + public void testGetProjectIdentifierForLevelOneModule() { + assertEquals(LEVEL_ONE_MID, + ProjectMetadata.getProjectIdentifier(LEVEL_ONE_MODULE)); + } + + @Test + public void testGetProjectIdentifierForLevelTwoModule() { + assertEquals(LEVEL_TWO_MID, + ProjectMetadata.getProjectIdentifier(LEVEL_TWO_MODULE)); + } + + @Test + public void testGetProjectIdentifierForRootModule() { + assertEquals(ROOT_MID, ProjectMetadata.getProjectIdentifier("")); + } + + @Test + public void testInvalidMIDIsNotValid() { + assertFalse(ProjectMetadata.isValid("MID:foo#bar?baz")); + } + + @Test + public void testLevelOneMIDIsValid() { + assertTrue(ProjectMetadata.isValid(LEVEL_ONE_MID)); + } + + @Test + public void testLevelTwoMIDIsValid() { + assertTrue(ProjectMetadata.isValid(LEVEL_TWO_MID)); + } + + @Test + public void testRootMIDIsValid() { + assertTrue(ProjectMetadata.isValid(ROOT_MID)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/RepositoryTest.java b/project/src/test/java/org/springframework/roo/project/RepositoryTest.java new file mode 100644 index 000000000..c5cec4b53 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/RepositoryTest.java @@ -0,0 +1,48 @@ +package org.springframework.roo.project; + +import org.apache.commons.lang3.SystemUtils; +import org.junit.Test; +import org.w3c.dom.Element; +import static org.junit.Assume.assumeTrue; + +/** + * Unit test of the {@link Repository} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class RepositoryTest extends XmlTestCase { + + private static final boolean ENABLE_SNAPSHOTS = true; + private static final String EXPECTED_XML = "\n" + + "\n" + + " the-id\n" + + " the-url\n" + + " the_name\n" + + " \n" + + " true\n" + + " \n" + ""; + private static final String ID = "the-id"; + private static final String NAME = "the_name"; + private static final String PATH = "pluginRepo"; + + private static final String URL = "the-url"; + + public void setUp() { + assumeTrue(SystemUtils.IS_JAVA_1_6); + } + + @Test + public void testGetElement() { + // Set up + final Repository repository = new Repository(ID, NAME, URL, + ENABLE_SNAPSHOTS); + + // Invoke + final Element element = repository.getElement( + DOCUMENT_BUILDER.newDocument(), PATH); + + // Check + assertXmlEquals(EXPECTED_XML, element); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/ResourceTest.java b/project/src/test/java/org/springframework/roo/project/ResourceTest.java new file mode 100644 index 000000000..817dc0ba5 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/ResourceTest.java @@ -0,0 +1,51 @@ +package org.springframework.roo.project; + +import java.util.Arrays; + +import org.junit.Test; +import org.w3c.dom.Element; + +/** + * Unit test of the {@link Resource} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ResourceTest extends XmlTestCase { + + private static final String DIRECTORY = "anything"; + private static final boolean FILTERING = true; + private static final String INCLUDE_1 = "include1"; + private static final String INCLUDE_2 = "include2"; + private static final String EXPECTED_XML = "\n" + + "\n" + + " " + + DIRECTORY + + "\n" + + " " + + FILTERING + + "\n" + + " \n" + + " " + + INCLUDE_1 + + "\n" + + " " + + INCLUDE_2 + + "\n" + + " \n" + ""; + + @Test + public void testGetElement() { + // Set up + + final Resource resource = new Resource(DIRECTORY, FILTERING, + Arrays.asList(INCLUDE_1, INCLUDE_2)); + + // Invoke + final Element element = resource.getElement(DOCUMENT_BUILDER + .newDocument()); + + // Check + assertXmlEquals(EXPECTED_XML, element); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/XmlTestCase.java b/project/src/test/java/org/springframework/roo/project/XmlTestCase.java new file mode 100644 index 000000000..133036caa --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/XmlTestCase.java @@ -0,0 +1,58 @@ +package org.springframework.roo.project; + +import static org.junit.Assert.assertEquals; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.Node; + +/** + * Convenient superclass for XML-based JUnit 4 test cases. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public abstract class XmlTestCase { + + /** + * A builder for XML DOM documents. + */ + protected static final DocumentBuilder DOCUMENT_BUILDER; + + static { + try { + DOCUMENT_BUILDER = DocumentBuilderFactory.newInstance() + .newDocumentBuilder(); + } + catch (final ParserConfigurationException e) { + throw new IllegalStateException(e); + } + } + + /** + * Asserts that the given XML node contains the expected content + * + * @param expectedLines the expected lines of XML (required); separate each + * line with "\n" regardless of the platform + * @param actualNode the actual XML node (required) + * @throws AssertionError if they are not equal + */ + protected final void assertXmlEquals(final String expectedXml, + final Node actualNode) { + // Replace the dummy line terminator with the platform-specific one that + // will be applied by XmlUtils.nodeToString. + final String normalisedXml = expectedXml.replace("\n", + IOUtils.LINE_SEPARATOR); + // Trim trailing whitespace as XmlUtils.nodeToString appends an extra + // newline. + final String actualXml = StringUtils.stripEnd( + XmlUtils.nodeToString(actualNode), null); + assertEquals(StringUtils.replace(normalisedXml, "\n", ""), + StringUtils.replace(actualXml, "\n", "")); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/converter/GAVConverterTest.java b/project/src/test/java/org/springframework/roo/project/converter/GAVConverterTest.java new file mode 100644 index 000000000..1bcfdd50a --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/converter/GAVConverterTest.java @@ -0,0 +1,138 @@ +package org.springframework.roo.project.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.springframework.roo.project.GAV; +import org.springframework.roo.project.MavenUtils; +import org.springframework.roo.shell.Completion; + +/** + * Unit test of {@link GAVConverter} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class GAVConverterTest { + + // Fixture + private GAVConverter converter; + + private void assertInvalidString(final String string, + final String expectedMessage) { + try { + converter.convertFromText(string, GAV.class, null); + fail("Expected a " + IllegalArgumentException.class); + } + catch (final Exception e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + /** + * Asserts the expected completions for the given input string + * + * @param existingData + * @param expectedComplete whether we expect the converter to report the + * conversion as complete + * @param expectedCompletions + */ + private void assertPossibleValues(final String existingData, + final boolean expectedComplete, + final Completion... expectedCompletions) { + // Set up + final List completions = new ArrayList(); + + // Invoke + final boolean complete = converter.getAllPossibleValues(completions, + null, existingData, null, null); + + // Check + assertEquals(expectedComplete, complete); + assertEquals(Arrays.asList(expectedCompletions), completions); + } + + @Before + public void setUp() { + converter = new GAVConverter(); + } + + @Test + public void testConvertFromEmptyString() { + assertInvalidString("", + "Expected three coordinates, but found 0: []; did you use the ':' separator?"); + } + + @Test + public void testConvertFromNull() { + assertInvalidString(null, + "Expected three coordinates, but found 0: []; did you use the ':' separator?"); + } + + @Test + public void testConvertFromOneTooFewCoordinates() { + assertInvalidString( + "foo:bar", + "Expected three coordinates, but found 2: [foo, bar]; did you use the ':' separator?"); + } + + @Test + public void testConvertFromOneTooManyCoordinates() { + assertInvalidString( + "foo:bar:baz:bop", + "Expected three coordinates, but found 4: [foo, bar, baz, bop]; did you use the ':' separator?"); + } + + @Test + public void testConvertFromValidCoordinates() { + // Set up + final String groupId = "org.springframework.roo"; + final String artifactId = "addon-gradle"; + final String version = "-0.1"; + final String coordinates = StringUtils.join( + Arrays.asList(groupId, artifactId, version), + MavenUtils.COORDINATE_SEPARATOR); + + // Invoke + final GAV gav = converter.convertFromText(coordinates, GAV.class, null); + + // Check + assertEquals(groupId, gav.getGroupId()); + assertEquals(artifactId, gav.getArtifactId()); + assertEquals(version, gav.getVersion()); + } + + @Test + public void testDoesNotSupportObjects() { + assertFalse(converter.supports(Object.class, null)); + } + + @Test + public void testGetAllPossibleValuesForNullInput() { + assertPossibleValues(null, true); + } + + @Test + public void testSupportsGAVs() { + assertTrue(converter.supports(GAV.class, null)); + } + + @Test + public void testSupportsSubclassOfGAV() { + // Set up + final Class subclass = new GAV("a", "b", "c") { + }.getClass(); + + // Invoke and check + assertTrue(converter.supports(subclass, null)); + } +} \ No newline at end of file diff --git a/project/src/test/java/org/springframework/roo/project/converter/PomConverterTest.java b/project/src/test/java/org/springframework/roo/project/converter/PomConverterTest.java new file mode 100644 index 000000000..d35f2a32a --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/converter/PomConverterTest.java @@ -0,0 +1,147 @@ +package org.springframework.roo.project.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.converter.PomConverter.INCLUDE_CURRENT_MODULE; +import static org.springframework.roo.project.converter.PomConverter.ROOT_MODULE_SYMBOL; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.project.ProjectOperations; +import org.springframework.roo.project.maven.Pom; +import org.springframework.roo.shell.Completion; + +/** + * Unit test of {@link PomConverter} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PomConverterTest { + + private static final String CHILD_MODULE = "child"; + private static final String ROOT_MODULE = ""; + + // Fixture + private PomConverter converter; + @Mock private ProjectOperations mockProjectOperations; + + private void assertCompletions(final String optionContext, + final String focusedModuleName, + final Collection moduleNames, + final String... expectedCompletions) { + // Set up + when(mockProjectOperations.getFocusedModuleName()).thenReturn( + focusedModuleName); + when(mockProjectOperations.getModuleNames()).thenReturn(moduleNames); + final List completions = new ArrayList(); + + // Invoke + final boolean allValuesComplete = converter.getAllPossibleValues( + completions, null, null, optionContext, null); + + // Check + assertTrue(allValuesComplete); + assertEquals("Expected " + Arrays.toString(expectedCompletions) + + " but was " + completions, expectedCompletions.length, + completions.size()); + for (int i = 0; i < expectedCompletions.length; i++) { + assertEquals(expectedCompletions[i], completions.get(i).getValue()); + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + converter = new PomConverter(); + converter.projectOperations = mockProjectOperations; + } + + @Test + public void testConvertOtherModuleName() { + final Pom mockPom = mock(Pom.class); + final String moduleName = "foo" + File.separator + "bar"; + when(mockProjectOperations.getPomFromModuleName(moduleName)) + .thenReturn(mockPom); + assertEquals(mockPom, converter.convertFromText(moduleName, null, null)); + } + + @Test + public void testConvertRootModuleSymbol() { + final Pom mockRootPom = mock(Pom.class); + when(mockProjectOperations.getPomFromModuleName("")).thenReturn( + mockRootPom); + assertEquals(mockRootPom, + converter.convertFromText(ROOT_MODULE_SYMBOL, null, null)); + } + + @Test + public void testDoesNotSupportOtherTypes() { + assertFalse(converter.supports(Object.class, null)); + } + + @Test + public void testGetCompletionsExcludingCurrentWhenChildModuleExistsAndChildIsFocused() { + assertCompletions(null, CHILD_MODULE, + Arrays.asList(ROOT_MODULE, CHILD_MODULE), ROOT_MODULE_SYMBOL); + } + + @Test + public void testGetCompletionsExcludingCurrentWhenChildModuleExistsAndRootIsFocused() { + assertCompletions(null, ROOT_MODULE, + Arrays.asList(ROOT_MODULE, CHILD_MODULE), CHILD_MODULE); + } + + @Test + public void testGetCompletionsExcludingCurrentWhenNoModulesExist() { + assertCompletions(null, ROOT_MODULE, Collections. emptyList()); + } + + @Test + public void testGetCompletionsExcludingCurrentWhenOnlyRootModuleExists() { + assertCompletions(null, ROOT_MODULE, Arrays.asList(ROOT_MODULE)); + } + + @Test + public void testGetCompletionsIncludingCurrentWhenChildModuleExistsAndChildIsFocused() { + assertCompletions(INCLUDE_CURRENT_MODULE, CHILD_MODULE, + Arrays.asList(ROOT_MODULE, CHILD_MODULE), ROOT_MODULE_SYMBOL, + CHILD_MODULE); + } + + @Test + public void testGetCompletionsIncludingCurrentWhenChildModuleExistsAndRootIsFocused() { + assertCompletions(INCLUDE_CURRENT_MODULE, ROOT_MODULE, + Arrays.asList(ROOT_MODULE, CHILD_MODULE), ROOT_MODULE_SYMBOL, + CHILD_MODULE); + } + + @Test + public void testGetCompletionsIncludingCurrentWhenNoModulesExist() { + assertCompletions(INCLUDE_CURRENT_MODULE, ROOT_MODULE, + Collections. emptyList()); + } + + @Test + public void testGetCompletionsIncludingCurrentWhenOnlyRootModuleExists() { + assertCompletions(INCLUDE_CURRENT_MODULE, ROOT_MODULE, + Arrays.asList(ROOT_MODULE), ROOT_MODULE_SYMBOL); + } + + @Test + public void testSupportsPoms() { + assertTrue(converter.supports(Pom.class, null)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/maven/ModuleTest.java b/project/src/test/java/org/springframework/roo/project/maven/ModuleTest.java new file mode 100644 index 000000000..59db1358a --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/maven/ModuleTest.java @@ -0,0 +1,37 @@ +package org.springframework.roo.project.maven; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Unit test of the {@link Module} class. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ModuleTest { + + private static final String VALID_NAME = "web"; + private static final String VALID_PATH = "/path/to/pom.xml"; + + @Test(expected = IllegalArgumentException.class) + public void testConstructWithEmptyName() { + new Module("", VALID_PATH); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructWithEmptyPath() { + new Module(VALID_NAME, ""); + } + + @Test + public void testConstructWithValidArguments() { + // Invoke + final Module module = new Module(VALID_NAME, VALID_PATH); + + // Check + assertEquals(VALID_NAME, module.getName()); + assertEquals(VALID_PATH, module.getPomPath()); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/maven/PomFactoryImplTest.java b/project/src/test/java/org/springframework/roo/project/maven/PomFactoryImplTest.java new file mode 100644 index 000000000..73e676e3b --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/maven/PomFactoryImplTest.java @@ -0,0 +1,182 @@ +package org.springframework.roo.project.maven; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.Path.SRC_MAIN_JAVA; +import static org.springframework.roo.project.Path.SRC_TEST_JAVA; +import static org.springframework.roo.project.maven.Pom.DEFAULT_PACKAGING; + +import java.io.File; +import java.net.URL; +import java.util.Collection; +import java.util.Iterator; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.packaging.PackagingProvider; +import org.springframework.roo.project.packaging.PackagingProviderRegistry; +import org.springframework.uaa.client.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Unit test of {@link PomFactoryImpl} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PomFactoryImplTest { + + private static final String MODULE_NAME = "my-module"; + + // Fixture + private PomFactoryImpl factory; + @Mock private PackagingProviderRegistry mockPackagingProviderRegistry; + + private void assertGav(final Pom pom, final String expectedGroupId, + final String expectedArtifactId, final String expectedVersion) { + assertEquals(expectedGroupId, pom.getGroupId()); + assertEquals(expectedArtifactId, pom.getArtifactId()); + assertEquals(expectedVersion, pom.getVersion()); + } + + private void assertModule(final Module module, final String expectedName, + final String pomFileName) throws Exception { + assertEquals(expectedName, module.getName()); + final File parentPomDirectory = getPomFile(pomFileName).getParentFile(); + final File moduleDirectory = new File(parentPomDirectory, expectedName); + final File modulePom = new File(moduleDirectory, "pom.xml"); + assertEquals(modulePom.getCanonicalPath(), module.getPomPath()); + } + + /** + * Returns the root element and canonical path of the given POM file + * + * @param pomFileName the name of a POM in this test's package + * @return a non-null pair + * @throws Exception + */ + private ImmutablePair getPom(final String pomFileName) + throws Exception { + final URL pomUrl = getPomUrl(pomFileName); + final File pomFile = new File(pomUrl.toURI()); + final Document pomDocument = XmlUtils.parse(pomUrl.openStream()); + return new ImmutablePair( + pomDocument.getDocumentElement(), pomFile.getCanonicalPath()); + } + + private File getPomFile(final String pomFileName) throws Exception { + final URL pomUrl = getPomUrl(pomFileName); + return new File(pomUrl.toURI()); + } + + /** + * Returns the URL of the given POM file + * + * @param pomFileName the name of a POM in this test's package + * @return a non-null URL + * @throws Exception + */ + private URL getPomUrl(final String pomFileName) throws Exception { + final URL pomUrl = getClass().getResource(pomFileName); + assertNotNull("Can't find test POM '" + pomFileName + + "' on classpath of " + getClass().getName(), pomUrl); + return pomUrl; + } + + private Pom invokeFactory(final String pomFile) throws Exception { + final ImmutablePair pomDetails = getPom(pomFile); + return factory.getInstance(pomDetails.getKey(), pomDetails.getValue(), + MODULE_NAME); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + factory = new PomFactoryImpl(); + factory.packagingProviderRegistry = mockPackagingProviderRegistry; + } + + private void setUpMockPackagingProvider(final String providerId) { + final PackagingProvider mockPackagingProvider = mock(PackagingProvider.class); + when(mockPackagingProviderRegistry.getPackagingProvider(providerId)) + .thenReturn(mockPackagingProvider); + } + + @Test + public void testGetInstanceWithDependency() throws Exception { + // Set up + setUpMockPackagingProvider(DEFAULT_PACKAGING); + + // Invoke + final Pom pom = invokeFactory("pom-with-dependencies.xml"); + + // Check + assertGav(pom, "com.example", "dependent-app", "2.1"); + final Collection dependencies = pom.getDependencies(); + assertEquals(1, dependencies.size()); + final Dependency dependency = dependencies.iterator().next(); + assertEquals("org.apache", dependency.getGroupId()); + assertEquals("commons-lang", dependency.getArtifactId()); + assertEquals("2.5", dependency.getVersion()); + } + + @Test + public void testGetInstanceWithInheritedGroupId() throws Exception { + // Set up + setUpMockPackagingProvider(DEFAULT_PACKAGING); + + // Invoke + final Pom pom = invokeFactory("inherited-groupId-pom.xml"); + + // Check + assertGav(pom, "com.example", "child-app", "2.0"); + assertEquals("prod-sources", pom.getSourceDirectory()); + assertEquals("test-sources", pom.getTestSourceDirectory()); + } + + @Test + public void testGetInstanceWithPomPackaging() throws Exception { + // Set up + setUpMockPackagingProvider("pom"); + final String pomFileName = "parent-pom.xml"; + + // Invoke + final Pom pom = invokeFactory(pomFileName); + + // Check + assertGav(pom, "com.example", "parent-app", "3.0"); + assertEquals("pom", pom.getPackaging()); + assertEquals(SRC_MAIN_JAVA.getDefaultLocation(), + pom.getSourceDirectory()); + assertEquals(SRC_TEST_JAVA.getDefaultLocation(), + pom.getTestSourceDirectory()); + final Collection modules = pom.getModules(); + assertEquals(2, modules.size()); + final Iterator moduleIterator = modules.iterator(); + assertModule(moduleIterator.next(), "module-one", pomFileName); + assertModule(moduleIterator.next(), "module-two", pomFileName); + } + + @Test + public void testGetMinimalInstance() throws Exception { + // Set up + setUpMockPackagingProvider(DEFAULT_PACKAGING); + + // Invoke + final Pom pom = invokeFactory("minimal-pom.xml"); + + // Check + assertGav(pom, "com.example", "minimal-app", "2.0"); + assertEquals(SRC_MAIN_JAVA.getDefaultLocation(), + pom.getSourceDirectory()); + assertEquals(SRC_TEST_JAVA.getDefaultLocation(), + pom.getTestSourceDirectory()); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/maven/PomTest.java b/project/src/test/java/org/springframework/roo/project/maven/PomTest.java new file mode 100644 index 000000000..77d5e2e24 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/maven/PomTest.java @@ -0,0 +1,213 @@ +package org.springframework.roo.project.maven; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.project.DependencyScope.COMPILE; +import static org.springframework.roo.project.Path.ROOT; +import static org.springframework.roo.project.maven.Pom.DEFAULT_PACKAGING; + +import java.io.File; +import java.util.Arrays; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.springframework.roo.project.Dependency; +import org.springframework.roo.project.DependencyType; +import org.springframework.roo.project.LogicalPath; +import org.springframework.roo.project.Path; +import org.springframework.roo.project.PhysicalPath; +import org.springframework.roo.support.util.FileUtils; + +/** + * Unit test of the {@link Pom} class + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PomTest { + + private static final String ARTIFACT_ID = "my-app"; + private static final String DEPENDENCY_ARTIFACT_ID = "commons-foo"; + private static final String DEPENDENCY_GROUP_ID = "org.apache"; + private static final String GROUP_ID = "com.example"; + private static final String JAR = "jar"; + private static final String POM = "pom"; + private static final String PROJECT_ROOT = File.separator + + FileUtils.getSystemDependentPath("users", "jbloggs", "projects", + "clinic"); + private static final String ROOT_MODULE = ""; + private static final String VERSION = "1.0.1.RELEASE"; + private static final String WAR = "war"; + + private Pom getMinimalPom(final String packaging, + final Dependency... dependencies) { + return new Pom(GROUP_ID, ARTIFACT_ID, VERSION, packaging, + Arrays.asList(dependencies), null, null, null, null, null, + null, null, null, null, null, null, PROJECT_ROOT + + File.separator + "pom.xml", ROOT_MODULE, null); + } + + private Dependency getMockDependency(final String groupId, + final String artifactId, final String version, + final DependencyType type) { + final Dependency mockDependency = mock(Dependency.class); + when(mockDependency.getGroupId()).thenReturn(groupId); + when(mockDependency.getArtifactId()).thenReturn(artifactId); + when(mockDependency.getVersion()).thenReturn(version); + when(mockDependency.getType()).thenReturn(type); + return mockDependency; + } + + @Test + public void testCanAddNewDependencyOfLowerType() { + // Set up + final Dependency mockNewDependency = mock(Dependency.class); + when(mockNewDependency.getType()).thenReturn(DependencyType.JAR); + final Pom pom = getMinimalPom(WAR); + + // Invoke and check + assertTrue(pom.canAddDependency(mockNewDependency)); + } + + @Test + public void testCanAddNewDependencyWhenOwnTypeIsNonStandard() { + // Set up + final Dependency mockNewDependency = mock(Dependency.class); + when(mockNewDependency.getType()).thenReturn(DependencyType.WAR); + final Pom pom = getMinimalPom("custom"); + + // Invoke and check + assertTrue(pom.canAddDependency(mockNewDependency)); + } + + @Test + public void testCannotAddAlreadyRegisteredDependency() { + // Set up + final Dependency mockExistingDependency = mock(Dependency.class); + when(mockExistingDependency.getType()).thenReturn(DependencyType.JAR); + final Pom pom = getMinimalPom(POM, mockExistingDependency); + + // Invoke and check + assertFalse(pom.canAddDependency(mockExistingDependency)); + } + + @Test + public void testCannotAddNewDependencyOfHigherType() { + // Set up + final Dependency mockNewDependency = mock(Dependency.class); + when(mockNewDependency.getType()).thenReturn(DependencyType.WAR); + final Pom pom = getMinimalPom(JAR); + + // Invoke and check + assertFalse(pom.canAddDependency(mockNewDependency)); + } + + @Test + public void testCannotAddNullDependency() { + // Set up + final Pom pom = getMinimalPom(POM); + + // Invoke and check + assertFalse(pom.canAddDependency(null)); + } + + @Test + public void testDefaultPackaging() { + assertEquals(DEFAULT_PACKAGING, getMinimalPom(DEFAULT_PACKAGING) + .getPackaging()); + } + + @Test + public void testGetAsDependency() { + // Set up + final Pom pom = getMinimalPom(WAR); + + // Invoke + final Dependency dependency = pom.asDependency(COMPILE); + + // Check + assertEquals(GROUP_ID, dependency.getGroupId()); + assertEquals(ARTIFACT_ID, dependency.getArtifactId()); + assertEquals(VERSION, dependency.getVersion()); + assertEquals(DependencyType.WAR, dependency.getType()); + assertEquals(COMPILE, dependency.getScope()); + assertTrue(StringUtils.isBlank(dependency.getClassifier())); + } + + @Test + public void testGetModulePathsForMinimalJarPom() { + // Set up + final Pom pom = getMinimalPom(DEFAULT_PACKAGING); + final Path[] expectedPaths = { ROOT }; + + // Invoke and check + assertEquals(expectedPaths.length, pom.getPhysicalPaths().size()); + for (final Path path : expectedPaths) { + final PhysicalPath modulePath = pom.getPhysicalPath(path); + assertEquals(new File(PROJECT_ROOT, path.getDefaultLocation()), + modulePath.getLocation()); + assertEquals(path.isJavaSource(), modulePath.isSource()); + final LogicalPath moduelPathId = modulePath.getLogicalPath(); + assertEquals(path, moduelPathId.getPath()); + assertEquals(ROOT_MODULE, moduelPathId.getModule()); + } + } + + @Test + public void testHasDependencyExcludingVersionWhenDependencyHasDifferentGroupId() { + // Set up + final Dependency mockExistingDependency = getMockDependency( + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, "1.0", + DependencyType.JAR); + final Pom pom = getMinimalPom(JAR, mockExistingDependency); + final Dependency mockOtherDependency = getMockDependency("au." + + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, "1.0", + DependencyType.JAR); + + // Invoke and check + assertFalse(pom.hasDependencyExcludingVersion(mockOtherDependency)); + } + + @Test + public void testHasDependencyExcludingVersionWhenDependencyHasDifferentType() { + // Set up + final Dependency mockExistingDependency = getMockDependency( + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, "1.0", + DependencyType.JAR); + final Pom pom = getMinimalPom(JAR, mockExistingDependency); + final Dependency mockOtherDependency = getMockDependency( + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, "1.0", + DependencyType.OTHER); + + // Invoke and check + assertFalse(pom.hasDependencyExcludingVersion(mockOtherDependency)); + } + + @Test + public void testHasDependencyExcludingVersionWhenDependencyHasDifferentVersion() { + // Set up + final String existingVersion = "1.0"; + final Dependency mockExistingDependency = getMockDependency( + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, existingVersion, + DependencyType.JAR); + final Pom pom = getMinimalPom(JAR, mockExistingDependency); + final Dependency mockOtherDependency = getMockDependency( + DEPENDENCY_GROUP_ID, DEPENDENCY_ARTIFACT_ID, existingVersion + + ".1", DependencyType.JAR); + + // Invoke and check + assertTrue(pom.hasDependencyExcludingVersion(mockOtherDependency)); + } + + @Test + public void testHasDependencyExcludingVersionWhenDependencyIsNull() { + // Set up + final Pom pom = getMinimalPom(JAR); + + // Invoke and check + assertFalse(pom.hasDependencyExcludingVersion(null)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/packaging/JarPackagingTest.java b/project/src/test/java/org/springframework/roo/project/packaging/JarPackagingTest.java new file mode 100644 index 000000000..17cdf3c46 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/packaging/JarPackagingTest.java @@ -0,0 +1,15 @@ +package org.springframework.roo.project.packaging; + +/** + * Unit test of the {@link JarPackaging} {@link PackagingProvider} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class JarPackagingTest extends PackagingProviderTestCase { + + @Override + protected JarPackaging getProvider() { + return new JarPackaging(); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderConverterTest.java b/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderConverterTest.java new file mode 100644 index 000000000..0b75399e8 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderConverterTest.java @@ -0,0 +1,145 @@ +package org.springframework.roo.project.packaging; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.roo.model.JavaType; +import org.springframework.roo.shell.Completion; + +/** + * Unit test of {@link PackagingProviderConverter} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PackagingProviderConverterTest { + + private static final String CORE_JAR_ID = "jar"; + private static final String CORE_WAR_ID = "war"; + private static final String CUSTOM_JAR_ID = "jar_custom"; + + // Fixture + private PackagingProviderConverter converter; + @Mock private CorePackagingProvider mockCoreJarPackaging; + @Mock private PackagingProvider mockCustomJarPackaging; + @Mock private PackagingProviderRegistry mockPackagingProviderRegistry; + @Mock private CorePackagingProvider mockWarPackaging; + + /** + * Asserts that the given string can't be converted to a + * {@link PackagingProvider} + * + * @param string the string to convert (can be blank) + */ + private void assertInvalidString(final String string) { + try { + converter.convertFromText(string, PackagingProvider.class, null); + fail("Expected a " + NullPointerException.class); + } + catch (final NullPointerException expected) { + assertEquals("Unsupported packaging id '" + string + "'", + expected.getMessage()); + } + } + + @Before + public void setUp() { + // Mocks + MockitoAnnotations.initMocks(this); + setUpMockPackagingProvider(mockCoreJarPackaging, CORE_JAR_ID, true); + setUpMockPackagingProvider(mockCustomJarPackaging, CUSTOM_JAR_ID, true); + setUpMockPackagingProvider(mockWarPackaging, CORE_WAR_ID, false); + + // Object under test + converter = new PackagingProviderConverter(); + converter.packagingProviderRegistry = mockPackagingProviderRegistry; + } + + private void setUpMockPackagingProvider( + final PackagingProvider mockPackagingProvider, final String id, + final boolean isDefault) { + when(mockPackagingProvider.getId()).thenReturn(id); + when(mockPackagingProvider.isDefault()).thenReturn(isDefault); + } + + @Test + public void testConvertEmptyString() { + assertInvalidString(""); + } + + @Test + public void testConvertNullString() { + assertInvalidString(null); + } + + @Test + public void testConvertPartialString() { + assertInvalidString(CORE_WAR_ID.substring(0, 1)); + } + + @Test + public void testConvertUnknownString() { + assertInvalidString("ear"); + } + + @Test + public void testConvertValidString() { + // Set up + final String id = "some-id"; + when(mockPackagingProviderRegistry.getPackagingProvider(id)) + .thenReturn(mockCoreJarPackaging); + + // Invoke + final PackagingProvider packagingProvider = converter.convertFromText( + id, PackagingProvider.class, null); + + // Check + assertEquals(mockCoreJarPackaging, packagingProvider); + } + + @Test + public void testDoesNotSupportWrongType() { + assertFalse(converter.supports(JavaType.class, null)); + } + + @Test + public void testGetAllPossibleValues() { + // Set up + final PackagingProvider[] providers = { mockCoreJarPackaging, + mockCustomJarPackaging, mockWarPackaging }; + when(mockPackagingProviderRegistry.getAllPackagingProviders()) + .thenReturn(Arrays.asList(providers)); + final List expectedCompletions = new ArrayList(); + for (final PackagingProvider provider : providers) { + expectedCompletions.add(new Completion(provider.getId() + .toUpperCase())); + } + final List completions = new ArrayList(); + + // Invoke + final boolean addSpace = converter.getAllPossibleValues(completions, + PackagingProvider.class, "ignored", null, null); + + // Check + assertTrue(addSpace); + assertEquals(expectedCompletions.size(), completions.size()); + assertTrue("Expected " + expectedCompletions + " but was " + + completions, completions.containsAll(expectedCompletions)); + } + + @Test + public void testSupportsCorrectType() { + assertTrue(converter.supports(PackagingProvider.class, null)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderRegistryTest.java b/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderRegistryTest.java new file mode 100644 index 000000000..f6ebe8d61 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderRegistryTest.java @@ -0,0 +1,93 @@ +package org.springframework.roo.project.packaging; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test of {@link PackagingProviderRegistryImpl} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PackagingProviderRegistryTest { + + private static final String CORE_JAR_ID = "jar"; + private static final String CORE_WAR_ID = "war"; + private static final String CUSTOM_JAR_ID = "jar_custom"; + + @Mock private CorePackagingProvider mockCoreJarPackaging; + @Mock private PackagingProvider mockCustomJarPackaging; + @Mock private CorePackagingProvider mockWarPackaging; + // Fixture + private PackagingProviderRegistryImpl registry; + + @Before + public void setUp() { + // Mocks + MockitoAnnotations.initMocks(this); + setUpMockPackagingProvider(mockCoreJarPackaging, CORE_JAR_ID, true); + setUpMockPackagingProvider(mockCustomJarPackaging, CUSTOM_JAR_ID, true); + setUpMockPackagingProvider(mockWarPackaging, CORE_WAR_ID, false); + + // Object under test + registry = new PackagingProviderRegistryImpl(); + registry.bindPackagingProvider(mockCoreJarPackaging); + registry.bindPackagingProvider(mockCustomJarPackaging); + registry.bindPackagingProvider(mockWarPackaging); + } + + private void setUpMockPackagingProvider( + final PackagingProvider mockPackagingProvider, final String id, + final boolean isDefault) { + when(mockPackagingProvider.getId()).thenReturn(id); + when(mockPackagingProvider.isDefault()).thenReturn(isDefault); + } + + @Test + public void testGetAllPackagingProviders() { + // Invoke + final Collection packagingProviders = registry + .getAllPackagingProviders(); + + // Check + final List expectedProviders = Arrays.asList( + mockCoreJarPackaging, mockCustomJarPackaging, mockWarPackaging); + assertEquals(expectedProviders.size(), packagingProviders.size()); + assertTrue(packagingProviders.containsAll(expectedProviders)); + } + + @Test + public void testGetDefaultPackagingProviderWhenACustomIsDefault() { + assertEquals(mockCustomJarPackaging, + registry.getDefaultPackagingProvider()); + } + + @Test + public void testGetDefaultPackagingProviderWhenNoCustomIsDefault() { + when(mockCustomJarPackaging.isDefault()).thenReturn(false); + assertEquals(mockCoreJarPackaging, + registry.getDefaultPackagingProvider()); + } + + @Test + public void testGetPackagingProviderByInvalidId() { + assertNull(registry.getPackagingProvider("no-such-provider")); + } + + @Test + public void testGetPackagingProviderByValidId() { + assertEquals(mockCustomJarPackaging, + registry.getPackagingProvider(CUSTOM_JAR_ID)); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderTestCase.java b/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderTestCase.java new file mode 100644 index 000000000..47083daa1 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/packaging/PackagingProviderTestCase.java @@ -0,0 +1,53 @@ +package org.springframework.roo.project.packaging; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.net.URL; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +/** + * Convenient superclass for writing tests of concrete {@link PackagingProvider} + * implementations. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public abstract class PackagingProviderTestCase { + + // Fixture + private T provider; + + /** + * Subclasses must return an instance of the provider being tested + * + * @return a non-null instance + */ + protected abstract T getProvider(); + + @Before + public void setUp() throws Exception { + this.provider = getProvider(); + } + + @Test + public void testIdIsNotBlank() { + assertTrue(StringUtils.isNotBlank(provider.getId())); + } + + @Test + public void testTemplateExists() { + // Set up + final String pomTemplate = provider.getPomTemplate(); + + // Invoke + final URL pomTemplateUrl = provider.getClass().getResource(pomTemplate); + + // Check + assertNotNull("Can't find POM template '" + pomTemplate + "'", + pomTemplateUrl); + } +} diff --git a/project/src/test/java/org/springframework/roo/project/packaging/PomPackagingTest.java b/project/src/test/java/org/springframework/roo/project/packaging/PomPackagingTest.java new file mode 100644 index 000000000..7970525d6 --- /dev/null +++ b/project/src/test/java/org/springframework/roo/project/packaging/PomPackagingTest.java @@ -0,0 +1,15 @@ +package org.springframework.roo.project.packaging; + +/** + * Unit test of the {@link PomPackaging} {@link PackagingProvider} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PomPackagingTest extends PackagingProviderTestCase { + + @Override + protected PomPackaging getProvider() { + return new PomPackaging(); + } +} diff --git a/project/src/test/resources/org/springframework/roo/project/empty/pom.xml b/project/src/test/resources/org/springframework/roo/project/empty/pom.xml new file mode 100644 index 000000000..e69de29bb diff --git a/project/src/test/resources/org/springframework/roo/project/maven/inherited-groupId-pom.xml b/project/src/test/resources/org/springframework/roo/project/maven/inherited-groupId-pom.xml new file mode 100644 index 000000000..a8786f10d --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/maven/inherited-groupId-pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + com.example + parent-app + 1.0 + + child-app + 2.0 + + prod-sources + test-sources + + + \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/maven/minimal-pom.xml b/project/src/test/resources/org/springframework/roo/project/maven/minimal-pom.xml new file mode 100644 index 000000000..a803b5620 --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/maven/minimal-pom.xml @@ -0,0 +1,9 @@ + + + 4.0.0 + com.example + minimal-app + 2.0 + \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/maven/module-one/readme.txt b/project/src/test/resources/org/springframework/roo/project/maven/module-one/readme.txt new file mode 100644 index 000000000..ef484f867 --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/maven/module-one/readme.txt @@ -0,0 +1 @@ +This directory is required by PomFactoryImplTest. \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/maven/module-two/readme.txt b/project/src/test/resources/org/springframework/roo/project/maven/module-two/readme.txt new file mode 100644 index 000000000..ef484f867 --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/maven/module-two/readme.txt @@ -0,0 +1 @@ +This directory is required by PomFactoryImplTest. \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/maven/parent-pom.xml b/project/src/test/resources/org/springframework/roo/project/maven/parent-pom.xml new file mode 100644 index 000000000..b44c64b7c --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/maven/parent-pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + com.example + parent-app + 3.0 + pom + + module-one + module-two + + \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/maven/pom-with-dependencies.xml b/project/src/test/resources/org/springframework/roo/project/maven/pom-with-dependencies.xml new file mode 100644 index 000000000..89ae27de3 --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/maven/pom-with-dependencies.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + com.example + dependent-app + 2.1 + + + org.apache + commons-lang + 2.5 + + + \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/multi/foo-child/pom.xml b/project/src/test/resources/org/springframework/roo/project/multi/foo-child/pom.xml new file mode 100644 index 000000000..89111cb9f --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/multi/foo-child/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + + com.foo + foo-parent + 2.0.0.BUILD-SNAPSHOT + + foo-child + \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/multi/pom.xml b/project/src/test/resources/org/springframework/roo/project/multi/pom.xml new file mode 100644 index 000000000..9889c3aaf --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/multi/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + com.foo + foo-parent + 2.0.0.BUILD-SNAPSHOT + pom + + foo-child + + \ No newline at end of file diff --git a/project/src/test/resources/org/springframework/roo/project/single/pom.xml b/project/src/test/resources/org/springframework/roo/project/single/pom.xml new file mode 100644 index 000000000..5437e1277 --- /dev/null +++ b/project/src/test/resources/org/springframework/roo/project/single/pom.xml @@ -0,0 +1,10 @@ + + + 4.0.0 + + com.foo + bar-parent + 2.0.0.BUILD-SNAPSHOT + + bar-child + \ No newline at end of file diff --git a/rwc.sh b/rwc.sh new file mode 100755 index 000000000..319f91c52 --- /dev/null +++ b/rwc.sh @@ -0,0 +1,2 @@ +#/bin/shell +wc -l `find . -name \*.java -print` diff --git a/shell-jline-osgi/pom.xml b/shell-jline-osgi/pom.xml new file mode 100644 index 000000000..a9e8af966 --- /dev/null +++ b/shell-jline-osgi/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.shell.jline.osgi + bundle + Spring Roo - Shell - JLine (OSGi Launcher) + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.shell.jline + + + + net.sourceforge.jline + jline + + + org.fusesource.jansi + jansi + + + + + + org.apache.felix + maven-bundle-plugin + + + *,org.fusesource.jansi;version="[1.4.0,2.0.0)";resolution:=optional + + + + + + \ No newline at end of file diff --git a/shell-jline-osgi/src/main/java/org/springframework/roo/shell/jline/osgi/JLineShellComponent.java b/shell-jline-osgi/src/main/java/org/springframework/roo/shell/jline/osgi/JLineShellComponent.java new file mode 100644 index 000000000..2b44a4b21 --- /dev/null +++ b/shell-jline-osgi/src/main/java/org/springframework/roo/shell/jline/osgi/JLineShellComponent.java @@ -0,0 +1,217 @@ +package org.springframework.roo.shell.jline.osgi; + +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_CYAN; +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_GREEN; +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_MAGENTA; +import static org.springframework.roo.support.util.AnsiEscapeCode.FG_YELLOW; +import static org.springframework.roo.support.util.AnsiEscapeCode.REVERSE; +import static org.springframework.roo.support.util.AnsiEscapeCode.UNDERSCORE; +import static org.springframework.roo.support.util.AnsiEscapeCode.decorate; + +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.ExecutionStrategy; +import org.springframework.roo.shell.Parser; +import org.springframework.roo.shell.jline.JLineShell; +import org.springframework.roo.support.osgi.OSGiUtils; +import org.springframework.roo.url.stream.UrlInputStreamService; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * OSGi component launcher for {@link JLineShell}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class JLineShellComponent extends JLineShell { + + protected final static Logger LOGGER = HandlerUtils.getLogger(JLineShellComponent.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + @Reference ExecutionStrategy executionStrategy; + @Reference Parser parser; + private UrlInputStreamService urlInputStreamService; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + final Thread thread = new Thread(this, "Spring Roo JLine Shell"); + thread.start(); + } + + protected void deactivate(final ComponentContext context) { + this.context = null; + closeShell(); + } + + @Override + protected Collection findResources(final String path) { + // For an OSGi bundle search, we add the root prefix to the given path + return OSGiUtils.findEntriesByPath(context, + OSGiUtils.ROOT_PATH + path); + } + + @Override + protected ExecutionStrategy getExecutionStrategy() { + return executionStrategy; + } + + private String getLatestFavouriteTweet() { + // Access Twitter's REST API + final String string = sendGetRequest( + "http://api.twitter.com/1/favorites.json", + "id=SpringRoo&count=5"); + if (StringUtils.isBlank(string)) { + return null; + } + // Parse the returned JSON. This is a once off operation so we can used + // JSONValue.parse without penalty + final JSONArray object = (JSONArray) JSONValue.parse(string); + if (object == null) { + return null; + } + int index = 0; + if (object.size() > 4) { + index = new Random().nextInt(5); + } + final JSONObject jsonObject = (JSONObject) object.get(index); + if (jsonObject == null) { + return null; + } + final String screenName = (String) ((JSONObject) jsonObject.get("user")) + .get("screen_name"); + String tweet = (String) jsonObject.get("text"); + // We only want one line + tweet = tweet.replace(IOUtils.LINE_SEPARATOR, " "); + final List words = Arrays.asList(tweet.split(" ")); + final StringBuilder sb = new StringBuilder(); + // Add in Roo's twitter account to give context to the notification + sb.append(decorate("@" + screenName + ":", + SystemUtils.IS_OS_WINDOWS ? FG_YELLOW : REVERSE)); + sb.append(" "); + + // We want to colourise certain words. The codes used here should be + // moved to a ShellUtils and include a few helper methods + // This is a basic attempt at pattern identification, it should be + // adequate in most cases although may be incorrect for URLs. + // For example url.com/ttym: is valid by may mean "url.com/ttym" + ":" + for (final String word : words) { + if (word.startsWith("http://") || word.startsWith("https://")) { + // It's a URL + if (SystemUtils.IS_OS_WINDOWS) { + sb.append(decorate(word, FG_GREEN)); + } + else { + sb.append(decorate(word, FG_GREEN, UNDERSCORE)); + } + } + else if (word.charAt(0) == '@') { + // It's a Twitter username + sb.append(decorate(word, FG_MAGENTA)); + } + else if (word.charAt(0) == '#') { + // It's a Twitter hash tag + sb.append(decorate(word, FG_CYAN)); + } + else { + // All else default + sb.append(word); + } + // Add back separator + sb.append(" "); + } + return sb.toString(); + } + + @Override + protected Parser getParser() { + return parser; + } + + @Override + public String getStartupNotifications() { + try { + return getLatestFavouriteTweet(); + } + catch (final Exception e) { + return null; + } + } + + // TODO: This should probably be moved to a HTTP service of some sort - JTT + // 29/08/11 + private String sendGetRequest(final String endpoint, + final String requestParameters) { + + if(urlInputStreamService == null){ + urlInputStreamService = getUrlInputStreamService(); + } + + Validate.notNull(urlInputStreamService, "UrlInputStreamService is required"); + + if (!(endpoint.startsWith("http://") || endpoint.startsWith("https://"))) { + return null; + } + + // Send a GET request to the servlet + InputStream inputStream = null; + try { + // Send data + String urlStr = endpoint; + if (StringUtils.isNotBlank(requestParameters)) { + urlStr += "?" + requestParameters; + } + // Get the response + final URL url = new URL(urlStr); + inputStream = urlInputStreamService.openConnection(url); + return IOUtils.toString(inputStream); + } + catch (final Exception e) { + return null; + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + + public UrlInputStreamService getUrlInputStreamService(){ + // Get all Services implement UrlInputStreamService interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(UrlInputStreamService.class.getName(), null); + + for(ServiceReference ref : references){ + return (UrlInputStreamService) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load UrlInputStreamService on JLineShellComponent."); + return null; + } + } +} \ No newline at end of file diff --git a/shell-jline/legal-shell-jline.txt b/shell-jline/legal-shell-jline.txt new file mode 100644 index 000000000..f1cb449e3 --- /dev/null +++ b/shell-jline/legal-shell-jline.txt @@ -0,0 +1,40 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: JLine +Software Web Site: http://jline.sourceforge.net/ +Effective License: The BSD License +License Info Page: http://jline.sourceforge.net/license.html + +JLine is a runtime dependency of this module. JLine provides services +for handling console input. + +----------------------------------------------------------------------- + +Licensed Software: Jansi +Software Web Site: http://jansi.fusesource.org/ +Effective License: Apache License V2.0 +License Info Page: http://jansi.fusesource.org/maven/1.1/license.html + +Jansi is an optional dependency of this module. Jansi provides color +support in the shell when running on Windows. + +----------------------------------------------------------------------- + +Licensed Software: JNA +Software Web Site: https://jna.dev.java.net/ +Effective License: Lesser General Public License (LGPL) +License Info Page: No dedicated page, LGPL links provided on web site + +JNA is an optional dependency of this module. It's used by Jansi on +Windows to make native calls to control the command shell. + +----------------------------------------------------------------------- + + +[end] diff --git a/shell-jline/pom.xml b/shell-jline/pom.xml new file mode 100644 index 000000000..f1f31242a --- /dev/null +++ b/shell-jline/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.shell.jline + bundle + Spring Roo - Shell - JLine + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.url.stream.jdk + + + + net.sourceforge.jline + jline + + + org.fusesource.jansi + jansi + + + org.springframework.roo.wrapping + org.springframework.roo.wrapping.json-simple + + + + + + org.apache.felix + maven-bundle-plugin + + + *,org.fusesource.jansi;version="[1.4.0,2.0.0)";resolution:=optional + + + + + + \ No newline at end of file diff --git a/shell-jline/src/main/java/org/springframework/roo/shell/jline/EclipseTerminal.java b/shell-jline/src/main/java/org/springframework/roo/shell/jline/EclipseTerminal.java new file mode 100644 index 000000000..f67746334 --- /dev/null +++ b/shell-jline/src/main/java/org/springframework/roo/shell/jline/EclipseTerminal.java @@ -0,0 +1,11 @@ +package org.springframework.roo.shell.jline; + +import jline.UnsupportedTerminal; + +public class EclipseTerminal extends UnsupportedTerminal { + + @Override + public boolean isANSISupported() { + return false; + } +} diff --git a/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineCompletorAdapter.java b/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineCompletorAdapter.java new file mode 100644 index 000000000..e2da3d38f --- /dev/null +++ b/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineCompletorAdapter.java @@ -0,0 +1,48 @@ +package org.springframework.roo.shell.jline; + +import java.util.ArrayList; +import java.util.List; + +import jline.Completor; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Parser; + +/** + * An implementation of JLine's {@link Completor} interface that delegates to a + * {@link Parser}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JLineCompletorAdapter implements Completor { + + private final Parser parser; + + public JLineCompletorAdapter(final Parser parser) { + Validate.notNull(parser, "Parser required"); + this.parser = parser; + } + + @SuppressWarnings("all") + public int complete(final String buffer, final int cursor, + final List candidates) { + int result; + try { + JLineLogHandler.cancelRedrawProhibition(); + final List completions = new ArrayList(); + result = parser.completeAdvanced(buffer, cursor, completions); + for (final Completion completion : completions) { + candidates + .add(new jline.Completion(completion.getValue(), + completion.getFormattedValue(), completion + .getHeading())); + } + } + finally { + JLineLogHandler.prohibitRedraw(); + } + return result; + } +} diff --git a/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineLogHandler.java b/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineLogHandler.java new file mode 100644 index 000000000..5e28fdb7d --- /dev/null +++ b/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineLogHandler.java @@ -0,0 +1,264 @@ +package org.springframework.roo.shell.jline; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jline.ANSIBuffer; +import jline.ConsoleReader; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.shell.ShellPromptAccessor; +import org.springframework.roo.support.util.AnsiEscapeCode; + +/** + * JDK logging {@link Handler} that emits log messages to a JLine + * {@link ConsoleReader}. + * + * @author Ben Alex + * @since 1.0 + */ +public class JLineLogHandler extends Handler { + + private static final boolean BRIGHT_COLORS = Boolean + .getBoolean("roo.bright"); + private static boolean includeThreadName = false; + private static String lastMessage; + private static ThreadLocal redrawProhibit = new ThreadLocal(); + private static boolean suppressDuplicateMessages = true; + + public static void cancelRedrawProhibition() { + redrawProhibit.remove(); + } + + /** + * Makes text brighter if requested through system property 'roo.bright' and + * works around issue on Windows in using reverse() in combination with the + * Jansi lib, which leaves its 'negative' flag set unless reset explicitly. + * + * @return new patched ANSIBuffer + */ + static ANSIBuffer getANSIBuffer() { + final char esc = (char) 27; + return new ANSIBuffer() { + @Override + public ANSIBuffer attrib(final String str, final int code) { + if (BRIGHT_COLORS && 30 <= code && code <= 37) { + // This is a color code: add a 'bright' code + return append(esc + "[" + code + ";1m").append(str).append( + ANSICodes.attrib(0)); + } + return super.attrib(str, code); + }; + + @Override + public ANSIBuffer reverse(final String str) { + if (SystemUtils.IS_OS_WINDOWS) { + return super.reverse(str).append(ANSICodes.attrib(esc)); + } + return super.reverse(str); + } + }; + } + + public static boolean isSuppressDuplicateMessages() { + return suppressDuplicateMessages; + } + + public static void prohibitRedraw() { + redrawProhibit.set(true); + } + + public static void resetMessageTracking() { + lastMessage = null; // see ROO-251 + } + + public static void setIncludeThreadName(final boolean include) { + includeThreadName = include; + } + + public static void setSuppressDuplicateMessages( + final boolean suppressDuplicateMessages) { + JLineLogHandler.suppressDuplicateMessages = suppressDuplicateMessages; + } + + private boolean ansiSupported; + private ConsoleReader reader; + private ShellPromptAccessor shellPromptAccessor; + private String userInterfaceThreadName; + + public JLineLogHandler(final ConsoleReader reader, + final ShellPromptAccessor shellPromptAccessor) { + Validate.notNull(reader, "Console reader required"); + Validate.notNull(shellPromptAccessor, "Shell prompt accessor required"); + this.reader = reader; + this.shellPromptAccessor = shellPromptAccessor; + userInterfaceThreadName = Thread.currentThread().getName(); + ansiSupported = reader.getTerminal().isANSISupported() + && AnsiEscapeCode.isAnsiEnabled(); + + setFormatter(new Formatter() { + @Override + public String format(final LogRecord record) { + final StringBuilder sb = new StringBuilder(); + String message = record.getMessage(); + if (message != null) { + final Object[] parameters = record.getParameters(); + if (!ArrayUtils.isEmpty(parameters)) { + final Pattern pattern = Pattern.compile("\\{.*?\\}"); + final Matcher matcher = pattern.matcher(message); + int i = 0; + while (matcher.find()) { + message = StringUtils.replace(message, + matcher.group(0), parameters[i].toString()); + i++; + } + } + sb.append(message).append(LINE_SEPARATOR); + } + if (record.getThrown() != null) { + PrintWriter pw = null; + try { + final StringWriter sw = new StringWriter(); + pw = new PrintWriter(sw); + record.getThrown().printStackTrace(pw); + sb.append(sw.toString()); + } + catch (final Exception ex) { + } + finally { + IOUtils.closeQuietly(pw); + } + } + return sb.toString(); + } + }); + } + + @Override + public void close() throws SecurityException { + } + + @Override + public void flush() { + } + + @Override + public void publish(final LogRecord record) { + try { + // Avoid repeating the same message that displayed immediately + // before the current message (ROO-30, ROO-1873) + final String toDisplay = toDisplay(record); + if (toDisplay.equals(lastMessage) && suppressDuplicateMessages) { + return; + } + lastMessage = toDisplay; + + final StringBuffer buffer = reader.getCursorBuffer().getBuffer(); + final int cursor = reader.getCursorBuffer().cursor; + if (reader.getCursorBuffer().length() > 0) { + // The user has semi-typed something, so put a new line in so + // the debug message is separated + reader.printNewline(); + + // We need to cancel whatever they typed (it's reset later on), + // so the line appears empty + reader.getCursorBuffer().setBuffer(new StringBuffer()); + reader.getCursorBuffer().cursor = 0; + } + + // This ensures nothing is ever displayed when redrawing the line + reader.setDefaultPrompt(""); + reader.redrawLine(); + + // Now restore the line formatting settings back to their original + reader.setDefaultPrompt(shellPromptAccessor.getShellPrompt()); + + reader.getCursorBuffer().setBuffer(buffer); + reader.getCursorBuffer().cursor = cursor; + + reader.printString(toDisplay); + + final Boolean prohibitingRedraw = redrawProhibit.get(); + if (prohibitingRedraw == null) { + reader.redrawLine(); + } + + reader.flushConsole(); + } + catch (final Exception e) { + reportError("Could not publish log message", e, + Level.SEVERE.intValue()); + } + } + + private String toDisplay(final LogRecord event) { + final StringBuilder sb = new StringBuilder(); + + String threadName; + String eventString; + if (includeThreadName + && !userInterfaceThreadName.equals(Thread.currentThread() + .getName()) + && !"".equals(Thread.currentThread().getName())) { + threadName = "[" + Thread.currentThread().getName() + "]"; + + // Build an event string that will indent nicely given the left hand + // side now contains a thread name + final StringBuilder lineSeparatorAndIndentingString = new StringBuilder(); + for (int i = 0; i <= threadName.length(); i++) { + lineSeparatorAndIndentingString.append(" "); + } + + eventString = " " + + getFormatter().format(event).replace( + LINE_SEPARATOR, + LINE_SEPARATOR + + lineSeparatorAndIndentingString + .toString()); + if (eventString + .endsWith(lineSeparatorAndIndentingString.toString())) { + eventString = eventString.substring(0, eventString.length() + - lineSeparatorAndIndentingString.length()); + } + } + else { + threadName = ""; + eventString = getFormatter().format(event); + } + + if (ansiSupported) { + if (event.getLevel().intValue() >= Level.SEVERE.intValue()) { + sb.append(getANSIBuffer().reverse(threadName).red(eventString)); + } + else if (event.getLevel().intValue() >= Level.WARNING.intValue()) { + sb.append(getANSIBuffer().reverse(threadName).magenta( + eventString)); + } + else if (event.getLevel().intValue() >= Level.INFO.intValue()) { + sb.append(getANSIBuffer().reverse(threadName) + .green(eventString)); + } + else { + sb.append(getANSIBuffer().reverse(threadName).append( + eventString)); + } + } + else { + sb.append(threadName).append(eventString); + } + + return sb.toString(); + } +} diff --git a/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineShell.java b/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineShell.java new file mode 100644 index 000000000..fd897fa80 --- /dev/null +++ b/shell-jline/src/main/java/org/springframework/roo/shell/jline/JLineShell.java @@ -0,0 +1,579 @@ +package org.springframework.roo.shell.jline; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jline.ANSIBuffer; +import jline.ANSIBuffer.ANSICodes; +import jline.ConsoleReader; +import jline.WindowsTerminal; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.shell.AbstractShell; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.ExitShellRequest; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.event.ShellStatus; +import org.springframework.roo.shell.event.ShellStatus.Status; +import org.springframework.roo.shell.event.ShellStatusListener; +import org.springframework.roo.support.util.AnsiEscapeCode; + +/** + * Uses the feature-rich JLine library to provide an + * interactive shell. + *

    + * Due to Windows' lack of color ANSI services out-of-the-box, this + * implementation automatically detects the classpath presence of Jansi and uses it if present. This + * library is not necessary for *nix machines, which support colour ANSI without + * any special effort. This implementation has been written to use reflection in + * order to avoid hard dependencies on Jansi. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class JLineShell extends AbstractShell implements + CommandMarker, Shell, Runnable { + + private static class FlashInfo { + Level flashLevel; + String flashMessage; + long flashMessageUntil; + int rowNumber; + } + + private static final String ANSI_CONSOLE_CLASSNAME = "org.fusesource.jansi.AnsiConsole"; + private static final boolean APPLE_TERMINAL = Boolean + .getBoolean("is.apple.terminal"); + private static final String BEL = "\007"; + private static final char ESCAPE = 27; + + private static final boolean JANSI_AVAILABLE = isPresent( + ANSI_CONSOLE_CLASSNAME, JLineShell.class.getClassLoader()); + + private static boolean isPresent(final String className, + final ClassLoader classLoader) { + try { + return classLoader.loadClass(className) != null; + } + catch (final Throwable t) { + // Class or one of its dependencies is not present... + return false; + } + } + + private boolean developmentMode = false; + private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private FileWriter fileLog; + /** key: slot name, value: flashInfo instance */ + private final Map flashInfoMap = new HashMap(); + private ConsoleReader reader; + /** key: row number, value: eraseLineFromPosition */ + private final Map rowErasureMap = new HashMap(); + + private boolean shutdownHookFired = false; // ROO-1599 + + protected ShellStatusListener statusListener; // ROO-836 + + /** + * Should be called by a subclass before deactivating the shell. + */ + protected void closeShell() { + // Notify we're closing down (normally our status is already + // shutting_down, but if it was a CTRL+C via the o.s.r.bootstrap.Main + // hook) + setShellStatus(Status.SHUTTING_DOWN); + if (statusListener != null) { + removeShellStatusListener(statusListener); + } + } + + private ConsoleReader createAnsiWindowsReader() throws Exception { + // Get decorated OutputStream that parses ANSI-codes + final PrintStream ansiOut = (PrintStream) ClassUtils + .getClass(JLineShell.class.getClassLoader(), + ANSI_CONSOLE_CLASSNAME).getMethod("out").invoke(null); + final WindowsTerminal ansiTerminal = new WindowsTerminal() { + @Override + public boolean isANSISupported() { + return true; + } + }; + ansiTerminal.initializeTerminal(); + // Make sure to reset the original shell's colors on shutdown by closing + // the stream + statusListener = new ShellStatusListener() { + public void onShellStatusChange(final ShellStatus oldStatus, + final ShellStatus newStatus) { + if (newStatus.getStatus().equals(Status.SHUTTING_DOWN)) { + ansiOut.close(); + } + } + }; + addShellStatusListener(statusListener); + + return new ConsoleReader(new FileInputStream(FileDescriptor.in), + new PrintWriter(new OutputStreamWriter(ansiOut, + // Default to Cp850 encoding for Windows console output + // (ROO-439) + System.getProperty( + "jline.WindowsTerminal.output.encoding", + "Cp850"))), null, ansiTerminal); + } + + // Externally synchronized via the two calling methods having a mutex on + // flashInfoMap + private void doAnsiFlash(final int row, final Level level, + final String message) { + final ANSIBuffer buff = JLineLogHandler.getANSIBuffer(); + if (APPLE_TERMINAL) { + buff.append(ESCAPE + "7"); + } + else { + buff.append(ANSICodes.save()); + } + + // Figure out the longest line we're presently displaying (or were) and + // erase the line from that position + int mostFurtherLeftColNumber = Integer.MAX_VALUE; + for (final Integer candidate : rowErasureMap.values()) { + if (candidate < mostFurtherLeftColNumber) { + mostFurtherLeftColNumber = candidate; + } + } + + if (mostFurtherLeftColNumber == Integer.MAX_VALUE) { + // There is nothing to erase + } + else { + buff.append(ANSICodes.gotoxy(row, mostFurtherLeftColNumber)); + // Clear what was present on the line + buff.append(ANSICodes.clreol()); + } + + if ("".equals(message)) { + // They want the line blank; we've already achieved this if needed + // via the erasing above + // Just need to record we no longer care about this line the next + // time doAnsiFlash is invoked + rowErasureMap.remove(row); + } + else { + if (shutdownHookFired) { + return; // ROO-1599 + } + // They want some message displayed + int startFrom = reader.getTermwidth() - message.length() + 1; + if (startFrom < 1) { + startFrom = 1; + } + buff.append(ANSICodes.gotoxy(row, startFrom)); + buff.reverse(message); + // Record we want to erase from this positioning next time (so we + // clean up after ourselves) + rowErasureMap.put(row, startFrom); + } + if (APPLE_TERMINAL) { + buff.append(ESCAPE + "8"); + } + else { + buff.append(ANSICodes.restore()); + } + + final String stg = buff.toString(); + try { + reader.printString(stg); + reader.flushConsole(); + } + catch (final IOException ignored) { + } + } + + @Override + public void flash(final Level level, final String message, final String slot) { + Validate.notNull(level, "Level is required for a flash message"); + Validate.notNull(message, "Message is required for a flash message"); + Validate.notBlank(slot, + "Slot name must be specified for a flash message"); + + if (Shell.WINDOW_TITLE_SLOT.equals(slot)) { + if (reader != null && reader.getTerminal().isANSISupported()) { + // We can probably update the window title, as requested + if (StringUtils.isBlank(message)) { + System.out.println("No text"); + } + + final ANSIBuffer buff = JLineLogHandler.getANSIBuffer(); + buff.append(ESCAPE + "]0;").append(message).append(BEL); + final String stg = buff.toString(); + try { + reader.printString(stg); + reader.flushConsole(); + } + catch (final IOException ignored) { + } + } + + return; + } + if (reader != null && !reader.getTerminal().isANSISupported()) { + super.flash(level, message, slot); + return; + } + synchronized (flashInfoMap) { + FlashInfo flashInfo = flashInfoMap.get(slot); + + if ("".equals(message)) { + // Request to clear the message, but give the user some time to + // read it first + if (flashInfo == null) { + // We didn't have a record of displaying it in the first + // place, so just quit + return; + } + flashInfo.flashMessageUntil = System.currentTimeMillis() + 1500; + } + else { + // Display this message displayed until further notice + if (flashInfo == null) { + // Find a row for this new slot; we basically take the first + // line number we discover + flashInfo = new FlashInfo(); + flashInfo.rowNumber = Integer.MAX_VALUE; + outer: for (int i = 1; i < Integer.MAX_VALUE; i++) { + for (final FlashInfo existingFlashInfo : flashInfoMap + .values()) { + if (existingFlashInfo.rowNumber == i) { + // Veto, let's try the new candidate row number + continue outer; + } + } + // If we got to here, nobody owns this row number, so + // use it + flashInfo.rowNumber = i; + break outer; + } + + // Store it + flashInfoMap.put(slot, flashInfo); + } + // Populate the instance with the latest data + flashInfo.flashMessageUntil = Long.MAX_VALUE; + flashInfo.flashLevel = level; + flashInfo.flashMessage = message; + + // Display right now + doAnsiFlash(flashInfo.rowNumber, flashInfo.flashLevel, + flashInfo.flashMessage); + } + } + } + + private void flashMessageRenderer() { + if (!reader.getTerminal().isANSISupported()) { + return; + } + // Setup a thread to ensure flash messages are displayed and cleared + // correctly + final Thread t = new Thread(new Runnable() { + public void run() { + while (!shellStatus.getStatus().equals(Status.SHUTTING_DOWN) + && !shutdownHookFired) { + synchronized (flashInfoMap) { + final long now = System.currentTimeMillis(); + + final Set toRemove = new HashSet(); + for (final String slot : flashInfoMap.keySet()) { + final FlashInfo flashInfo = flashInfoMap.get(slot); + + if (flashInfo.flashMessageUntil < now) { + // Message has expired, so clear it + toRemove.add(slot); + doAnsiFlash(flashInfo.rowNumber, Level.ALL, ""); + } + else { + // The expiration time for this message has not + // been reached, so preserve it + doAnsiFlash(flashInfo.rowNumber, + flashInfo.flashLevel, + flashInfo.flashMessage); + } + } + for (final String slot : toRemove) { + flashInfoMap.remove(slot); + } + } + try { + Thread.sleep(200); + } + catch (final InterruptedException ignore) { + } + } + } + }, "Spring Roo JLine Flash Message Manager"); + t.start(); + } + + /** + * Obtains the "roo.home" from the system property, falling back to the + * current working directory if missing. + * + * @return the 'roo.home' system property + */ + @Override + protected String getHomeAsString() { + String rooHome = System.getProperty("roo.home"); + if (rooHome == null) { + try { + rooHome = new File(".").getCanonicalPath(); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + return rooHome; + } + + public String getStartupNotifications() { + return null; + } + + public boolean isDevelopmentMode() { + return developmentMode; + } + + @Override + protected void logCommandToOutput(final String processedLine) { + if (fileLog == null) { + openFileLogIfPossible(); + if (fileLog == null) { + // Still failing, so give up + return; + } + } + try { + // Unix line endings only from Roo + fileLog.write(processedLine + "\n"); + // So tail -f will show it's working + fileLog.flush(); + if (getExitShellRequest() != null) { + // Shutting down, so close our file (we can always reopen it + // later if needed) + fileLog.write("// Spring Roo " + versionInfo() + + " log closed at " + df.format(new Date()) + "\n"); + IOUtils.closeQuietly(fileLog); + fileLog = null; + } + } + catch (final IOException ignored) { + } + } + + private void openFileLogIfPossible() { + try { + fileLog = new FileWriter("log.roo", true); + // First write, so let's record the date and time of the first user + // command + fileLog.write("// Spring Roo " + versionInfo() + " log opened at " + + df.format(new Date()) + "\n"); + fileLog.flush(); + } + catch (final IOException ignoreIt) { + } + } + + public void promptLoop() { + setShellStatus(Status.USER_INPUT); + String line; + + try { + while (exitShellRequest == null + && (line = reader.readLine()) != null) { + JLineLogHandler.resetMessageTracking(); + setShellStatus(Status.USER_INPUT); + + if ("".equals(line)) { + continue; + } + + executeCommand(line); + } + } + catch (final IOException ioe) { + throw new IllegalStateException("Shell line reading failure", ioe); + } + + setShellStatus(Status.SHUTTING_DOWN); + } + + private void removeHandlers(final Logger l) { + final Handler[] handlers = l.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (final Handler h : handlers) { + l.removeHandler(h); + } + } + } + + public void run() { + try { + if (JANSI_AVAILABLE && SystemUtils.IS_OS_WINDOWS) { + try { + reader = createAnsiWindowsReader(); + } + catch (final Exception e) { + // Try again using default ConsoleReader constructor + logger.warning("Can't initialize jansi AnsiConsole, falling back to default: " + + e); + } + } + if (reader == null) { + reader = new ConsoleReader(); + } + } + catch (final IOException ioe) { + throw new IllegalStateException("Cannot start console class", ioe); + } + + setPromptPath(null); + + final JLineLogHandler handler = new JLineLogHandler(reader, this); + JLineLogHandler.prohibitRedraw(); // Affects this thread only + final Logger mainLogger = Logger.getLogger(""); + removeHandlers(mainLogger); + mainLogger.addHandler(handler); + + reader.addCompletor(new JLineCompletorAdapter(getParser())); + + reader.setBellEnabled(true); + if (Boolean.getBoolean("jline.nobell")) { + reader.setBellEnabled(false); + } + + // reader.setDebug(new PrintWriter(new FileWriter("writer.debug", + // true))); + + openFileLogIfPossible(); + + // Try to build previous command history from the project's log + try { + final String logFileContents = FileUtils.readFileToString(new File( + "log.roo")); + final String[] logEntries = logFileContents + .split(IOUtils.LINE_SEPARATOR); + // LIFO + for (final String logEntry : logEntries) { + if (!logEntry.startsWith("//")) { + reader.getHistory().addToHistory(logEntry); + } + } + } + catch (final IOException ignored) { + } + + flashMessageRenderer(); + + logger.info(version(null)); + + flash(Level.FINE, "Spring Roo " + versionInfo(), + Shell.WINDOW_TITLE_SLOT); + + logger.info("Welcome to Spring Roo. For assistance press " + + completionKeys + " or type \"hint\" then hit ENTER."); + + final String startupNotifications = getStartupNotifications(); + if (StringUtils.isNotBlank(startupNotifications)) { + logger.info(startupNotifications); + } + + setShellStatus(Status.STARTED); + + // Monitor CTRL+C initiated shutdowns (ROO-1599) + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + public void run() { + shutdownHookFired = true; + // We don't need to closeShell(), as the shutdown hook in + // o.s.r.bootstrap.Main calls stop() which calls + // JLineShellComponent.deactivate() and that calls closeShell() + } + }, "Spring Roo JLine Shutdown Hook")); + + // Handle any "execute-then-quit" operation + final String rooArgs = System.getProperty("roo.args"); + if (rooArgs != null && !"".equals(rooArgs)) { + setShellStatus(Status.USER_INPUT); + final boolean success = executeCommand(rooArgs); + if (exitShellRequest == null) { + // The command itself did not specify an exit shell code, so + // we'll fall back to something sensible here + executeCommand("quit"); // ROO-839 + exitShellRequest = success ? ExitShellRequest.NORMAL_EXIT + : ExitShellRequest.FATAL_EXIT; + } + setShellStatus(Status.SHUTTING_DOWN); + } + else { + // Normal RPEL processing + promptLoop(); + } + } + + public void setDevelopmentMode(final boolean developmentMode) { + JLineLogHandler.setIncludeThreadName(developmentMode); + // We want to see duplicate messages during development time (ROO-1873) + JLineLogHandler.setSuppressDuplicateMessages(!developmentMode); + this.developmentMode = developmentMode; + } + + @Override + public void setPromptPath(final String path) { + setPromptPath(path, false); + } + + @Override + public void setPromptPath(final String path, final boolean overrideStyle) { + if (reader.getTerminal().isANSISupported()) { + if (StringUtils.isBlank(path)) { + shellPrompt = AnsiEscapeCode.decorate(ROO_PROMPT, + AnsiEscapeCode.FG_YELLOW); + } + else { + final String decoratedPath = overrideStyle ? AnsiEscapeCode + .decorate(path) : AnsiEscapeCode.decorate(path, + AnsiEscapeCode.FG_CYAN); + shellPrompt = decoratedPath + + AnsiEscapeCode.decorate(" " + ROO_PROMPT, + AnsiEscapeCode.FG_YELLOW); + } + } + else { + // The superclass will do for this non-ANSI terminal + super.setPromptPath(path); + } + + // The shellPrompt is now correct; let's ensure it now gets used + reader.setDefaultPrompt(AbstractShell.shellPrompt); + } +} diff --git a/shell-osgi/pom.xml b/shell-osgi/pom.xml new file mode 100644 index 000000000..7b3cbfaa9 --- /dev/null +++ b/shell-osgi/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.shell.osgi + bundle + Spring Roo - Shell (OSGi Launcher) + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.shell + + + \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/AbstractFlashingObject.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/AbstractFlashingObject.java new file mode 100644 index 000000000..9c54242a2 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/AbstractFlashingObject.java @@ -0,0 +1,87 @@ +package org.springframework.roo.shell.osgi; + +import java.util.logging.Level; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.springframework.roo.shell.Shell; + +/** + * Provides an easy way for subclasses to publish flash messages if a + * {@link Shell} is available but without creating a dependency on the + * {@link Shell} being available. This abstract class also enables subclasses to + * safely determine if the {@link Shell} is in development mode. + *

    + * Subclasses should not use the normal + * {@link Shell#flash(Level, String, String)} method. Instead they should use + * {@link #flash(Level, String, String)} and not declare a direct dependency on + * {@link Shell}. + *

    + * If a {@link Shell} is not available, this class will simply not publish flash + * messages. If a {@link Shell} is available, flash messages will be sent to + * that {@link Shell}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component(componentAbstract = true) +@Reference(name = "shell", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = Shell.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY) +public abstract class AbstractFlashingObject { + + private final Class mutex = getClass(); + /** + * Provided as a convenience for subclasses so they have a unique slot name + * for flash messages. + */ + protected final String MY_SLOT = getClass().getName(); + private Shell shell; + + protected final void bindShell(final Shell shell) { + synchronized (mutex) { + this.shell = shell; + } + } + + /** + * Same signature as {@link Shell#flash(Level, String, String)}. If this + * method is called and the {@link Shell} is not available, it will simply + * discard the flash message. + * + * @param level see {@link Shell#flash(Level, String, String)} + * @param message see {@link Shell#flash(Level, String, String)} + * @param slot see {@link Shell#flash(Level, String, String)} + */ + protected final void flash(final Level level, final String message, + final String slot) { + synchronized (mutex) { + if (shell != null) { + shell.flash(level, message, slot); + } + } + } + + /** + * Delegates to the {@link Shell#isDevelopmentMode()} method if available. + * If no {@link Shell} is available, simply returns false. + * + * @return true if the shell is available and it is in development mode + * (false in any other case) + */ + protected final boolean isDevelopmentMode() { + synchronized (mutex) { + if (shell != null) { + return shell.isDevelopmentMode(); + } + return false; + } + } + + protected final void unbindShell(final Shell shell) { + synchronized (mutex) { + this.shell = null; + } + } +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/SimpleParserComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/SimpleParserComponent.java new file mode 100644 index 000000000..b442ed2bd --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/SimpleParserComponent.java @@ -0,0 +1,88 @@ +package org.springframework.roo.shell.osgi; + +import java.util.logging.Logger; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.ReferenceStrategy; +import org.apache.felix.scr.annotations.References; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.AbstractShell; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.Parser; +import org.springframework.roo.shell.SimpleParser; +import org.springframework.roo.support.api.AddOnSearch; + +/** + * OSGi component launcher for {@link SimpleParser}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service(value = Parser.class) +@References(value = { + @Reference(name = "addOnSearch", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = AddOnSearch.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY) }) +public class SimpleParserComponent extends SimpleParser implements + CommandMarker { + private AddOnSearch addOnSearch; + + protected void activate(final ComponentContext cContext) { + context = cContext.getBundleContext(); + } + + protected void bindAddOnSearch(final AddOnSearch s) { + addOnSearch = s; + } + + @Override + protected void commandNotFound(final Logger logger, final String buffer) { + logger.warning("Command '" + buffer + + "' not found (for assistance press " + + AbstractShell.completionKeys + + " or type \"hint\" then hit ENTER)"); + + if (addOnSearch == null) { + return; + } + + // Decide which command they asked for + String command = buffer.trim(); + + // Truncate from the first option, if any was given + final int firstDash = buffer.indexOf("--"); + if (firstDash > 1) { + command = buffer.substring(0, firstDash - 1).trim(); + } + + // Do a silent (console message free) lookup of matches + Integer matches = null; + matches = addOnSearch.searchAddOns(false, null, false, 1, 99, false, + false, false, command); + + // Render to screen if required + if (matches == null) { + logger.info("Spring Roo automatic add-on discovery service currently unavailable"); + } + else if (matches == 0) { + logger.info("addon search --requiresCommand \"" + command + + "\" found no matches"); + } + else if (matches > 0) { + logger.info("Located add-on" + (matches == 1 ? "" : "s") + + " that may offer this command"); + addOnSearch.searchAddOns(true, null, false, 1, 99, false, false, + false, command); + } + } + + protected void unbindAddOnSearch(final AddOnSearch s) { + addOnSearch = null; + } +} diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/AvailableCommandsConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/AvailableCommandsConverterComponent.java new file mode 100644 index 000000000..38f693bb2 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/AvailableCommandsConverterComponent.java @@ -0,0 +1,18 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.AvailableCommandsConverter; +import org.springframework.roo.shell.converters.StaticFieldConverterImpl; + +/** + * OSGi component launcher for {@link StaticFieldConverterImpl}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class AvailableCommandsConverterComponent extends + AvailableCommandsConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BigDecimalConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BigDecimalConverterComponent.java new file mode 100644 index 000000000..b1f3a5f8c --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BigDecimalConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.BigDecimalConverter; + +/** + * OSGi component launcher for {@link BigDecimalConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class BigDecimalConverterComponent extends BigDecimalConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BigIntegerConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BigIntegerConverterComponent.java new file mode 100644 index 000000000..779564e99 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BigIntegerConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.BigIntegerConverter; + +/** + * OSGi component launcher for {@link BigIntegerConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class BigIntegerConverterComponent extends BigIntegerConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BooleanConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BooleanConverterComponent.java new file mode 100644 index 000000000..ffbf8953c --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/BooleanConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.BooleanConverter; + +/** + * OSGi component launcher for {@link BooleanConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class BooleanConverterComponent extends BooleanConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/CharacterConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/CharacterConverterComponent.java new file mode 100644 index 000000000..2fbed9b35 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/CharacterConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.CharacterConverter; + +/** + * OSGi component launcher for {@link CharacterConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class CharacterConverterComponent extends CharacterConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/DateConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/DateConverterComponent.java new file mode 100644 index 000000000..d6ca7c5ed --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/DateConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.DateConverter; + +/** + * OSGi component launcher for {@link DateConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class DateConverterComponent extends DateConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/DoubleConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/DoubleConverterComponent.java new file mode 100644 index 000000000..bf6eba509 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/DoubleConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.DoubleConverter; + +/** + * OSGi component launcher for {@link DoubleConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class DoubleConverterComponent extends DoubleConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/EnumConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/EnumConverterComponent.java new file mode 100644 index 000000000..d07d98fe1 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/EnumConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.EnumConverter; + +/** + * OSGi component launcher for {@link EnumConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class EnumConverterComponent extends EnumConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/FileConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/FileConverterComponent.java new file mode 100644 index 000000000..9e1457397 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/FileConverterComponent.java @@ -0,0 +1,66 @@ +package org.springframework.roo.shell.osgi.converters; + +import java.io.File; +import java.util.logging.Logger; +import org.apache.commons.lang3.Validate; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.converters.FileConverter; +import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; + + +/** + * OSGi component launcher for {@link FileConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class FileConverterComponent extends FileConverter { + + private Shell shell; + + protected final static Logger LOGGER = HandlerUtils.getLogger(FileConverterComponent.class); + + // ------------ OSGi component attributes ---------------- + private BundleContext context; + + protected void activate(final ComponentContext context) { + this.context = context.getBundleContext(); + } + + @Override + protected File getWorkingDirectory() { + if(shell == null){ + shell = getShell(); + } + Validate.notNull(shell, "Shell required"); + return shell.getHome(); + } + + public Shell getShell(){ + // Get all Services implement Shell interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(Shell.class.getName(), null); + + for(ServiceReference ref : references){ + return (Shell) this.context.getService(ref); + } + + return null; + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load Shell on DefaultFileManager."); + return null; + } + } + +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/FloatConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/FloatConverterComponent.java new file mode 100644 index 000000000..598015ddf --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/FloatConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.FloatConverter; + +/** + * OSGi component launcher for {@link FloatConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class FloatConverterComponent extends FloatConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/IntegerConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/IntegerConverterComponent.java new file mode 100644 index 000000000..e7cd8d57b --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/IntegerConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.IntegerConverter; + +/** + * OSGi component launcher for {@link IntegerConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class IntegerConverterComponent extends IntegerConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/LocaleConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/LocaleConverterComponent.java new file mode 100644 index 000000000..f04418917 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/LocaleConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.LocaleConverter; + +/** + * OSGi component launcher for {@link LocaleConverter}. + * + * @author Stefan Schmidt + * @since 1.1 + */ +@Component +@Service +public class LocaleConverterComponent extends LocaleConverter { +} diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/LongConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/LongConverterComponent.java new file mode 100644 index 000000000..a3201e5d3 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/LongConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.LongConverter; + +/** + * OSGi component launcher for {@link LongConverterComponent}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class LongConverterComponent extends LongConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/ShortConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/ShortConverterComponent.java new file mode 100644 index 000000000..4c0dd2c55 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/ShortConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.ShortConverter; + +/** + * OSGi component launcher for {@link ShortConverterComponent}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class ShortConverterComponent extends ShortConverter { +} \ No newline at end of file diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/StaticFieldConverterImplComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/StaticFieldConverterImplComponent.java new file mode 100644 index 000000000..dccd18c06 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/StaticFieldConverterImplComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.StaticFieldConverterImpl; + +/** + * OSGi component launcher for {@link StaticFieldConverterImpl}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class StaticFieldConverterImplComponent extends StaticFieldConverterImpl { +} diff --git a/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/StringConverterComponent.java b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/StringConverterComponent.java new file mode 100644 index 000000000..fa5ecb8a2 --- /dev/null +++ b/shell-osgi/src/main/java/org/springframework/roo/shell/osgi/converters/StringConverterComponent.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell.osgi.converters; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.converters.StringConverter; + +/** + * OSGi component launcher for {@link StringConverter}. + * + * @author Ben Alex + * @since 1.1 + */ +@Service +@Component +public class StringConverterComponent extends StringConverter { +} \ No newline at end of file diff --git a/shell/legal-shell.txt b/shell/legal-shell.txt new file mode 100644 index 000000000..01389452d --- /dev/null +++ b/shell/legal-shell.txt @@ -0,0 +1,34 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +NaturalOrderComparator.java -- Perform natural order comparisons of strings in Java. + +Copyright (C) 2003 by Pierre-Luc Paour +Based on the C version by Martin Pool, of which this is more or less a straight conversion. +Copyright (C) 2000 by Martin Pool + +This software is provided as-is, without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +----------------------------------------------------------------------- + + +[end] diff --git a/shell/pom.xml b/shell/pom.xml new file mode 100644 index 000000000..e11cfc069 --- /dev/null +++ b/shell/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.shell + bundle + Spring Roo - Shell + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/AbstractShell.java b/shell/src/main/java/org/springframework/roo/shell/AbstractShell.java new file mode 100644 index 000000000..e2416f1c3 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/AbstractShell.java @@ -0,0 +1,671 @@ +package org.springframework.roo.shell; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.springframework.roo.shell.event.AbstractShellStatusPublisher; +import org.springframework.roo.shell.event.ShellStatus; +import org.springframework.roo.shell.event.ShellStatus.Status; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Provides a base {@link Shell} implementation. + * + * @author Ben Alex + */ +@Component +public abstract class AbstractShell extends AbstractShellStatusPublisher + implements Shell { + + List commandListeners = new ArrayList(); + + private static final Logger LOGGER = HandlerUtils + .getLogger(AbstractShell.class); + + private CommandListener commandListener; + + private static final String MY_SLOT = AbstractShell.class.getName(); + protected static final String ROO_PROMPT = "roo> "; + + // Public static fields; don't rename, make final, or make non-public, as + // they are part of the public API, e.g. are changed by STS. + public static String completionKeys = "TAB"; + public static String shellPrompt = ROO_PROMPT; + + public static String versionInfo() { + // Try to determine the bundle version + String bundleVersion = null; + String gitCommitHash = null; + JarFile jarFile = null; + try { + final URL classContainer = AbstractShell.class + .getProtectionDomain().getCodeSource().getLocation(); + if (classContainer.toString().endsWith(".jar")) { + // Attempt to obtain the "Bundle-Version" version from the + // manifest + jarFile = new JarFile(new File(classContainer.toURI()), false); + final ZipEntry manifestEntry = jarFile + .getEntry("META-INF/MANIFEST.MF"); + final Manifest manifest = new Manifest( + jarFile.getInputStream(manifestEntry)); + bundleVersion = manifest.getMainAttributes().getValue( + "Bundle-Version"); + gitCommitHash = manifest.getMainAttributes().getValue( + "Git-Commit-Hash"); + } + } + catch (final IOException ignoreAndMoveOn) { + } + catch (final URISyntaxException ignoreAndMoveOn) { + } + finally { + if (jarFile != null) { + try { + jarFile.close(); + } + catch (final IOException ignored) { + } + } + } + + final StringBuilder sb = new StringBuilder(); + + if (bundleVersion != null) { + sb.append(bundleVersion); + } + + if (gitCommitHash != null && gitCommitHash.length() > 7) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append("[rev "); + sb.append(gitCommitHash.substring(0, 7)); + sb.append("]"); + } + + if (sb.length() == 0) { + sb.append("UNKNOWN VERSION"); + } + + return sb.toString(); + } + + protected final Logger logger = HandlerUtils.getLogger(getClass()); + protected boolean inBlockComment; + protected ExitShellRequest exitShellRequest; + private Tailor tailor; + + @CliCommand(value = { "/*" }, help = "Start of block comment") + public void blockCommentBegin() { + Validate.isTrue(!inBlockComment, + "Cannot open a new block comment when one already active"); + inBlockComment = true; + } + + @CliCommand(value = { "*/" }, help = "End of block comment") + public void blockCommentFinish() { + Validate.isTrue(inBlockComment, + "Cannot close a block comment when it has not been opened"); + inBlockComment = false; + } + + @CliCommand(value = { "date" }, help = "Displays the local date and time") + public String date() { + return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL) + .format(new Date()); + } + + public boolean executeCommand(final String line) { + if (tailor == null) { + return executeCommandImpl(line); + } + /* + * If getTailor() is not null, then try to transform input command and + * execute all outputs sequentially + */ + List commands = null; + commands = tailor.sew(line); + + if (CollectionUtils.isEmpty(commands)) { + return executeCommandImpl(line); + } + for (final String command : commands) { + logger.info("roo-tailor> " + command); + if (!executeCommandImpl(command)) { + return false; + } + } + return true; + } + + /** + * Runs the specified command. Control will return to the caller after the + * command is run. + */ + private boolean executeCommandImpl(String line) { + // Another command was attempted + setShellStatus(ShellStatus.Status.PARSING); + + final ExecutionStrategy executionStrategy = getExecutionStrategy(); + boolean flashedMessage = false; + while (executionStrategy == null + || !executionStrategy.isReadyForCommands()) { + // Wait + try { + Thread.sleep(500); + } + catch (final InterruptedException ignore) { + } + if (!flashedMessage) { + flash(Level.INFO, "Please wait - still loading", MY_SLOT); + flashedMessage = true; + } + } + if (flashedMessage) { + flash(Level.INFO, "", MY_SLOT); + } + + ParseResult parseResult = null; + try { + // We support simple block comments; ie a single pair per line + if (!inBlockComment && line.contains("/*") && line.contains("*/")) { + blockCommentBegin(); + final String lhs = line.substring(0, line.lastIndexOf("/*")); + if (line.contains("*/")) { + line = lhs + line.substring(line.lastIndexOf("*/") + 2); + blockCommentFinish(); + } + else { + line = lhs; + } + } + if (inBlockComment) { + if (!line.contains("*/")) { + return true; + } + blockCommentFinish(); + line = line.substring(line.lastIndexOf("*/") + 2); + } + // We also support inline comments (but only at start of line, + // otherwise valid + // command options like http://www.helloworld.com will fail as per + // ROO-517) + if (!inBlockComment + && (line.trim().startsWith("//") || line.trim().startsWith( + "#"))) { // # support in ROO-1116 + line = ""; + } + // Convert any TAB characters to whitespace (ROO-527) + line = line.replace('\t', ' '); + if ("".equals(line.trim())) { + setShellStatus(Status.EXECUTION_SUCCESS); + return true; + } + parseResult = getParser().parse(line); + if (parseResult == null) { + return false; + } + try { + notifyBeginExecute(parseResult); + }catch (final Exception ignored) { + } + setShellStatus(Status.EXECUTING); + final Object result = executionStrategy.execute(parseResult); + setShellStatus(Status.EXECUTION_RESULT_PROCESSING); + if (result != null) { + if (result instanceof ExitShellRequest) { + exitShellRequest = (ExitShellRequest) result; + // Give ProcessManager a chance to close down its threads + // before the overall OSGi framework is terminated + // (ROO-1938) + executionStrategy.terminate(); + } + else if (result instanceof Iterable) { + for (final Object o : (Iterable) result) { + logger.info(o.toString()); + } + } + else { + logger.info(result.toString()); + } + } + + logCommandIfRequired(line, true); + setShellStatus(Status.EXECUTION_SUCCESS, line, parseResult); + // ROO-3581: When command success, execute command listener SUCCESS + try { + notifyExecutionSuccess(); + }catch (final Exception ignored) { + } + + return true; + } + catch (final RuntimeException e) { + setShellStatus(Status.EXECUTION_FAILED, line, parseResult); + try { + // ROO-3581: When command fails, execute command listener FAILS + notifyExecutionFailed(); + }catch (final Exception ignored) { + } + // We rely on execution strategy to log it + try { + logCommandIfRequired(line, false); + } + catch (final Exception ignored) { + } + return false; + } + finally { + setShellStatus(Status.USER_INPUT); + } + } + + /** + * Execute the single line from a script. + *

    + * This method can be overridden by sub-classes to pre-process script lines. + */ + protected boolean executeScriptLine(final String line) { + return executeCommand(line); + } + + /** + * Returns any classpath resources with the given path + * + * @param path the path for which to search (never null) + * @return null if the search can't be performed + * @since 1.2.0 + */ + protected abstract Collection findResources(String path); + + /** + * Simple implementation of {@link #flash(Level, String, String)} that + * simply displays the message via the logger. It is strongly recommended + * shell implementations override this method with a more effective + * approach. + */ + public void flash(final Level level, final String message, final String slot) { + Validate.notNull(level, "Level is required for a flash message"); + Validate.notNull(message, "Message is required for a flash message"); + Validate.notBlank(slot, + "Slot name must be specified for a flash message"); + if (!"".equals(message)) { + logger.log(level, message); + } + } + + @CliCommand(value = { "flash test" }, help = "Tests message flashing") + public void flashCustom() throws Exception { + flash(Level.FINE, "Hello world", "a"); + Thread.sleep(150); + flash(Level.FINE, "Short world", "a"); + Thread.sleep(150); + flash(Level.FINE, "Small", "a"); + Thread.sleep(150); + flash(Level.FINE, "Downloading xyz", "b"); + Thread.sleep(150); + flash(Level.FINE, "", "a"); + Thread.sleep(150); + flash(Level.FINE, "Downloaded xyz", "b"); + Thread.sleep(150); + flash(Level.FINE, "System online", "c"); + Thread.sleep(150); + flash(Level.FINE, "System ready", "c"); + Thread.sleep(150); + flash(Level.FINE, "System farewell", "c"); + Thread.sleep(150); + flash(Level.FINE, "", "c"); + Thread.sleep(150); + flash(Level.FINE, "", "b"); + } + + protected abstract ExecutionStrategy getExecutionStrategy(); + + public ExitShellRequest getExitShellRequest() { + return exitShellRequest; + } + + /** + * Obtains the home directory for the current shell instance. + *

    + * Note: calls the {@link #getHomeAsString()} method to allow subclasses to + * provide the home directory location as string using different + * environment-specific strategies. + *

    + * If the path indicated by {@link #getHomeAsString()} exists and refers to + * a directory, that directory is returned. + *

    + * If the path indicated by {@link #getHomeAsString()} exists and refers to + * a file, an exception is thrown. + *

    + * If the path indicated by {@link #getHomeAsString()} does not exist, it + * will be created as a directory. If this fails, an exception will be + * thrown. + * + * @return the home directory for the current shell instance (which is + * guaranteed to exist and be a directory) + */ + public File getHome() { + final String rooHome = getHomeAsString(); + final File f = new File(rooHome); + Validate.isTrue(!f.exists() || f.exists() && f.isDirectory(), + "Path '%s' must be a directory, or it must not exist", + f.getAbsolutePath()); + if (!f.exists()) { + f.mkdirs(); + } + Validate.isTrue( + f.exists() && f.isDirectory(), + "Path '%s' is not a directory; please specify roo.home system property correctly", + f.getAbsolutePath()); + return f; + } + + protected abstract String getHomeAsString(); + + protected abstract Parser getParser(); + + public String getShellPrompt() { + return shellPrompt; + } + + @CliCommand(value = { "//", ";" }, help = "Inline comment markers (start of line only)") + public void inlineComment() { + } + + /** + * Allows a subclass to log the execution of a well-formed command. This is + * invoked after a command has completed, and indicates whether the command + * returned normally or returned an exception. Note that attempted commands + * that are not well-formed (eg they are missing a mandatory argument) will + * never be presented to this method, as the command execution is never + * actually attempted in those cases. This method is only invoked if an + * attempt is made to execute a particular command. + *

    + * Implementations should consider specially handling the "script" commands, + * and also indicating whether a command was successful or not. + * Implementations that wish to behave consistently with other + * {@link AbstractShell} subclasses are encouraged to simply override + * {@link #logCommandToOutput(String)} instead, and only override this + * method if you actually need to fine-tune the output logic. + * + * @param line the parsed line (any comments have been removed; never null) + * @param successful if the command was successful or not + */ + protected void logCommandIfRequired(final String line, + final boolean successful) { + if (line.startsWith("script")) { + logCommandToOutput((successful ? "// " : "// [failed] ") + line); + } + else { + logCommandToOutput((successful ? "" : "// [failed] ") + line); + } + } + + /** + * Allows a subclass to actually write the resulting logged command to some + * form of output. This frees subclasses from needing to implement the logic + * within {@link #logCommandIfRequired(String, boolean)}. + *

    + * Implementations should invoke {@link #getExitShellRequest()} to monitor + * any attempts to exit the shell and release resources such as output log + * files. + * + * @param processedLine the line that should be appended to some type of + * output (excluding the \n character) + */ + protected void logCommandToOutput(final String processedLine) { + } + + /** + * Opens the given script for reading + * + * @param script the script to read (required) + * @return a non-null input stream + */ + private InputStream openScript(final File script) { + try { + return new BufferedInputStream(new FileInputStream(script)); + } + catch (final FileNotFoundException fnfe) { + // Try to find the script via the classloader + final Collection urls = findResources(script.getName()); + + // Handle search failure + Validate.notNull(urls, "Unexpected error looking for '%s'", + script.getName()); + + // Handle the search being OK but the file simply not being present + Validate.notEmpty(urls, + "Script '%s' not found on disk or in classpath", script); + Validate.isTrue( + urls.size() == 1, + "More than one '%s' was found in the classpath; unable to continue", + script); + try { + return urls.iterator().next().openStream(); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + } + } + + @CliCommand(value = { "system properties" }, help = "Shows the shell's properties") + public String props() { + final Set data = new TreeSet(); + for (final Entry entry : System.getProperties() + .entrySet()) { + data.add(entry.getKey() + " = " + entry.getValue()); + } + + return StringUtils.join(data, LINE_SEPARATOR) + LINE_SEPARATOR; + } + + private double round(final double valueToRound, + final int numberOfDecimalPlaces) { + final double multiplicationFactor = Math.pow(10, numberOfDecimalPlaces); + final double interestedInZeroDPs = valueToRound * multiplicationFactor; + return Math.round(interestedInZeroDPs) / multiplicationFactor; + } + + @CliCommand(value = { "script" }, help = "Parses the specified resource file and executes its commands") + public void script( + @CliOption(key = { "", "file" }, help = "The file to locate and execute", mandatory = true) final File script, + @CliOption(key = "lineNumbers", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Display line numbers when executing the script") final boolean lineNumbers) { + + Validate.notNull(script, "Script file to parse is required"); + final double startedNanoseconds = System.nanoTime(); + + final InputStream inputStream = openScript(script); + try { + int i = 0; + for (final String line : IOUtils.readLines(inputStream)) { + i++; + if (lineNumbers) { + logger.fine("Line " + i + ": " + line); + } + else { + logger.fine(line); + } + if (!"".equals(line.trim())) { + final boolean success = executeScriptLine(line); + if (success + && (line.trim().startsWith("q") || line.trim() + .startsWith("ex"))) { + break; + } + else if (!success) { + // Abort script processing, given something went wrong + throw new IllegalStateException( + "Script execution aborted"); + } + } + } + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + final double executionDurationInSeconds = (System.nanoTime() - startedNanoseconds) / 1000000000D; + logger.fine("Script required " + + round(executionDurationInSeconds, 3) + + " seconds to execute"); + } + } + + /** + * Base implementation of the {@link Shell#setPromptPath(String)} method, + * designed for simple shell implementations. Advanced implementations (eg + * those that support ANSI codes etc) will likely want to override this + * method and set the {@link #shellPrompt} variable directly. + * + * @param path to set (can be null or empty; must NOT be formatted in any + * special way eg ANSI codes) + */ + public void setPromptPath(final String path) { + shellPrompt = (StringUtils.isNotBlank(path) ? path + " " : "") + + ROO_PROMPT; + } + + /** + * Default implementation of {@link Shell#setPromptPath(String, boolean))} + * method to satisfy STS compatibility. + * + * @param path to set (can be null or empty) + * @param overrideStyle + */ + public void setPromptPath(final String path, final boolean overrideStyle) { + setPromptPath(path); + } + + public void setTailor(final Tailor tailor) { + this.tailor = tailor; + } + + @Override + public void addListerner(CommandListener listener) { + commandListeners.add(listener); + } + + @Override + public void removeListener(CommandListener listener) { + commandListeners.remove(listener); + } + + private void notifyExecutionFailed() { + if (commandListeners.isEmpty()) { + return; + } + + for (CommandListener listener : commandListeners) { + listener.onCommandFails(); + } + } + + private void notifyExecutionSuccess() { + if (commandListeners.isEmpty()) { + return; + } + for (CommandListener listener : commandListeners) { + listener.onCommandSuccess(); + } + + } + + private void notifyBeginExecute(ParseResult parseResult) { + if (commandListeners.isEmpty()) { + return; + } + for (CommandListener listener : commandListeners) { + listener.onCommandBegin(parseResult); + } + } + + @CliCommand(value = { "version" }, help = "Displays shell version") + public String version( + @CliOption(key = "", help = "Special version flags") final String extra) { + final StringBuilder sb = new StringBuilder(); + + if ("roorocks".equals(extra)) { + sb.append(" /\\ /l").append(LINE_SEPARATOR); + sb.append(" ((.Y(!").append(LINE_SEPARATOR); + sb.append(" \\ |/").append(LINE_SEPARATOR); + sb.append(" / 6~6,").append(LINE_SEPARATOR); + sb.append(" \\ _ +-.").append(LINE_SEPARATOR); + sb.append(" \\`-=--^-' \\").append(LINE_SEPARATOR); + sb.append( + " \\ \\ |\\--------------------------+") + .append(LINE_SEPARATOR); + sb.append( + " _/ \\ | Thanks for loading Roo! |") + .append(LINE_SEPARATOR); + sb.append( + " ( . Y +---------------------------+") + .append(LINE_SEPARATOR); + sb.append(" /\"\\ `---^--v---.").append( + LINE_SEPARATOR); + sb.append(" / _ `---\"T~~\\/~\\/").append( + LINE_SEPARATOR); + sb.append(" / \" ~\\. !").append(LINE_SEPARATOR); + sb.append(" _ Y Y.~~~ /'").append(LINE_SEPARATOR); + sb.append(" Y^| | | Roo 7").append(LINE_SEPARATOR); + sb.append(" | l | / . /'").append(LINE_SEPARATOR); + sb.append(" | `L | Y .^/ ~T").append(LINE_SEPARATOR); + sb.append(" | l ! | |/ | | ____ ____ ____") + .append(LINE_SEPARATOR); + sb.append( + " | .`\\/' | Y | ! / __ \\/ __ \\/ __ \\") + .append(LINE_SEPARATOR); + sb.append( + " l \"~ j l j L______ / /_/ / / / / / / /") + .append(LINE_SEPARATOR); + sb.append( + " \\,____{ __\"\" ~ __ ,\\_,\\_ / _, _/ /_/ / /_/ /") + .append(LINE_SEPARATOR); + sb.append(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /_/ |_|\\____/\\____/") + .append(" ").append(versionInfo()).append(LINE_SEPARATOR); + return sb.toString(); + } + + sb.append(" ____ ____ ____ ").append(LINE_SEPARATOR); + sb.append(" / __ \\/ __ \\/ __ \\ ").append(LINE_SEPARATOR); + sb.append(" / /_/ / / / / / / / ").append(LINE_SEPARATOR); + sb.append(" / _, _/ /_/ / /_/ / ").append(LINE_SEPARATOR); + sb.append("/_/ |_|\\____/\\____/ ").append(" ").append(versionInfo()) + .append(LINE_SEPARATOR); + sb.append(LINE_SEPARATOR); + + return sb.toString(); + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java b/shell/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java new file mode 100644 index 000000000..be8ed3061 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java @@ -0,0 +1,37 @@ +package org.springframework.roo.shell; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method that can indicate whether a particular command is + * presently available or not. + *

    + * This annotation must only be applied to a public no-argument method that + * returns primitive boolean. The method should be inexpensive to evaluate, as + * this method can be called very frequently. If expensive operations are + * necessary to compute command availability, it is suggested the method return + * a boolean field that is maintained using the observer pattern. + *

    + * It is possible that a particular availability method might be able to + * represent the availability status of multiple commands. As such, an + * availability indicator annotation will indicate the commands that it applies + * to. If a specific command has multiple aliases (ie by using an array for + * {@link CliCommand#value()}), only one of the commands need to be specified in + * the {@link CliAvailabilityIndicator} annotation. + * + * @author Ben Alex + * @since 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CliAvailabilityIndicator { + + /** + * @return the name of the command or commands that this availability + * indicator represents + */ + String[] value(); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CliCommand.java b/shell/src/main/java/org/springframework/roo/shell/CliCommand.java new file mode 100644 index 000000000..b596388cc --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CliCommand.java @@ -0,0 +1,25 @@ +package org.springframework.roo.shell; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CliCommand { + + /** + * @return a help message for this command (the default is a blank String, + * which means there is no help) + */ + String help() default ""; + + /** + * @return one or more strings which must serve as the start of a particular + * command in order to match this method (these must be unique + * within the entire application; if not unique, behaviour is not + * specified) + */ + String[] value(); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CliOption.java b/shell/src/main/java/org/springframework/roo/shell/CliOption.java new file mode 100644 index 000000000..d8c89e2f4 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CliOption.java @@ -0,0 +1,78 @@ +package org.springframework.roo.shell; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface CliOption { + + // Special value that denotes a null value + static final String NULL = "__NULL__"; + // Special value that denotes an empty string + static final String EMPTY = "__EMPTY__"; + + /** + * @return a help message for this option (the default is a blank String, + * which means there is no help) + */ + String help() default ""; + + /** + * @return the name of the option, which must be unique within this + * {@link CliCommand} (an empty String may be given, which would + * denote this option is the default for the command) + */ + String[] key(); + + /** + * @return true if this option must be specified one way or the other by the + * user (defaults to false) + */ + boolean mandatory() default false; + + /** + * Returns a string providing context-specific information (e.g. a + * comma-delimited set of keywords) to the {@link Converter} that handles + * the annotated parameter's type. + *

    + * For example, if a method parameter "thing" of type "Thing" is annotated + * as follows: + * + *

    +     * @CliOption(..., optionContext = "foo,bar", ...) Thing thing
    +     * 
    + * + * ... then the {@link Converter} that converts the text entered by the user + * into an instance of Thing will be passed "foo,bar" as the value of the + * optionContext parameter in its public methods. This allows + * the behaviour of that Converter to be individually customised for each + * {@link CliOption} of each {@link CliCommand}. + * + * @return a non-null string (can be empty) + */ + String optionContext() default ""; + + /** + * @return the default value to use if this option is included by the user, + * but they didn't specify an actual value (most commonly used for + * flags; defaults to __NULL__, which causes null to be presented to + * any non-primitive parameter) + */ + String specifiedDefaultValue() default NULL; + + /** + * @return if true, the user cannot specify this option and it is provided + * by the shell infrastructure (defaults to false) + */ + boolean systemProvided() default false; + + /** + * @return the default value to use if this option is unspecified by the + * user (defaults to __NULL__, which causes null to be presented to + * any non-primitive parameter) + */ + String unspecifiedDefaultValue() default NULL; +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CliOptionContext.java b/shell/src/main/java/org/springframework/roo/shell/CliOptionContext.java new file mode 100644 index 000000000..f60de842a --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CliOptionContext.java @@ -0,0 +1,41 @@ +package org.springframework.roo.shell; + +/** + * Utility methods relating to shell option contexts + */ +public final class CliOptionContext { + + // Class fields + private static ThreadLocal optionContextHolder = new ThreadLocal(); + + /** + * Returns the option context for the current thread. + * + * @return null if none has been set + */ + public static String getOptionContext() { + return optionContextHolder.get(); + } + + /** + * Resets the option context for the current thread. + */ + public static void resetOptionContext() { + optionContextHolder.remove(); + } + + /** + * Stores the given option context for the current thread. + * + * @param optionContext the option context to store + */ + public static void setOptionContext(final String optionContext) { + optionContextHolder.set(optionContext); + } + + /** + * Constructor is private to prevent instantiation + */ + private CliOptionContext() { + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java b/shell/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java new file mode 100644 index 000000000..f2aaaa200 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java @@ -0,0 +1,29 @@ +package org.springframework.roo.shell; + +/** + * Utility methods relating to shell simple parser contexts. + */ +public final class CliSimpleParserContext { + + // Class fields + private static ThreadLocal simpleParserContextHolder = new ThreadLocal(); + + public static Parser getSimpleParserContext() { + return simpleParserContextHolder.get(); + } + + public static void resetSimpleParserContext() { + simpleParserContextHolder.remove(); + } + + public static void setSimpleParserContext( + final SimpleParser simpleParserContext) { + simpleParserContextHolder.set(simpleParserContext); + } + + /** + * Constructor is private to prevent instantiation + */ + private CliSimpleParserContext() { + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CommandListener.java b/shell/src/main/java/org/springframework/roo/shell/CommandListener.java new file mode 100644 index 000000000..9c66527b7 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CommandListener.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell; + +/** + * + * All components that need to run code before the execution of + * a command, when command completes successfully or when command + * fails, needs to implement this Interface. + * + * @author Juan Carlos García + * @since 1.3.1 + * + */ +public interface CommandListener { + + /** + * This method will be executed when some command finish + * success + */ + public void onCommandSuccess(); + + /** + * This method will be executed when some command fails + */ + public void onCommandFails(); + + /** + * This method will be executed before command execution + * + * @param parseResult + */ + public void onCommandBegin(ParseResult parseResult); + +} diff --git a/shell/src/main/java/org/springframework/roo/shell/CommandMarker.java b/shell/src/main/java/org/springframework/roo/shell/CommandMarker.java new file mode 100644 index 000000000..df8fb9de2 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/CommandMarker.java @@ -0,0 +1,7 @@ +package org.springframework.roo.shell; + +/** + * Marker interface indicating a provider of one or more shell commands. + */ +public interface CommandMarker { +} diff --git a/shell/src/main/java/org/springframework/roo/shell/Completion.java b/shell/src/main/java/org/springframework/roo/shell/Completion.java new file mode 100644 index 000000000..9504f6de2 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/Completion.java @@ -0,0 +1,95 @@ +package org.springframework.roo.shell; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.roo.support.util.AnsiEscapeCode; + +public class Completion { + + private final String formattedValue; + private final String heading; + private final int order; + private final String value; + + /** + * Constructor + * + * @param value + */ + public Completion(final String value) { + this(value, value, null, 0); + } + + /** + * Constructor + * + * @param value + * @param formattedValue + * @param heading + * @param order + */ + public Completion(final String value, final String formattedValue, + String heading, final int order) { + this.formattedValue = formattedValue; + this.order = order; + this.value = value; + if (StringUtils.isNotBlank(heading)) { + heading = AnsiEscapeCode.decorate(heading, + AnsiEscapeCode.UNDERSCORE, AnsiEscapeCode.FG_GREEN); + } + this.heading = heading; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final Completion that = (Completion) o; + if (formattedValue != null ? !formattedValue + .equals(that.formattedValue) : that.formattedValue != null) { + return false; + } + if (heading != null ? !heading.equals(that.heading) + : that.heading != null) { + return false; + } + if (value != null ? !value.equals(that.value) : that.value != null) { + return false; + } + return true; + } + + public String getFormattedValue() { + return formattedValue; + } + + public String getHeading() { + return heading; + } + + public int getOrder() { + return order; + } + + public String getValue() { + return value; + } + + @Override + public int hashCode() { + int result = value != null ? value.hashCode() : 0; + result = 31 * result + + (formattedValue != null ? formattedValue.hashCode() : 0); + result = 31 * result + (heading != null ? heading.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return order + ". " + heading + " - " + value; + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/Converter.java b/shell/src/main/java/org/springframework/roo/shell/Converter.java new file mode 100644 index 000000000..c16666d83 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/Converter.java @@ -0,0 +1,63 @@ +package org.springframework.roo.shell; + +import java.util.List; + +/** + * Converts between Strings (as displayed by and entered via the shell) and Java + * objects + * + * @author Ben Alex + * @param the type being converted to/from + */ +public interface Converter { + + /** + * Converts from the given String value to type T + * + * @param value the value to convert + * @param targetType the type being converted to; can't be null + * @param optionContext a non-null string that customises the + * behaviour of this converter for a given {@link CliOption} of a + * given {@link CliCommand}; the contents will have special + * meaning to this converter (e.g. be a comma-separated list of + * keywords known to this converter) + * @return see above + * @throws RuntimeException if the given value could not be converted + */ + T convertFromText(String value, Class targetType, String optionContext); + + /** + * Populates the given list with the possible completions + * + * @param completions the list to populate; can't be null + * @param targetType the type of parameter for which a string is being + * entered + * @param existingData what the user has typed so far + * @param optionContext a non-null string that customises the + * behaviour of this converter for a given {@link CliOption} of a + * given {@link CliCommand}; the contents will have special + * meaning to this converter (e.g. be a comma-separated list of + * keywords known to this converter) + * @param target + * @return true if all the added completions are complete + * values, or false if the user can press TAB to add + * further information to some or all of them + */ + boolean getAllPossibleValues(List completions, + Class targetType, String existingData, String optionContext, + MethodTarget target); + + /** + * Indicates whether this converter supports the given type in the given + * option context + * + * @param type the type being checked + * @param optionContext a non-null string that customises the + * behaviour of this converter for a given {@link CliOption} of a + * given {@link CliCommand}; the contents will have special + * meaning to this converter (e.g. be a comma-separated list of + * keywords known to this converter) + * @return see above + */ + boolean supports(Class type, String optionContext); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java b/shell/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java new file mode 100644 index 000000000..78b52b981 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java @@ -0,0 +1,41 @@ +package org.springframework.roo.shell; + +/** + * Strategy interface to permit the controlled execution of methods. + *

    + * This interface is used to enable a {@link Shell} to execute methods in a + * consistent, system-wide manner. A typical use case is to ensure user + * interface commands are not executed concurrently when other background + * threads are performing certain operations. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ExecutionStrategy { + + /** + * Executes the method indicated by the {@link ParseResult}. + * + * @param parseResult that should be executed (never presented as null) + * @return an object which will be rendered by the {@link Shell} + * implementation (may return null) + * @throws RuntimeException which is handled by the {@link Shell} + * implementation + */ + Object execute(ParseResult parseResult) throws RuntimeException; + + /** + * Indicates commands are able to be presented. This generally means all + * important system startup activities have completed. + * + * @return whether commands can be presented for processing at this time + */ + boolean isReadyForCommands(); + + /** + * Indicates the execution runtime should be terminated. This allows it to + * cleanup before returning control flow to the caller. Necessary for clean + * shutdowns. + */ + void terminate(); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/ExitShellRequest.java b/shell/src/main/java/org/springframework/roo/shell/ExitShellRequest.java new file mode 100644 index 000000000..d84b5a450 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/ExitShellRequest.java @@ -0,0 +1,28 @@ +package org.springframework.roo.shell; + +/** + * An immutable representation of a request to exit the shell. + *

    + * Implementations of the shell are free to handle these requests in whatever + * way they wish. Callers should not expect an exit request to be completed. + * + * @author Ben Alex + */ +public class ExitShellRequest { + + public static final ExitShellRequest FATAL_EXIT = new ExitShellRequest(1); + public static final ExitShellRequest JVM_TERMINATED_EXIT = new ExitShellRequest( + 99); // Ensure 99 is maintained in o.s.r.bootstrap.Main as it's the + // default for a null roo.exit code + public static final ExitShellRequest NORMAL_EXIT = new ExitShellRequest(0); + + private final int exitCode; + + private ExitShellRequest(final int exitCode) { + this.exitCode = exitCode; + } + + public int getExitCode() { + return exitCode; + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/MethodTarget.java b/shell/src/main/java/org/springframework/roo/shell/MethodTarget.java new file mode 100644 index 000000000..0ebf3c42a --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/MethodTarget.java @@ -0,0 +1,111 @@ +package org.springframework.roo.shell; + +import java.lang.reflect.Method; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * A method that can be executed via a shell command. + *

    + * Immutable since 1.2.0. + * + * @author Ben Alex + */ +public class MethodTarget { + + private final String key; + private final Method method; + private final String remainingBuffer; + private final Object target; + + /** + * Constructor for a null remainingBuffer and key + * + * @param method the method to invoke (required) + * @param target the object on which the method is to be invoked (required) + * @since 1.2.0 + */ + public MethodTarget(final Method method, final Object target) { + this(method, target, null, null); + } + + /** + * Constructor that allows all fields to be set + * + * @param method the method to invoke (required) + * @param target the object on which the method is to be invoked (required) + * @param remainingBuffer can be blank + * @param key can be blank + * @since 1.2.0 + */ + public MethodTarget(final Method method, final Object target, + final String remainingBuffer, final String key) { + Validate.notNull(method, "Method is required"); + Validate.notNull(target, "Target is required"); + this.key = StringUtils.stripToEmpty(key); + this.method = method; + this.remainingBuffer = StringUtils.stripToEmpty(remainingBuffer); + this.target = target; + } + + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; + } + if (!(other instanceof MethodTarget)) { + return false; + } + final MethodTarget otherMethodTarget = (MethodTarget) other; + return method.equals(otherMethodTarget.getMethod()) + && target.equals(otherMethodTarget.getTarget()); + } + + /** + * @since 1.2.0 + */ + public String getKey() { + return key; + } + + /** + * @return a non-null method + * @since 1.2.0 + */ + public Method getMethod() { + return method; + } + + /** + * @since 1.2.0 + */ + public String getRemainingBuffer() { + return remainingBuffer; + } + + /** + * @return a non-null Object + * @since 1.2.0 + */ + public Object getTarget() { + return target; + } + + @Override + public int hashCode() { + return ObjectUtils.hashCodeMulti(method, target); + } + + @Override + public final String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("target", target); + builder.append("method", method); + builder.append("remainingBuffer", remainingBuffer); + builder.append("key", key); + return builder.toString(); + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java b/shell/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java new file mode 100644 index 000000000..5a65cd550 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java @@ -0,0 +1,182 @@ +package org.springframework.roo.shell; + +import java.util.Comparator; + +/** + * NaturalOrderComparator.java -- Perform natural order comparisons of strings + * in Java. Copyright (C) 2003 by Pierre-Luc Paour Based on + * the C version by Martin Pool, of which this is more or less a straight + * conversion. Copyright (C) 2000 by Martin Pool This + * software is provided as-is, without any express or implied warranty. In no + * event will the authors be held liable for any damages arising from the use of + * this software. Permission is granted to anyone to use this software for any + * purpose, including commercial applications, and to alter it and redistribute + * it freely, subject to the following restrictions: 1. The origin of this + * software must not be misrepresented; you must not claim that you wrote the + * original software. If you use this software in a product, an acknowledgement + * in the product documentation would be appreciated but is not required. 2. + * Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. 3. This notice may not be + * removed or altered from any source distribution. + */ +public class NaturalOrderComparator implements Comparator { + + /** + * Returns the character at the given position of the given string; + * equivalent to {@link String#charAt(int)}, but handles overly large + * indices. + * + * @param s the string to read (can't be null) + * @param i the index at which to read (zero-based) + * @return 0 if the given index is beyond the end of the string + */ + static char charAt(final String s, final int i) { + if (i >= s.length()) { + return 0; + } + return s.charAt(i); + } + + /** + * Indicates whether the given character is whitespace + * + * @param c the character to check + * @return see above + */ + public static boolean isSpace(final char c) { + switch (c) { + case ' ': + return true; + case '\n': + return true; + case '\t': + return true; + case '\f': + return true; + case '\r': + return true; + default: + return false; + } + } + + public int compare(final E o1, final E o2) { + if (o1 == null && o2 == null) { + return 1; + } + + if (o1 == null) { + return 1; + } + + if (o2 == null) { + return -1; + } + + final String a = stringify(o1); + final String b = stringify(o2); + + int ia = 0, ib = 0; + int nza = 0, nzb = 0; + char ca, cb; + int result; + + while (true) { + // Only count the number of zeroes leading the last number compared + nza = nzb = 0; + + ca = charAt(a, ia); + cb = charAt(b, ib); + + // Skip over leading spaces or zeros + while (isSpace(ca) || ca == '0') { + if (ca == '0') { + nza++; + } + else { + // Only count consecutive zeroes + nza = 0; + } + + ca = charAt(a, ++ia); + } + + while (isSpace(cb) || cb == '0') { + if (cb == '0') { + nzb++; + } + else { + // Only count consecutive zeroes + nzb = 0; + } + + cb = charAt(b, ++ib); + } + + // Process run of digits + if (Character.isDigit(ca) && Character.isDigit(cb)) { + if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0) { + return result; + } + } + + if (ca == 0 && cb == 0) { + // The strings compare the same. Perhaps the caller + // will want to call strcmp to break the tie. + return nza - nzb; + } + + if (ca < cb) { + return -1; + } + else if (ca > cb) { + return +1; + } + + ++ia; + ++ib; + } + } + + int compareRight(final String a, final String b) { + int bias = 0; + int ia = 0; + int ib = 0; + + // The longest run of digits wins. That aside, the greatest + // value wins, but we can't know that it will until we've scanned + // both numbers to know that they have the same magnitude, so we + // remember it in BIAS. + for (;; ia++, ib++) { + final char ca = charAt(a, ia); + final char cb = charAt(b, ib); + + if (!Character.isDigit(ca) && !Character.isDigit(cb)) { + return bias; + } + else if (!Character.isDigit(ca)) { + return -1; + } + else if (!Character.isDigit(cb)) { + return +1; + } + else if (ca < cb) { + if (bias == 0) { + bias = -1; + } + } + else if (ca > cb) { + if (bias == 0) { + bias = +1; + } + } + else if (ca == 0 && cb == 0) { + return bias; + } + } + } + + protected String stringify(final E object) { + return object.toString(); + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/OptionContexts.java b/shell/src/main/java/org/springframework/roo/shell/OptionContexts.java new file mode 100644 index 000000000..4747adb60 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/OptionContexts.java @@ -0,0 +1,39 @@ +package org.springframework.roo.shell; + +/** + * Common constants used in the option contexts of {@link CliOption}s. + * + * @author Alan Stewart + */ +public final class OptionContexts { + + /** + * If this string appears in an option context, a {@link Converter} will + * return only interface types appearing in any module of the user's + * project. + */ + public static final String INTERFACE = "interface"; + + /** + * If this string appears in an option context, a {@link Converter} will + * return types appearing in any module of the user's project. + */ + public static final String PROJECT = "project"; + + /** + * If this string appears in an option context, this converter will return + * non-final types appearing in any module of the user's project. + */ + public static final String SUPERCLASS = "superclass"; + + /** + * If this string appears in an option context, this converter will update + * the last used type and the focused module as applicable. + */ + public static final String UPDATE = "update"; + + public static final String UPDATE_PROJECT = "update,project"; + + private OptionContexts() { + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/ParseResult.java b/shell/src/main/java/org/springframework/roo/shell/ParseResult.java new file mode 100644 index 000000000..69d927af2 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/ParseResult.java @@ -0,0 +1,104 @@ +package org.springframework.roo.shell; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Immutable representation of the outcome of parsing a given shell line. + *

    + * Note that contained objects (the instance and the arguments) may be mutable, + * as the shell infrastructure has no way of restricting which methods can be + * the target of CLI commands and nor the arguments they will accept via the + * {@link Converter} infrastructure. + * + * @author Ben Alex + * @since 1.0 + */ +public class ParseResult { + + private final Object[] arguments; // May be null if no arguments needed + private final Object instance; + private final Method method; + + public ParseResult(final Method method, final Object instance, + final Object[] arguments) { + Validate.notNull(method, "Method required"); + Validate.notNull(instance, "Instance required"); + final int length = arguments == null ? 0 : arguments.length; + Validate.isTrue(method.getParameterTypes().length == length, + "Required %d arguments, but received %d", + method.getParameterTypes().length, length); + this.method = method; + this.instance = instance; + this.arguments = arguments; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ParseResult other = (ParseResult) obj; + if (!Arrays.equals(arguments, other.arguments)) { + return false; + } + if (instance == null) { + if (other.instance != null) { + return false; + } + } + else if (!instance.equals(other.instance)) { + return false; + } + if (method == null) { + if (other.method != null) { + return false; + } + } + else if (!method.equals(other.method)) { + return false; + } + return true; + } + + public Object[] getArguments() { + return arguments; + } + + public Object getInstance() { + return instance; + } + + public Method getMethod() { + return method; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(arguments); + result = prime * result + (instance == null ? 0 : instance.hashCode()); + result = prime * result + (method == null ? 0 : method.hashCode()); + return result; + } + + @Override + public String toString() { + final ToStringBuilder builder = new ToStringBuilder(this); + builder.append("method", method); + builder.append("instance", instance); + builder.append("arguments", StringUtils.join(arguments, ",")); + return builder.toString(); + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/Parser.java b/shell/src/main/java/org/springframework/roo/shell/Parser.java new file mode 100644 index 000000000..6d52d90b0 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/Parser.java @@ -0,0 +1,36 @@ +package org.springframework.roo.shell; + +import java.util.List; + +/** + * Interface for {@link SimpleParser}. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +public interface Parser { + + /** + * Populates a list of completion candidates. This method is required for + * backward compatibility for STS versions up to 2.8.0. + * + * @param buffer + * @param cursor + * @param candidates + * @return + */ + int complete(String buffer, int cursor, List candidates); + + /** + * Populates a list of completion candidates. + * + * @param buffer + * @param cursor + * @param candidates + * @return + */ + int completeAdvanced(String buffer, int cursor, List candidates); + + ParseResult parse(String buffer); +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/ParserUtils.java b/shell/src/main/java/org/springframework/roo/shell/ParserUtils.java new file mode 100644 index 000000000..61b3c27d6 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/ParserUtils.java @@ -0,0 +1,213 @@ +package org.springframework.roo.shell; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.Validate; + +import static org.springframework.roo.shell.CliOption.*; + +/** + * Utilities for parsing. + * + * @author Ben Alex + * @since 1.0 + */ +public class ParserUtils { + + private static void store(final Map results, + final StringBuilder currentOption, final StringBuilder currentValue) { + if (currentOption.length() > 0) { + // There is an option marker + final String option = currentOption.toString(); + Validate.isTrue(!results.containsKey(option), + "You cannot specify option '" + option + + "' more than once in a single command"); + results.put(option, currentValue.toString()); + } + else { + // There was no option marker, so verify this isn't the first + Validate.isTrue( + !results.containsKey(""), + "You cannot add more than one default option ('%s') in a single command", + currentValue.toString()); + results.put("", currentValue.toString()); + } + } + + /** + * Converts a particular buffer into a tokenized structure. + *

    + * Properly treats double quotes (") as option delimiters. + *

    + * Expects option names to be preceded by a single or double dash. We call + * this an "option marker". + *

    + * Treats spaces as the default option tokenizer. + *

    + * Any token without an option marker is considered the default. The default + * is returned in the Map as an element with an empty string key (""). There + * can only be a single default. + * + * @param remainingBuffer to tokenize + * @return a Map where keys are the option names (minus any dashes) and + * values are the option values (any double-quotes are removed) + */ + public static Map tokenize(final String remainingBuffer) { + Validate.notNull(remainingBuffer, + "Remaining buffer cannot be null, although it can be empty"); + final Map result = new LinkedHashMap(); + StringBuilder currentOption = new StringBuilder(); + StringBuilder currentValue = new StringBuilder(); + boolean inQuotes = false; + + // Verify correct number of double quotes are present + int count = 0; + for (final char c : remainingBuffer.toCharArray()) { + if ('"' == c) { + count++; + } + } + Validate.isTrue(count % 2 == 0, + "Cannot have an unbalanced number of quotation marks"); + + if ("".equals(remainingBuffer.trim())) { + // They've not specified anything, so exit now + return result; + } + + final String[] split = remainingBuffer.split(" "); + for (int i = 0; i < split.length; i++) { + final String currentToken = split[i]; + + if (currentToken.startsWith("\"") && currentToken.endsWith("\"") + && currentToken.length() > 1) { + final String tokenLessDelimiters = currentToken.substring(1, + currentToken.length() - 1); + currentValue.append(tokenLessDelimiters); + + // If the current value is an empty string that means the + // user has explicitly set it as such so mark it as empty + // so that it doesn't get replaced by null or a default + // value during parsing. + if ("".equals(currentValue.toString())) { + currentValue.append(EMPTY); + } + + // Store this token + store(result, currentOption, currentValue); + currentOption = new StringBuilder(); + currentValue = new StringBuilder(); + continue; + } + + if (inQuotes) { + // We're only interested in this token series ending + if (currentToken.endsWith("\"")) { + final String tokenLessDelimiters = currentToken.substring( + 0, currentToken.length() - 1); + currentValue.append(" ").append(tokenLessDelimiters); + inQuotes = false; + + // Store this now-ended token series + store(result, currentOption, currentValue); + currentOption = new StringBuilder(); + currentValue = new StringBuilder(); + } + else { + // The current token series has not ended + currentValue.append(" ").append(currentToken); + } + continue; + } + + if (currentToken.startsWith("\"")) { + // We're about to start a new delimited token + final String tokenLessDelimiters = currentToken.substring(1); + currentValue.append(tokenLessDelimiters); + inQuotes = true; + continue; + } + + if (currentToken.trim().equals("")) { + // It's simply empty, so ignore it (ROO-23) + continue; + } + + if (currentToken.startsWith("--")) { + // We're about to start a new option marker + // First strip all of the - or -- or however many there are + final int lastIndex = currentToken.lastIndexOf("-"); + final String tokenLessDelimiters = currentToken + .substring(lastIndex + 1); + currentOption.append(tokenLessDelimiters); + + // Store this token if it's the last one, or the next token + // starts with a "-" + if (i + 1 == split.length) { + // We're at the end of the tokens, so store this one and + // stop processing + store(result, currentOption, currentValue); + break; + } + + if (split[i + 1].startsWith("-")) { + // A new token is being started next iteration, so store + // this one now + store(result, currentOption, currentValue); + currentOption = new StringBuilder(); + currentValue = new StringBuilder(); + } + + continue; + } + + // We must be in a standard token + + // If the standard token has no option name, we allow it to contain + // unquoted spaces + if (currentOption.length() == 0) { + if (currentValue.length() > 0) { + // Existing content, so add a space first + currentValue.append(" "); + } + currentValue.append(currentToken); + + // Store this token if it's the last one, or the next token + // starts with a "-" + if (i + 1 == split.length) { + // We're at the end of the tokens, so store this one and + // stop processing + store(result, currentOption, currentValue); + break; + } + + if (split[i + 1].startsWith("--")) { + // A new token is being started next iteration, so store + // this one now + store(result, currentOption, currentValue); + currentOption = new StringBuilder(); + currentValue = new StringBuilder(); + } + + continue; + } + + // This is an ordinary token, so store it now + currentValue.append(currentToken); + store(result, currentOption, currentValue); + currentOption = new StringBuilder(); + currentValue = new StringBuilder(); + } + + // Strip out an empty default option, if it was returned (ROO-379) + if (result.containsKey("") && result.get("").trim().equals("")) { + result.remove(""); + } + + return result; + } + + private ParserUtils() { + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/Shell.java b/shell/src/main/java/org/springframework/roo/shell/Shell.java new file mode 100644 index 000000000..9803aad12 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/Shell.java @@ -0,0 +1,129 @@ +package org.springframework.roo.shell; + +import java.io.File; +import java.util.logging.Level; + +import org.springframework.roo.shell.event.ShellStatusProvider; + +/** + * Specifies the contract for an interactive shell. + *

    + * Any interactive shell class which implements these methods can be launched by + * the roo-bootstrap mechanism. + *

    + * It is envisaged implementations will be provided for JLine initially, with + * possible implementations for Eclipse in the future. + * + * @author Ben Alex + * @since 1.0 + */ +public interface Shell extends ShellStatusProvider, ShellPromptAccessor { + + /** + * The slot name to use with {@link #flash(Level, String, String)} if a + * caller wishes to modify the window title. This may not be supported by + * all operating system shells. It is provided on a best-effort basis only. + */ + String WINDOW_TITLE_SLOT = "WINDOW_TITLE_SLOT"; + + /** + * Runs the specified command. Control will return to the caller after the + * command is run. + * + * @param line to execute (required) + * @return true if the command was successful, false if there was an + * exception + */ + boolean executeCommand(String line); + + /** + * Displays a progress notification to the user. This notification will + * ideally be displayed in a consistent screen location by the shell + * implementation. + *

    + * An implementation may allow multiple messages to be displayed + * concurrently. So an implementation can determine when a flash message + * replaces a previous flash message, callers should allocate a unique + * "slot" name for their messages. It is suggested the class name of the + * caller be used. This way a slot will be updated without conflicting with + * flash message sequences from other slots. + *

    + * Passing an empty string in as the "message" indicates the slot should be + * cleared. + *

    + * An implementation need not necessarily use the level or slot concepts. + * They are expected to be used in most cases, though. + * + * @param level the importance of the message (cannot be null) + * @param message to display (cannot be null, but may be empty) + * @param slot the identification slot for the message (cannot be null or + * empty) + */ + void flash(Level level, String message, String slot); + + /** + * @return null if no exit was requested, otherwise the last exit code + * indicated to the shell to use + */ + ExitShellRequest getExitShellRequest(); + + /** + * Returns the home directory of the current running shell instance + * + * @return the home directory of the current shell instance + */ + File getHome(); + + boolean isDevelopmentMode(); + + /** + * Presents a console prompt and allows the user to interact with the shell. + * The shell should not return to the caller until the user has finished + * their session (by way of a "quit" or similar command). + */ + void promptLoop(); + + /** + * Indicates the shell should switch into a lower-level development mode. + * The exact meaning varies by shell implementation. + * + * @param developmentMode true if development mode should be enabled, false + * otherwise + */ + void setDevelopmentMode(boolean developmentMode); + + /** + * Changes the "path" displayed in the shell prompt. An implementation will + * ensure this path is included on the screen, taking care to merge it with + * the product name and handle any special formatting requirements (such as + * ANSI, if supported by the implementation). + * + * @param path to set (can be null or empty; must NOT be formatted in any + * special way eg ANSI codes) + */ + void setPromptPath(String path); + + void setPromptPath(String path, boolean overrideStyle); + + /** + * To support API compatibility with STS shell, tailor implementation should + * be injected explicitly when activated. + * + * @param tailor the tailor implementation + */ + void setTailor(Tailor tailor); + + /** + * To add new CommandListener on Shell object, use addListener method. + * + * @param listener + */ + void addListerner(CommandListener listener); + + /** + * To remove CommandListener on Shell object, use removeListener method + * + * @param listener + */ + void removeListener(CommandListener listener); +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java b/shell/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java new file mode 100644 index 000000000..de088e63a --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java @@ -0,0 +1,17 @@ +package org.springframework.roo.shell; + +/** + * Obtains the prompt used by a {@link Shell}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ShellPromptAccessor { + + /** + * @return the shell prompt (never null; the result may include special + * characters such as ANSI escape codes if the implementation is + * using them) + */ + String getShellPrompt(); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/SimpleParser.java b/shell/src/main/java/org/springframework/roo/shell/SimpleParser.java new file mode 100644 index 000000000..47b4c0dab --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/SimpleParser.java @@ -0,0 +1,1150 @@ +package org.springframework.roo.shell; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; +import static org.springframework.roo.shell.CliOption.EMPTY; +import static org.springframework.roo.shell.CliOption.NULL; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.Transformer; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.XmlElementBuilder; +import org.springframework.roo.support.util.XmlUtils; +import org.w3c.dom.CDATASection; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.CollectionUtils; + +/** + * Default implementation of {@link Parser}. + * + * @author Ben Alex + * @since 1.0 + */ +public class SimpleParser implements Parser { + + // ------------ OSGi component attributes ---------------- + public BundleContext context; + + private static final Comparator COMPARATOR = new NaturalOrderComparator(); + private static final Logger LOGGER = HandlerUtils + .getLogger(SimpleParser.class); + + static String isMatch(final String buffer, final String command, + final boolean strictMatching) { + if ("".equals(buffer.trim())) { + return ""; + } + final String[] commandWords = StringUtils.split(command, " "); + int lastCommandWordUsed = 0; + Validate.notEmpty(commandWords, "Command required"); + + String bufferToReturn = null; + String lastWord = null; + + next_buffer_loop: for (int bufferIndex = 0; bufferIndex < buffer + .length(); bufferIndex++) { + final String bufferSoFarIncludingThis = buffer.substring(0, + bufferIndex + 1); + final String bufferRemaining = buffer.substring(bufferIndex + 1); + + final int bufferLastIndexOfWord = bufferSoFarIncludingThis + .lastIndexOf(" "); + String wordSoFarIncludingThis = bufferSoFarIncludingThis; + if (bufferLastIndexOfWord != -1) { + wordSoFarIncludingThis = bufferSoFarIncludingThis + .substring(bufferLastIndexOfWord); + } + + if (wordSoFarIncludingThis.equals(" ") + || bufferIndex == buffer.length() - 1) { + if (bufferIndex == buffer.length() - 1 + && !"".equals(wordSoFarIncludingThis.trim())) { + lastWord = wordSoFarIncludingThis.trim(); + } + + // At end of word or buffer. Let's see if a word matched or not + for (int candidate = lastCommandWordUsed; candidate < commandWords.length; candidate++) { + if (lastWord != null && lastWord.length() > 0 + && commandWords[candidate].startsWith(lastWord)) { + if (bufferToReturn == null) { + // This is the first match, so ensure the intended + // match really represents the start of a command + // and not a later word within it + if (lastCommandWordUsed == 0 && candidate > 0) { + // This is not a valid match + break next_buffer_loop; + } + } + + if (bufferToReturn != null) { + // We already matched something earlier, so ensure + // we didn't skip any word + if (candidate != lastCommandWordUsed + 1) { + // User has skipped a word + bufferToReturn = null; + break next_buffer_loop; + } + } + + bufferToReturn = bufferRemaining; + lastCommandWordUsed = candidate; + if (candidate + 1 == commandWords.length) { + // This was a match for the final word in the + // command, so abort + break next_buffer_loop; + } + // There are more words left to potentially match, so + // continue + continue next_buffer_loop; + } + } + + // This word is unrecognised as part of a command, so abort + bufferToReturn = null; + break next_buffer_loop; + } + + lastWord = wordSoFarIncludingThis.trim(); + } + + // We only consider it a match if ALL words were actually used + if (bufferToReturn != null) { + if (!strictMatching + || lastCommandWordUsed + 1 == commandWords.length) { + return bufferToReturn; + } + } + + return null; // Not a match + } + + private final Map availabilityIndicators = new HashMap(); + private final Set commands = new HashSet(); + private final Set> converters = new HashSet>(); + + private final Object mutex = new Object(); + + public final void add(final CommandMarker command) { + synchronized (mutex) { + commands.add(command); + for (final Method method : command.getClass().getMethods()) { + final CliAvailabilityIndicator availability = method + .getAnnotation(CliAvailabilityIndicator.class); + if (availability != null) { + Validate.isTrue( + method.getParameterTypes().length == 0, + "CliAvailabilityIndicator is only legal for 0 parameter methods ('%s')", + method.toGenericString()); + Validate.isTrue( + method.getReturnType().equals(Boolean.TYPE), + "CliAvailabilityIndicator is only legal for primitive boolean return types (%s)", + method.toGenericString()); + for (final String cmd : availability.value()) { + Validate.isTrue( + !availabilityIndicators.containsKey(cmd), + "Cannot specify an availability indicator for '%s' more than once", + cmd); + availabilityIndicators.put(cmd, new MethodTarget( + method, command)); + } + } + } + } + } + + public final void add(final Converter converter) { + synchronized (mutex) { + converters.add(converter); + } + } + + protected void commandNotFound(final Logger logger, final String buffer) { + logger.warning("Command '" + buffer + + "' not found (for assistance press " + + AbstractShell.completionKeys + + " or type \"hint\" then hit ENTER)"); + } + + public int complete(final String buffer, final int cursor, + final List candidates) { + final List completions = new ArrayList(); + final int result = completeAdvanced(buffer, cursor, completions); + for (final Completion completion : completions) { + candidates.add(completion.getValue()); + } + return result; + } + + public int completeAdvanced(String buffer, int cursor, + final List candidates) { + synchronized (mutex) { + + // Loading converters if needed + loadConverters(); + + Validate.notNull(buffer, "Buffer required"); + Validate.notNull(candidates, "Candidates list required"); + + // Remove all spaces from beginning of command + while (buffer.startsWith(" ")) { + buffer = buffer.replaceFirst("^ ", ""); + cursor--; + } + + // Replace all multiple spaces with a single space + while (buffer.contains(" ")) { + buffer = StringUtils.replace(buffer, " ", " ", 1); + cursor--; + } + + // Begin by only including the portion of the buffer represented to + // the present cursor position + final String translated = buffer.substring(0, cursor); + + // Start by locating a method that matches + final Collection targets = locateTargets(translated, + false, true); + final SortedSet results = new TreeSet( + COMPARATOR); + + if (targets.isEmpty()) { + // Nothing matches the buffer they've presented + return cursor; + } + if (targets.size() > 1) { + // Assist them locate a particular target + for (final MethodTarget target : targets) { + // Calculate the correct starting position + final int startAt = translated.length(); + + // Only add the first word of each target + int stopAt = target.getKey().indexOf(" ", startAt); + if (stopAt == -1) { + stopAt = target.getKey().length(); + } + + results.add(new Completion(target.getKey().substring(0, + stopAt) + + " ")); + } + candidates.addAll(results); + return 0; + } + + // There is a single target of this method, so provide completion + // services for it + final MethodTarget methodTarget = targets.iterator().next(); + + // Identify the command we're working with + final CliCommand cmd = methodTarget.getMethod().getAnnotation( + CliCommand.class); + Validate.notNull(cmd, "CliCommand unavailable for '%s'", + methodTarget.getMethod().toGenericString()); + + // Make a reasonable attempt at parsing the remainingBuffer + Map options; + try { + options = ParserUtils.tokenize(methodTarget + .getRemainingBuffer()); + } + catch (final IllegalArgumentException ex) { + // Assume any IllegalArgumentException is due to a quotation + // mark mismatch + candidates.add(new Completion(translated + "\"")); + return 0; + } + + // Lookup arguments for this target + final Annotation[][] parameterAnnotations = methodTarget + .getMethod().getParameterAnnotations(); + + // If there aren't any parameters for the method, at least ensure + // they have typed the command properly + if (parameterAnnotations.length == 0) { + for (final String value : cmd.value()) { + if (buffer.startsWith(value) || value.startsWith(buffer)) { + // No space at the end, as there's no need to continue + // the command further + results.add(new Completion(value)); + } + } + candidates.addAll(results); + return 0; + } + + // If they haven't specified any parameters yet, at least verify the + // command name is fully completed + if (options.isEmpty()) { + for (final String value : cmd.value()) { + if (value.startsWith(buffer)) { + // They are potentially trying to type this command + // We only need provide completion, though, if they + // failed to specify it fully + if (!buffer.startsWith(value)) { + // They failed to specify the command fully + results.add(new Completion(value + " ")); + } + } + } + + // Only quit right now if they have to finish specifying the + // command name + if (results.size() > 0) { + candidates.addAll(results); + return 0; + } + } + + // To get this far, we know there are arguments required for this + // CliCommand, and they specified a valid command name + + // Record all the CliOptions applicable to this command + final List cliOptions = new ArrayList(); + for (final Annotation[] annotations : parameterAnnotations) { + CliOption cliOption = null; + for (final Annotation a : annotations) { + if (a instanceof CliOption) { + cliOption = (CliOption) a; + } + } + Validate.notNull(cliOption, + "CliOption not found for parameter '%s'", + Arrays.toString(annotations)); + cliOptions.add(cliOption); + } + + // Make a list of all CliOptions they've already included or are + // system-provided + final List alreadySpecified = new ArrayList(); + for (final CliOption option : cliOptions) { + for (final String value : option.key()) { + if (options.containsKey(value)) { + alreadySpecified.add(option); + break; + } + } + if (option.systemProvided()) { + alreadySpecified.add(option); + } + } + + // Make a list of all CliOptions they have not provided + final List unspecified = new ArrayList( + cliOptions); + unspecified.removeAll(alreadySpecified); + + // Determine whether they're presently editing an option key or an + // option value + // (and if possible, the full or partial name of the said option key + // being edited) + String lastOptionKey = null; + String lastOptionValue = null; + + // The last item in the options map is *always* the option key + // they're editing (will never be null) + if (options.size() > 0) { + lastOptionKey = new ArrayList(options.keySet()) + .get(options.keySet().size() - 1); + lastOptionValue = options.get(lastOptionKey); + } + + // Handle if they are trying to find out the available option keys; + // always present option keys in order + // of their declaration on the method signature, thus we can stop + // when mandatory options are filled in + if (methodTarget.getRemainingBuffer().endsWith("--")) { + boolean showAllRemaining = true; + for (final CliOption include : unspecified) { + if (include.mandatory()) { + showAllRemaining = false; + break; + } + } + + for (final CliOption include : unspecified) { + for (final String value : include.key()) { + if (!"".equals(value)) { + results.add(new Completion(translated + value + " ")); + } + } + if (!showAllRemaining) { + break; + } + } + candidates.addAll(results); + return 0; + } + + // Handle suggesting an option key if they haven't got one presently + // specified (or they've completed a full option key/value pair) + if (lastOptionKey == null || !"".equals(lastOptionKey) + && !"".equals(lastOptionValue) && translated.endsWith(" ")) { + // We have either NEVER specified an option key/value pair + // OR we have specified a full option key/value pair + + // Let's list some other options the user might want to try + // (naturally skip the "" option, as that's the default) + for (final CliOption include : unspecified) { + for (final String value : include.key()) { + // Manually determine if this non-mandatory but + // unspecifiedDefaultValue=* requiring option is able to + // be bound + if (!include.mandatory() + && "*".equals(include.unspecifiedDefaultValue()) + && !"".equals(value)) { + try { + for (final Converter candidate : converters) { + // Find the target parameter + Class paramType = null; + int index = -1; + for (final Annotation[] a : methodTarget + .getMethod() + .getParameterAnnotations()) { + index++; + for (final Annotation an : a) { + if (an instanceof CliOption) { + if (an.equals(include)) { + // Found the parameter, so + // store it + paramType = methodTarget + .getMethod() + .getParameterTypes()[index]; + break; + } + } + } + } + if (paramType != null + && candidate.supports(paramType, + include.optionContext())) { + // Try to invoke this usable converter + candidate.convertFromText("*", + paramType, + include.optionContext()); + // If we got this far, the converter is + // happy with "*" so we need not bother + // the user with entering the data in + // themselves + break; + } + } + } + catch (final RuntimeException notYetReady) { + if (translated.endsWith(" ")) { + results.add(new Completion(translated + + "--" + value + " ")); + } + else { + results.add(new Completion(translated + + " --" + value + " ")); + } + continue; + } + } + + // Handle normal mandatory options + if (!"".equals(value) && include.mandatory()) { + if (translated.endsWith(" ")) { + results.add(new Completion(translated + "--" + + value + " ")); + } + else { + results.add(new Completion(translated + " --" + + value + " ")); + } + } + } + } + + // Only abort at this point if we have some suggestions; + // otherwise we might want to try to complete the "" option + if (results.size() > 0) { + candidates.addAll(results); + return 0; + } + } + + // Handle completing the option key they're presently typing + if ((lastOptionValue == null || "".equals(lastOptionValue)) + && !translated.endsWith(" ")) { + // Given we haven't got an option value of any form, and there's + // no space at the buffer end, we must still be typing an option + // key + + for (final CliOption option : cliOptions) { + for (final String value : option.key()) { + if (value != null + && lastOptionKey != null + && value.regionMatches(true, 0, lastOptionKey, + 0, lastOptionKey.length())) { + final String completionValue = translated + .substring(0, translated.length() + - lastOptionKey.length()) + + value + " "; + results.add(new Completion(completionValue)); + } + } + } + candidates.addAll(results); + return 0; + } + + // To be here, we are NOT typing an option key (or we might be, and + // there are no further option keys left) + if (lastOptionKey != null && !"".equals(lastOptionKey)) { + // Lookup the relevant CliOption that applies to this + // lastOptionKey + // We do this via the parameter type + final Class[] parameterTypes = methodTarget.getMethod() + .getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + final CliOption option = cliOptions.get(i); + final Class parameterType = parameterTypes[i]; + + for (final String key : option.key()) { + if (key.equals(lastOptionKey)) { + final List allValues = new ArrayList(); + String suffix = " "; + + // Let's use a Converter if one is available + for (final Converter candidate : converters) { + if (candidate.supports(parameterType, + option.optionContext())) { + // Found a usable converter + final boolean addSpace = candidate + .getAllPossibleValues(allValues, + parameterType, + lastOptionValue, + option.optionContext(), + methodTarget); + if (!addSpace) { + suffix = ""; + } + break; + } + } + + if (allValues.isEmpty()) { + // Doesn't appear to be a custom Converter, so + // let's go and provide defaults for simple + // types + + // Provide some simple options for common types + if (Boolean.class + .isAssignableFrom(parameterType) + || Boolean.TYPE + .isAssignableFrom(parameterType)) { + allValues.add(new Completion("true")); + allValues.add(new Completion("false")); + } + + if (Number.class + .isAssignableFrom(parameterType)) { + allValues.add(new Completion("0")); + allValues.add(new Completion("1")); + allValues.add(new Completion("2")); + allValues.add(new Completion("3")); + allValues.add(new Completion("4")); + allValues.add(new Completion("5")); + allValues.add(new Completion("6")); + allValues.add(new Completion("7")); + allValues.add(new Completion("8")); + allValues.add(new Completion("9")); + } + } + + String prefix = ""; + if (!translated.endsWith(" ")) { + prefix = " "; + } + + // Only include in the candidates those results + // which are compatible with the present buffer + for (final Completion currentValue : allValues) { + // We only provide a suggestion if the + // lastOptionValue == "" + if (StringUtils.isBlank(lastOptionValue)) { + // We should add the result, as they haven't + // typed anything yet + results.add(new Completion(prefix + + currentValue.getValue() + suffix, + currentValue.getFormattedValue(), + currentValue.getHeading(), + currentValue.getOrder())); + } + else { + // Only add the result **if** what they've + // typed is compatible *AND* they haven't + // already typed it in full + if (currentValue + .getValue() + .toLowerCase() + .startsWith( + lastOptionValue + .toLowerCase()) + && !lastOptionValue + .equalsIgnoreCase(currentValue + .getValue()) + && lastOptionValue.length() < currentValue + .getValue().length()) { + results.add(new Completion(prefix + + currentValue.getValue() + + suffix, currentValue + .getFormattedValue(), + currentValue.getHeading(), + currentValue.getOrder())); + } + } + } + + // ROO-389: give inline options given there's + // multiple choices available and we want to help + // the user + final StringBuilder help = new StringBuilder(); + help.append(LINE_SEPARATOR); + help.append(option.mandatory() ? "required --" + : "optional --"); + if ("".equals(option.help())) { + help.append(lastOptionKey).append(": ") + .append("No help available"); + } + else { + help.append(lastOptionKey).append(": ") + .append(option.help()); + } + if (option.specifiedDefaultValue().equals( + option.unspecifiedDefaultValue())) { + if (option.specifiedDefaultValue().equals( + NULL)) { + help.append("; no default value"); + } + else { + help.append("; default: '") + .append(option + .specifiedDefaultValue()) + .append("'"); + } + } + else { + if (!"".equals(option.specifiedDefaultValue()) + && !NULL.equals(option + .specifiedDefaultValue())) { + help.append( + "; default if option present: '") + .append(option + .specifiedDefaultValue()) + .append("'"); + } + if (!"".equals(option.unspecifiedDefaultValue()) + && !NULL.equals(option + .unspecifiedDefaultValue())) { + help.append( + "; default if option not present: '") + .append(option + .unspecifiedDefaultValue()) + .append("'"); + } + } + LOGGER.info(help.toString()); + + if (results.size() == 1) { + final String suggestion = results.iterator() + .next().getValue().trim(); + if (suggestion.equals(lastOptionValue)) { + // They have pressed TAB in the default + // value, and the default value has already + // been provided as an explicit option + return 0; + } + } + + if (results.size() > 0) { + candidates.addAll(results); + // Values presented from the last space onwards + if (translated.endsWith(" ")) { + return translated.lastIndexOf(" ") + 1; + } + return translated.trim().lastIndexOf(" "); + } + return 0; + } + } + } + } + + return 0; + } + } + + private MethodTarget getAvailabilityIndicator(final String command) { + return availabilityIndicators.get(command); + } + + private Set getCliOptions( + final Annotation[][] parameterAnnotations) { + final Set cliOptions = new LinkedHashSet(); + for (final Annotation[] annotations : parameterAnnotations) { + for (final Annotation annotation : annotations) { + if (annotation instanceof CliOption) { + final CliOption cliOption = (CliOption) annotation; + cliOptions.add(cliOption); + } + } + } + return cliOptions; + } + + public Set getEveryCommand() { + synchronized (mutex) { + + if(commands.isEmpty()){ + // Get all Services implement CommandMarker interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(CommandMarker.class.getName(), null); + + for(ServiceReference ref : references){ + add((CommandMarker) this.context.getService(ref)); + } + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CommandMarker on SimpleParser."); + } + } + + + final SortedSet result = new TreeSet(COMPARATOR); + for (final Object o : commands) { + final Method[] methods = o.getClass().getMethods(); + for (final Method m : methods) { + final CliCommand cmd = m.getAnnotation(CliCommand.class); + if (cmd != null) { + result.addAll(Arrays.asList(cmd.value())); + } + } + } + return result; + } + } + + private Set getSpecifiedUnavailableOptions( + final Set cliOptions, final Map options) { + final Set cliOptionKeySet = new LinkedHashSet(); + for (final CliOption cliOption : cliOptions) { + for (final String key : cliOption.key()) { + cliOptionKeySet.add(key.toLowerCase()); + } + } + final Set unavailableOptions = new LinkedHashSet(); + for (final String suppliedOption : options.keySet()) { + if (!cliOptionKeySet.contains(suppliedOption.toLowerCase())) { + unavailableOptions.add(suppliedOption); + } + } + return unavailableOptions; + } + + private Collection locateTargets(final String buffer, + final boolean strictMatching, + final boolean checkAvailabilityIndicators) { + + if(commands.isEmpty()){ + // Get all Services implement CommandMarker interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(CommandMarker.class.getName(), null); + + for(ServiceReference ref : references){ + add((CommandMarker) this.context.getService(ref)); + } + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load CommandMarker on SimpleParser."); + } + } + + Validate.notNull(buffer, "Buffer required"); + final Collection result = new HashSet(); + + // The reflection could certainly be optimised, but it's good enough for + // now (and cached reflection + // is unlikely to be noticeable to a human being using the CLI) + for (final CommandMarker command : commands) { + for (final Method method : command.getClass().getMethods()) { + final CliCommand cmd = method.getAnnotation(CliCommand.class); + if (cmd != null) { + // We have a @CliCommand. + if (checkAvailabilityIndicators) { + // Decide if this @CliCommand is available at this + // moment + Boolean available = null; + for (final String value : cmd.value()) { + final MethodTarget mt = getAvailabilityIndicator(value); + if (mt != null) { + Validate.isTrue(available == null, + "More than one availability indicator is defined for '" + + method.toGenericString() + + "'"); + try { + available = (Boolean) mt.getMethod() + .invoke(mt.getTarget()); + // We should "break" here, but we loop over + // all to ensure no conflicting availability + // indicators are defined + } + catch (final Exception e) { + available = false; + } + } + } + // Skip this @CliCommand if it's not available + if (available != null && !available) { + continue; + } + } + + for (final String value : cmd.value()) { + final String remainingBuffer = isMatch(buffer, value, + strictMatching); + if (remainingBuffer != null) { + result.add(new MethodTarget(method, command, + remainingBuffer, value)); + } + } + } + } + } + return result; + } + + /** + * Normalises the given raw user input string ready for parsing + * + * @param rawInput the string to normalise; can't be null + * @return a non-null string + */ + String normalise(final String rawInput) { + // Replace all multiple spaces with a single space and then trim + return rawInput.replaceAll(" +", " ").trim(); + } + + public ParseResult parse(final String rawInput) { + synchronized (mutex) { + + // Load converters if needed + loadConverters(); + + Validate.notNull(rawInput, "Raw input required"); + final String input = normalise(rawInput); + + // Locate the applicable targets which match this buffer + final Collection matchingTargets = locateTargets( + input, true, true); + if (matchingTargets.isEmpty()) { + // Before we just give up, let's see if we can offer a more + // informative message to the user + // by seeing the command is simply unavailable at this point in + // time + CollectionUtils.populate(matchingTargets, + locateTargets(input, true, false)); + if (matchingTargets.isEmpty()) { + commandNotFound(LOGGER, input); + } + else { + LOGGER.warning("Command '" + + input + + "' was found but is not currently available (type 'help' then ENTER to learn about this command)"); + } + return null; + } + if (matchingTargets.size() > 1) { + LOGGER.warning("Ambigious command '" + input + + "' (for assistance press " + + AbstractShell.completionKeys + + " or type \"hint\" then hit ENTER)"); + return null; + } + final MethodTarget methodTarget = matchingTargets.iterator().next(); + + // Argument conversion time + final Annotation[][] parameterAnnotations = methodTarget + .getMethod().getParameterAnnotations(); + if (parameterAnnotations.length == 0) { + // No args + return new ParseResult(methodTarget.getMethod(), + methodTarget.getTarget(), null); + } + + // Oh well, we need to convert some arguments + final List arguments = new ArrayList(methodTarget + .getMethod().getParameterTypes().length); + + // Attempt to parse + Map options = null; + try { + options = ParserUtils.tokenize(methodTarget + .getRemainingBuffer()); + } + catch (final IllegalArgumentException e) { + LOGGER.warning(StringUtils.defaultIfBlank( + ExceptionUtils.getRootCauseMessage(e), e.getMessage())); + return null; + } + + final Set cliOptions = getCliOptions(parameterAnnotations); + for (final CliOption cliOption : cliOptions) { + final Class requiredType = methodTarget.getMethod() + .getParameterTypes()[arguments.size()]; + + if (cliOption.systemProvided()) { + Object result; + if (SimpleParser.class.isAssignableFrom(requiredType)) { + result = this; + } + else { + LOGGER.warning("Parameter type '" + requiredType + + "' is not system provided"); + return null; + } + arguments.add(result); + continue; + } + + // Obtain the value the user specified, taking care to ensure + // they only specified it via a single alias + String value = null; + String sourcedFrom = null; + for (final String possibleKey : cliOption.key()) { + if (options.containsKey(possibleKey)) { + if (sourcedFrom != null) { + LOGGER.warning("You cannot specify option '" + + possibleKey + + "' when you have also specified '" + + sourcedFrom + "' in the same command"); + return null; + } + sourcedFrom = possibleKey; + value = options.get(possibleKey); + } + } + + // Ensure the user specified a value if the value is mandatory + if (StringUtils.isBlank(value) && cliOption.mandatory()) { + if ("".equals(cliOption.key()[0])) { + final StringBuilder message = new StringBuilder( + "You must specify a default option "); + if (cliOption.key().length > 1) { + message.append("(otherwise known as option '") + .append(cliOption.key()[1]).append("') "); + } + message.append("for this command"); + LOGGER.warning(message.toString()); + } + else { + LOGGER.warning("You must specify option '" + + cliOption.key()[0] + "' for this command"); + } + return null; + } + + // Accept a default if the user specified the option, but didn't + // provide a value + if ("".equals(value)) { + value = cliOption.specifiedDefaultValue(); + } + + // Accept a default if the user didn't specify the option at all + if (value == null) { + value = cliOption.unspecifiedDefaultValue(); + } + + // Special token that denotes a null value is sought (useful for + // default values) + if (NULL.equals(value)) { + if (requiredType.isPrimitive()) { + LOGGER.warning("Nulls cannot be presented to primitive type " + + requiredType.getSimpleName() + + " for option '" + + StringUtils.join(cliOption.key(), ",") + "'"); + return null; + } + arguments.add(null); + continue; + } + + // Change the empty string marker back into an empty string now + // that we are passed the default and null value checks. + if (EMPTY.equals(value)) { + value = ""; + } + + // Now we're ready to perform a conversion + try { + CliOptionContext + .setOptionContext(cliOption.optionContext()); + CliSimpleParserContext.setSimpleParserContext(this); + Object result; + Converter c = null; + for (final Converter candidate : converters) { + if (candidate.supports(requiredType, + cliOption.optionContext())) { + // Found a usable converter + c = candidate; + break; + } + } + if (c == null) { + throw new IllegalStateException( + "TODO: Add basic type conversion"); + // TODO Fall back to a normal SimpleTypeConverter and + // attempt conversion + // SimpleTypeConverter simpleTypeConverter = new + // SimpleTypeConverter(); + // result = + // simpleTypeConverter.convertIfNecessary(value, + // requiredType, mp); + } + + // Use the converter + result = c.convertFromText(value, requiredType, + cliOption.optionContext()); + + // If the option has been specified to be mandatory then the + // result should never be null + if (result == null && cliOption.mandatory()) { + throw new IllegalStateException(); + } + arguments.add(result); + } + catch (final RuntimeException e) { + LOGGER.warning(e.getClass().getName() + + ": Failed to convert '" + value + "' to type " + + requiredType.getSimpleName() + " for option '" + + StringUtils.join(cliOption.key(), ",") + "'"); + if (StringUtils.isNotBlank(e.getMessage())) { + LOGGER.warning(e.getMessage()); + } + return null; + } + finally { + CliOptionContext.resetOptionContext(); + CliSimpleParserContext.resetSimpleParserContext(); + } + } + + // Check for options specified by the user but are unavailable for + // the command + final Set unavailableOptions = getSpecifiedUnavailableOptions( + cliOptions, options); + if (!unavailableOptions.isEmpty()) { + final StringBuilder message = new StringBuilder(); + if (unavailableOptions.size() == 1) { + message.append("Option '") + .append(unavailableOptions.iterator().next()) + .append("' is not available for this command. "); + } + else { + message.append("Options ") + .append(collectionToDelimitedString( + unavailableOptions, ", ", "'", "'")) + .append(" are not available for this command. "); + } + message.append("Use tab assist or the \"help\" command to see the legal options"); + LOGGER.warning(message.toString()); + return null; + } + + return new ParseResult(methodTarget.getMethod(), + methodTarget.getTarget(), arguments.toArray()); + } + } + + private String collectionToDelimitedString(final Collection coll, + final String delim, final String prefix, final String suffix) { + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + final Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext() && delim != null) { + sb.append(delim); + } + } + return sb.toString(); + } + + public final void remove(final CommandMarker command) { + synchronized (mutex) { + commands.remove(command); + for (final Method m : command.getClass().getMethods()) { + final CliAvailabilityIndicator availability = m + .getAnnotation(CliAvailabilityIndicator.class); + if (availability != null) { + for (final String cmd : availability.value()) { + availabilityIndicators.remove(cmd); + } + } + } + } + } + + public final void remove(final Converter converter) { + synchronized (mutex) { + converters.remove(converter); + } + } + + public final void loadConverters(){ + if(converters.isEmpty()){ + // Get all Services implement Converter interface + try { + ServiceReference[] references = this.context.getAllServiceReferences(Converter.class.getName(), null); + + for(ServiceReference ref : references){ + add((Converter) this.context.getService(ref)); + } + + } catch (InvalidSyntaxException e) { + LOGGER.warning("Cannot load Converter on SimpleParser."); + } + } + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/Tailor.java b/shell/src/main/java/org/springframework/roo/shell/Tailor.java new file mode 100644 index 000000000..419b764cf --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/Tailor.java @@ -0,0 +1,16 @@ +package org.springframework.roo.shell; + +import java.util.List; + +public interface Tailor { + + /** + * Transforms input command using available Tailor implementation and + * activated configuration. + * + * @param command - roo command line + * @return - adjusted command or list of commands. empty list if command is + * not tailored + */ + List sew(String command); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java new file mode 100644 index 000000000..e9a2bcd66 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java @@ -0,0 +1,48 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; +import org.springframework.roo.shell.SimpleParser; + +/** + * Available commands converter. + * + * @author Ben Alex + * @since 1.0 + */ +public class AvailableCommandsConverter implements Converter { + + public String convertFromText(final String text, + final Class requiredType, final String optionContext) { + return text; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + if (target.getTarget() instanceof SimpleParser) { + final SimpleParser cmd = (SimpleParser) target.getTarget(); + + // Only include the first word of each command + for (final String s : cmd.getEveryCommand()) { + if (s.contains(" ")) { + completions.add(new Completion(s.substring(0, + s.indexOf(" ")))); + } + else { + completions.add(new Completion(s)); + } + } + } + return true; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return String.class.isAssignableFrom(requiredType) + && "availableCommands".equals(optionContext); + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java new file mode 100644 index 000000000..69056df65 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.math.BigDecimal; +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link BigDecimal}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class BigDecimalConverter implements Converter { + + public BigDecimal convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new BigDecimal(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return BigDecimal.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java new file mode 100644 index 000000000..209e321ad --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.math.BigInteger; +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link BigInteger}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class BigIntegerConverter implements Converter { + + public BigInteger convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new BigInteger(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return BigInteger.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java new file mode 100644 index 000000000..8512f262c --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java @@ -0,0 +1,50 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Boolean}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class BooleanConverter implements Converter { + + public Boolean convertFromText(final String value, + final Class requiredType, final String optionContext) { + if ("true".equalsIgnoreCase(value) || "1".equals(value) + || "yes".equalsIgnoreCase(value)) { + return true; + } + else if ("false".equalsIgnoreCase(value) || "0".equals(value) + || "no".equalsIgnoreCase(value)) { + return false; + } + else { + throw new IllegalArgumentException("Cannot convert " + value + + " to type Boolean."); + } + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + completions.add(new Completion("true")); + completions.add(new Completion("false")); + completions.add(new Completion("yes")); + completions.add(new Completion("no")); + completions.add(new Completion("1")); + completions.add(new Completion("0")); + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Boolean.class.isAssignableFrom(requiredType) + || boolean.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java new file mode 100644 index 000000000..f9968e74e --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Character}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class CharacterConverter implements Converter { + + public Character convertFromText(final String value, + final Class requiredType, final String optionContext) { + return value.charAt(0); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Character.class.isAssignableFrom(requiredType) + || char.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/DateConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/DateConverter.java new file mode 100644 index 000000000..be906b805 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/DateConverter.java @@ -0,0 +1,53 @@ +package org.springframework.roo.shell.converters; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Date}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class DateConverter implements Converter { + + private final DateFormat dateFormat; + + public DateConverter() { + dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, + Locale.getDefault()); + } + + public DateConverter(final DateFormat dateFormat) { + this.dateFormat = dateFormat; + } + + public Date convertFromText(final String value, + final Class requiredType, final String optionContext) { + try { + return dateFormat.parse(value); + } + catch (final ParseException e) { + throw new IllegalArgumentException("Could not parse date: " + + e.getMessage()); + } + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Date.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java new file mode 100644 index 000000000..a150f1ac9 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Double}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class DoubleConverter implements Converter { + + public Double convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new Double(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Double.class.isAssignableFrom(requiredType) + || double.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java new file mode 100644 index 000000000..cb9fa886b --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java @@ -0,0 +1,48 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Enum}. + * + * @author Ben Alex + * @author Alan Stewart + * @since 1.0 + */ +@SuppressWarnings("all") +public class EnumConverter implements Converter { + + public Enum convertFromText(final String value, + final Class requiredType, final String optionContext) { + final Class enumClass = (Class) requiredType; + return Enum.valueOf(enumClass, value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + final Class enumClass = (Class) requiredType; + for (final Enum enumValue : enumClass.getEnumConstants()) { + final String candidate = enumValue.name(); + if ("".equals(existingData) + || candidate.startsWith(existingData) + || existingData.startsWith(candidate) + || candidate.toUpperCase().startsWith( + existingData.toUpperCase()) + || existingData.toUpperCase().startsWith( + candidate.toUpperCase())) { + completions.add(new Completion(candidate)); + } + } + return true; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Enum.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/FileConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/FileConverter.java new file mode 100644 index 000000000..53a2ebf25 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/FileConverter.java @@ -0,0 +1,180 @@ +package org.springframework.roo.shell.converters; + +import java.io.File; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link File}. + * + * @author Stefan Schmidt + * @author Roman Kuzmik + * @author Ben Alex + * @since 1.0 + */ +public abstract class FileConverter implements Converter { + + private static final String HOME_DIRECTORY_SYMBOL = "~"; + private static final String home = System.getProperty("user.home"); + private static final String WINDOWS_DRIVE_PREFIX = "^[A-Za-z]:"; + + // Doesn't check for backslash after the colon, since Java has no issues + // with paths like c:/Windows + private static final Pattern WINDOWS_DRIVE_PATH = Pattern + .compile(WINDOWS_DRIVE_PREFIX + ".*"); + + private String convertCompletionBackIntoUserInputStyle( + final String originalUserInput, final String completion) { + if (denotesAbsolutePath(originalUserInput)) { + // Input was originally as a fully-qualified path, so we just keep + // the completion in that form + return completion; + } + if (originalUserInput.startsWith(HOME_DIRECTORY_SYMBOL)) { + // Input originally started with this symbol, so replace the user's + // home directory with it again + Validate.notNull(home, + "Home directory could not be determined from system properties"); + return HOME_DIRECTORY_SYMBOL + completion.substring(home.length()); + } + // The path was working directory specific, so strip the working + // directory given the user never typed it + return completion.substring(getWorkingDirectoryAsString().length()); + } + + public File convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new File(convertUserInputIntoAFullyQualifiedPath(value)); + } + + /** + * If the user input starts with a tilde character (~), replace the tilde + * character with the user's home directory. If the user input does not + * start with a tilde, simply return the original user input without any + * changes if the input specifies an absolute path, or return an absolute + * path based on the working directory if the input specifies a relative + * path. + * + * @param userInput the user input, which may commence with a tilde + * (required) + * @return a string that is guaranteed to no longer contain a tilde as the + * first character (never null) + */ + private String convertUserInputIntoAFullyQualifiedPath( + final String userInput) { + if (denotesAbsolutePath(userInput)) { + // Input is already in a fully-qualified path form + return userInput; + } + if (userInput.startsWith(HOME_DIRECTORY_SYMBOL)) { + // Replace this symbol with the user's actual home directory + Validate.notNull(home, + "Home directory could not be determined from system properties"); + if (userInput.length() > 1) { + return home + userInput.substring(1); + } + } + // The path is working directory specific, so prepend the working + // directory + final String fullPath = getWorkingDirectoryAsString() + userInput; + return fullPath; + } + + /** + * Checks if the provided fileName denotes an absolute path on the file + * system. On Windows, this includes both paths with and without drive + * letters, where the latter have to start with '\'. No check is performed + * to see if the file actually exists! + * + * @param fileName name of a file, which could be an absolute path + * @return true if the fileName looks like an absolute path for the current + * OS + */ + private boolean denotesAbsolutePath(final String fileName) { + if (SystemUtils.IS_OS_WINDOWS) { + // first check for drive letter + if (WINDOWS_DRIVE_PATH.matcher(fileName).matches()) { + return true; + } + } + return fileName.startsWith(File.separator); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String originalUserInput, + final String optionContext, final MethodTarget target) { + String adjustedUserInput = convertUserInputIntoAFullyQualifiedPath(originalUserInput); + + final String directoryData = adjustedUserInput.substring(0, + adjustedUserInput.lastIndexOf(File.separator) + 1); + adjustedUserInput = adjustedUserInput.substring(adjustedUserInput + .lastIndexOf(File.separator) + 1); + + populate(completions, adjustedUserInput, originalUserInput, + directoryData); + + return false; + } + + /** + * @return the "current working directory" this {@link FileConverter} should + * use if the user fails to provide an explicit directory in their + * input (required) + */ + protected abstract File getWorkingDirectory(); + + private String getWorkingDirectoryAsString() { + try { + return getWorkingDirectory().getCanonicalPath() + File.separator; + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + protected void populate(final List completions, + final String adjustedUserInput, final String originalUserInput, + final String directoryData) { + final File directory = new File(directoryData); + + if (!directory.isDirectory()) { + return; + } + + for (final File file : directory.listFiles()) { + if (adjustedUserInput == null + || adjustedUserInput.length() == 0 + || file.getName().toLowerCase() + .startsWith(adjustedUserInput.toLowerCase())) { + + String completion = ""; + if (directoryData.length() > 0) { + completion += directoryData; + } + completion += file.getName(); + + completion = convertCompletionBackIntoUserInputStyle( + originalUserInput, completion); + + if (file.isDirectory()) { + completions + .add(new Completion(completion + File.separator)); + } + else { + completions.add(new Completion(completion)); + } + } + } + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return File.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java new file mode 100644 index 000000000..4e0f1c9e5 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Float}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class FloatConverter implements Converter { + + public Float convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new Float(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Float.class.isAssignableFrom(requiredType) + || float.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java new file mode 100644 index 000000000..0fd8f46ec --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Integer}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class IntegerConverter implements Converter { + + public Integer convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new Integer(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Integer.class.isAssignableFrom(requiredType) + || int.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java new file mode 100644 index 000000000..9e4367238 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java @@ -0,0 +1,45 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; +import java.util.Locale; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Locale}. Supports locales with ISO-639 (ie 'en') + * or a combination of ISO-639 and ISO-3166 (ie 'en_AU'). + * + * @author Stefan Schmidt + * @since 1.1 + */ +public class LocaleConverter implements Converter { + + public Locale convertFromText(final String value, + final Class requiredType, final String optionContext) { + if (value.length() == 2) { + // In case only a simpele ISO-639 code is provided we use that code + // also for the country (ie 'de_DE') + return new Locale(value, value.toUpperCase()); + } + else if (value.length() == 5) { + final String[] split = value.split("_"); + return new Locale(split[0], split[1]); + } + else { + return null; + } + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Locale.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/LongConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/LongConverter.java new file mode 100644 index 000000000..c18f00667 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/LongConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Long}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class LongConverter implements Converter { + + public Long convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new Long(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Long.class.isAssignableFrom(requiredType) + || long.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java new file mode 100644 index 000000000..df5e1cdcb --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java @@ -0,0 +1,33 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link Short}. + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class ShortConverter implements Converter { + + public Short convertFromText(final String value, + final Class requiredType, final String optionContext) { + return new Short(value); + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return Short.class.isAssignableFrom(requiredType) + || short.class.isAssignableFrom(requiredType); + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java new file mode 100644 index 000000000..21c3740b7 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java @@ -0,0 +1,17 @@ +package org.springframework.roo.shell.converters; + +import org.springframework.roo.shell.Converter; + +/** + * Interface for adding and removing classes that provide static fields which + * should be made available via a {@link Converter}. + * + * @author Ben Alex + * @since 1.0 + */ +public interface StaticFieldConverter extends Converter { + + void add(Class clazz); + + void remove(Class clazz); +} \ No newline at end of file diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java b/shell/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java new file mode 100644 index 000000000..2ed79be6d --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java @@ -0,0 +1,100 @@ +package org.springframework.roo.shell.converters; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * A simple {@link Converter} for those classes which provide public static + * fields to represent possible textual values. + * + * @author Stefan Schmidt + * @author Ben Alex + * @since 1.0 + */ +public class StaticFieldConverterImpl implements StaticFieldConverter { + + private final Map, Map> fields = new HashMap, Map>(); + + public void add(final Class clazz) { + Validate.notNull(clazz, + "A class to provide conversion services is required"); + Validate.isTrue(fields.get(clazz) == null, + "Class '%s' is already registered for completion services", + clazz); + final Map ffields = new HashMap(); + for (final Field field : clazz.getFields()) { + final int modifier = field.getModifiers(); + if (Modifier.isStatic(modifier) && Modifier.isPublic(modifier)) { + ffields.put(field.getName(), field); + } + } + Validate.notEmpty(ffields, + "Zero public static fields accessible in '%s'", clazz); + fields.put(clazz, ffields); + } + + public Object convertFromText(final String value, + final Class requiredType, final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + final Map ffields = fields.get(requiredType); + if (ffields == null) { + return null; + } + Field f = ffields.get(value); + if (f == null) { + // Fallback to case insensitive search + for (final Field candidate : ffields.values()) { + if (candidate.getName().equalsIgnoreCase(value)) { + f = candidate; + break; + } + } + if (f == null) { + // Still not found, despite a case-insensitive search + return null; + } + } + try { + return f.get(null); + } + catch (final Exception ex) { + throw new IllegalStateException("Unable to acquire field '" + value + + "' from '" + requiredType.getName() + "'", ex); + } + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + final Map ffields = fields.get(requiredType); + if (ffields == null) { + return true; + } + for (final String field : ffields.keySet()) { + completions.add(new Completion(field)); + } + return true; + } + + public void remove(final Class clazz) { + Validate.notNull(clazz, + "A class that was providing conversion services is required"); + fields.remove(clazz); + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return fields.get(requiredType) != null; + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/converters/StringConverter.java b/shell/src/main/java/org/springframework/roo/shell/converters/StringConverter.java new file mode 100644 index 000000000..bc24714e8 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/converters/StringConverter.java @@ -0,0 +1,34 @@ +package org.springframework.roo.shell.converters; + +import java.util.List; + +import org.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; + +/** + * {@link Converter} for {@link String}. + * + * @author Ben Alex + * @since 1.0 + */ +public class StringConverter implements Converter { + + public String convertFromText(final String value, + final Class requiredType, final String optionContext) { + return value; + } + + public boolean getAllPossibleValues(final List completions, + final Class requiredType, final String existingData, + final String optionContext, final MethodTarget target) { + return false; + } + + public boolean supports(final Class requiredType, + final String optionContext) { + return String.class.isAssignableFrom(requiredType) + && (optionContext == null || !optionContext + .contains("disable-string-converter")); + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java b/shell/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java new file mode 100644 index 000000000..9c94db662 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java @@ -0,0 +1,72 @@ +package org.springframework.roo.shell.event; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.apache.commons.lang3.Validate; +import org.springframework.roo.shell.ParseResult; +import org.springframework.roo.shell.event.ShellStatus.Status; + +/** + * Provides a convenience superclass for those shells wishing to publish status + * messages. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractShellStatusPublisher implements + ShellStatusProvider { + + protected Set shellStatusListeners = new CopyOnWriteArraySet(); + protected ShellStatus shellStatus = new ShellStatus(Status.STARTING); + + public final void addShellStatusListener( + final ShellStatusListener shellStatusListener) { + Validate.notNull(shellStatusListener, "Status listener required"); + synchronized (shellStatus) { + shellStatusListeners.add(shellStatusListener); + } + } + + public final ShellStatus getShellStatus() { + synchronized (shellStatus) { + return shellStatus; + } + } + + public final void removeShellStatusListener( + final ShellStatusListener shellStatusListener) { + Validate.notNull(shellStatusListener, "Status listener required"); + synchronized (shellStatus) { + shellStatusListeners.remove(shellStatusListener); + } + } + + protected void setShellStatus(final Status shellStatus) { + setShellStatus(shellStatus, null, null); + } + + protected void setShellStatus(final Status shellStatus, final String msg, + final ParseResult parseResult) { + Validate.notNull(shellStatus, "Shell status required"); + + synchronized (this.shellStatus) { + ShellStatus st; + if (msg == null || msg.length() == 0) { + st = new ShellStatus(shellStatus); + } + else { + st = new ShellStatus(shellStatus, msg, parseResult); + } + + if (this.shellStatus.equals(st)) { + return; + } + + for (final ShellStatusListener listener : shellStatusListeners) { + listener.onShellStatusChange(this.shellStatus, st); + } + this.shellStatus = st; + } + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/event/ShellStatus.java b/shell/src/main/java/org/springframework/roo/shell/event/ShellStatus.java new file mode 100644 index 000000000..bef4bcecc --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/event/ShellStatus.java @@ -0,0 +1,99 @@ +package org.springframework.roo.shell.event; + +import org.springframework.roo.shell.ParseResult; + +/** + * Represents the different states that a shell can legally be in. + *

    + * There is no "shut down" state because the shell would have been terminated by + * that stage and potentially garbage collected. There is no guarantee that a + * shell implementation will necessarily publish every state. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.0 + */ +public class ShellStatus { + + public enum Status { + STARTING, STARTED, USER_INPUT, PARSING, EXECUTING, EXECUTION_RESULT_PROCESSING, EXECUTION_SUCCESS, EXECUTION_FAILED, SHUTTING_DOWN + } + + private final Status status; + private String message = ""; + + private ParseResult parseResult; + + ShellStatus(final Status status) { + this.status = status; + } + + ShellStatus(final Status status, final String msg, + final ParseResult parseResult) { + this.status = status; + message = msg; + this.parseResult = parseResult; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ShellStatus other = (ShellStatus) obj; + if (message == null) { + if (other.message != null) { + return false; + } + } + else if (!message.equals(other.message)) { + return false; + } + if (parseResult == null) { + if (other.parseResult != null) { + return false; + } + } + else if (!parseResult.equals(other.parseResult)) { + return false; + } + if (status == null) { + if (other.status != null) { + return false; + } + } + else if (!status.equals(other.status)) { + return false; + } + return true; + } + + public String getMessage() { + return message; + } + + public final ParseResult getParseResult() { + return parseResult; + } + + public Status getStatus() { + return status; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (message == null ? 0 : message.hashCode()); + result = prime * result + + (parseResult == null ? 0 : parseResult.hashCode()); + result = prime * result + (status == null ? 0 : status.hashCode()); + return result; + } +} diff --git a/shell/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java b/shell/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java new file mode 100644 index 000000000..0ebe79578 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java @@ -0,0 +1,18 @@ +package org.springframework.roo.shell.event; + +/** + * Implemented by classes that wish to be notified of shell status changes. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ShellStatusListener { + + /** + * Invoked by the shell to report a new status. + * + * @param oldStatus the old status + * @param newStatus the new status + */ + void onShellStatusChange(ShellStatus oldStatus, ShellStatus newStatus); +} diff --git a/shell/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java b/shell/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java new file mode 100644 index 000000000..afe1431c6 --- /dev/null +++ b/shell/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java @@ -0,0 +1,46 @@ +package org.springframework.roo.shell.event; + +/** + * Implemented by shells that support the publication of shell status changes. + *

    + * Implementations are not required to provide any guarantees with respect to + * the order in which notifications are delivered to listeners. + *

    + * Implementations must permit modification of the listener list, even while + * delivering event notifications to listeners. However, listeners do not + * receive any guarantee that their addition or removal from the listener list + * will be effective or not for any event notification that is currently + * proceeding. + *

    + * Implementations must ensure that status notifications are only delivered when + * an actual change has taken place. + * + * @author Ben Alex + * @since 1.0 + */ +public interface ShellStatusProvider { + + /** + * Registers a new status listener. + * + * @param shellStatusListener to register (cannot be null) + */ + void addShellStatusListener(ShellStatusListener shellStatusListener); + + /** + * Returns the current shell status. + * + * @return the current status (never null) + */ + ShellStatus getShellStatus(); + + /** + * Removes an existing status listener. + *

    + * If the presented status listener is not found, the method returns without + * exception. + * + * @param shellStatusListener to remove (cannot be null) + */ + void removeShellStatusListener(ShellStatusListener shellStatusListener); +} diff --git a/shell/src/test/java/org/springframework/roo/shell/AbstractShellTest.java b/shell/src/test/java/org/springframework/roo/shell/AbstractShellTest.java new file mode 100644 index 000000000..14acda551 --- /dev/null +++ b/shell/src/test/java/org/springframework/roo/shell/AbstractShellTest.java @@ -0,0 +1,30 @@ +package org.springframework.roo.shell; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +/** + * Unit test of {@link AbstractShell} (not a superclass for writing tests for + * {@link AbstractShell} subclasses) + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class AbstractShellTest { + + @Test + public void testProps() { + // Set up + final AbstractShell shell = mock(AbstractShell.class); + when(shell.props()).thenCallRealMethod(); + + // Invoke + final String props = shell.props(); + + // Check + assertNotNull(props); + } +} diff --git a/shell/src/test/java/org/springframework/roo/shell/CliOptionContextTest.java b/shell/src/test/java/org/springframework/roo/shell/CliOptionContextTest.java new file mode 100644 index 000000000..94bee4575 --- /dev/null +++ b/shell/src/test/java/org/springframework/roo/shell/CliOptionContextTest.java @@ -0,0 +1,44 @@ +package org.springframework.roo.shell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +/** + * Unit test of {@link CliOptionContext} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class CliOptionContextTest { + + private static final String OPTION_CONTEXT = "anything"; + + @Test + public void testGetOptionContextWhenNoneSet() { + CliOptionContext.setOptionContext(null); + assertNull(CliOptionContext.getOptionContext()); + } + + @Test + public void testResetOptionContext() { + // Set up + CliOptionContext.setOptionContext(OPTION_CONTEXT); + + // Invoke + CliOptionContext.resetOptionContext(); + + // Check + assertNull(CliOptionContext.getOptionContext()); + } + + @Test + public void testSetAndGetOptionContext() { + // Set up + CliOptionContext.setOptionContext(OPTION_CONTEXT); + + // Invoke and check + assertEquals(OPTION_CONTEXT, CliOptionContext.getOptionContext()); + } +} diff --git a/shell/src/test/java/org/springframework/roo/shell/MethodTargetTest.java b/shell/src/test/java/org/springframework/roo/shell/MethodTargetTest.java new file mode 100644 index 000000000..484380caa --- /dev/null +++ b/shell/src/test/java/org/springframework/roo/shell/MethodTargetTest.java @@ -0,0 +1,56 @@ +package org.springframework.roo.shell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.lang.reflect.Method; + +import org.junit.Test; + +/** + * Unit test of {@link MethodTarget} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class MethodTargetTest { + + private static final Object TARGET_1 = new CommandMarker() { + }; + private static final Object TARGET_2 = new CommandMarker() { + }; + private static final Method METHOD_1 = TARGET_1.getClass().getMethods()[0]; // Unmockable + private static final Method METHOD_2 = TARGET_2.getClass().getMethods()[1]; // Unmockable + + @Test + public void testInstanceDoesNotEqualNull() { + assertFalse(new MethodTarget(METHOD_1, TARGET_1).equals(null)); + } + + @Test + public void testInstanceEqualsItself() { + final MethodTarget instance = new MethodTarget(METHOD_1, TARGET_1); + assertEquals(instance, instance); + } + + @Test + public void testInstancesWithDifferentMethodAreNotEqual() { + assertFalse(new MethodTarget(METHOD_1, TARGET_1) + .equals(new MethodTarget(METHOD_2, TARGET_1))); + } + + @Test + public void testInstancesWithDifferentTargetAreNotEqual() { + assertFalse(new MethodTarget(METHOD_1, TARGET_1) + .equals(new MethodTarget(METHOD_1, TARGET_2))); + } + + @Test + public void testInstancesWithSameMethodAndTargetAreEqualAndHaveSameHashCode() { + final MethodTarget instance1 = new MethodTarget(METHOD_1, TARGET_1, + "the-buff", "the-key"); + final MethodTarget instance2 = new MethodTarget(METHOD_1, TARGET_1); + assertEquals(instance1, instance2); + assertEquals(instance1.hashCode(), instance2.hashCode()); + } +} diff --git a/shell/src/test/java/org/springframework/roo/shell/SimpleParserTest.java b/shell/src/test/java/org/springframework/roo/shell/SimpleParserTest.java new file mode 100644 index 000000000..c7a803903 --- /dev/null +++ b/shell/src/test/java/org/springframework/roo/shell/SimpleParserTest.java @@ -0,0 +1,52 @@ +package org.springframework.roo.shell; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test of {@link SimpleParser} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class SimpleParserTest { + + // Fixture + private SimpleParser simpleParser; + + /** + * Asserts that normalising the given input produces the given output + * + * @param input can't be null + * @param output + */ + private void assertNormalised(final String input, final String output) { + Assert.assertEquals(output, simpleParser.normalise(input)); + } + + @Before + public void setUp() { + simpleParser = new SimpleParser(); + } + + @Test + public void testNormaliseEmptyString() { + assertNormalised("", ""); + } + + @Test + public void testNormaliseMultipleWords() { + assertNormalised(" security setup ", "security setup"); + } + + @Test + public void testNormaliseSingleWord() { + assertNormalised("hint", "hint"); + } + + @Test + public void testNormaliseSpaces() { + assertNormalised(" ", ""); + } +} diff --git a/shell/src/test/java/org/springframework/roo/shell/converters/EnumConverterTest.java b/shell/src/test/java/org/springframework/roo/shell/converters/EnumConverterTest.java new file mode 100644 index 000000000..21ed89494 --- /dev/null +++ b/shell/src/test/java/org/springframework/roo/shell/converters/EnumConverterTest.java @@ -0,0 +1,68 @@ +package org.springframework.roo.shell.converters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.roo.shell.Completion; + +/** + * Unit test of {@link EnumConverter} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class EnumConverterTest { + + /** + * A simple test enum (enums can't be mocked). + * + * @author Andrew Swan + * @since 1.2.0 + */ + private enum Flavour { + BANANA, CHERRY, RASPBERRY; + } + + // Fixture + private EnumConverter enumConverter; + + @Before + public void setUp() { + enumConverter = new EnumConverter(); + } + + @Test + public void testConvertFromText() { + // Invoke + final Enum result = enumConverter.convertFromText( + Flavour.BANANA.name(), Flavour.class, "anything"); + + // Check + assertEquals(Flavour.BANANA, result); + } + + @Test + public void testGetAllPossibleValuesForPartialName() { + // Set up + final List completions = new ArrayList(); + + // Invoke + final boolean result = enumConverter.getAllPossibleValues(completions, + Flavour.class, "b", "anything", null); + + // Check + assertTrue(result); + assertEquals(1, completions.size()); + assertEquals(Flavour.BANANA.name(), completions.get(0).getValue()); + } + + @Test + public void testSupports() { + assertTrue(enumConverter.supports(Flavour.class, "anything")); + } +} diff --git a/startlevel/pom.xml b/startlevel/pom.xml new file mode 100644 index 000000000..206b7237b --- /dev/null +++ b/startlevel/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.startlevel + bundle + Spring Roo - OSGi Start Level Control + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + + + org.apache.felix + maven-bundle-plugin + true + + + ${project.artifactId}.Activator + ${project.artifactId} + Copyright ${project.organization.name}. All Rights Reserved. + ${project.url} + + + + + + \ No newline at end of file diff --git a/startlevel/src/main/java/org/springframework/roo/startlevel/Activator.java b/startlevel/src/main/java/org/springframework/roo/startlevel/Activator.java new file mode 100644 index 000000000..a1a68934a --- /dev/null +++ b/startlevel/src/main/java/org/springframework/roo/startlevel/Activator.java @@ -0,0 +1,244 @@ +package org.springframework.roo.startlevel; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.commons.io.IOUtils; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.service.startlevel.StartLevel; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * Changes the OSGi framework to start level 99 once all the services marked + * with "immediate" are activated. + *

    + * The OSGi Declarative Service Specification ensures services are only loaded + * when required. While the "immediate" attribute ensures they are loaded + * eagerly, it is impossible to receive a callback when all "immediate" services + * have finished loading. This {@link BundleActivator} resolves this issue by + * discovering all enabled service components with an "immediate" flag and + * monitoring their startup. Once all are started, the OSGi framework is set to + * start level 99, which enables other classes that depend on the activation of + * such services to react to the start level change. + *

    + * Note that this functionality is only provided for services (simple components + * are insufficient). Services must be defined in the XML file indicated by the + * "Service-Component" manifest header. + * + * @author Ben Alex + */ +public class Activator implements BundleActivator { + + /** key: required class, any one of its services interfaces */ + private final SortedMap requiredImplementations = new TreeMap(); + private final SortedSet runningImplementations = new TreeSet(); + private StartLevel startLevel; + private ServiceReference startLevelServiceReference; + + private String getClassName(final ServiceReference sr, + final BundleContext context) { + if (sr == null) { + return null; + } + if (sr.getProperty("component.name") != null) { + // Roo's convention is the component name should be the fully + // qualified class name. + // Roo's other convention is bundle symbolic names should be fully + // qualified package names. + // However, the user can change the BSN or component name, so we + // need to do a quick sanity check. + final String componentName = sr.getProperty("component.name") + .toString(); + if (componentName.startsWith(sr.getBundle().getSymbolicName())) { + // The type name appears under the BSN package, so they probably + // haven't changed our convention + return componentName; + } + } + + // To get here we couldn't rely on component name. The following is far + // less reliable given the + // service may be unavailable by the time we try to do a getService(sr) + // invocation (ROO-1156). + final Object obj = context.getService(sr); + if (obj == null) { + return null; + } + return obj.getClass().getName(); + } + + private void potentiallyChangeStartLevel() { + if (requiredImplementations.keySet().equals(runningImplementations)) { + if (System.getProperty("roo.pause") != null) { + System.out + .println("roo.pause detected; press any key to proceed"); + try { + System.in.read(); + } + catch (final IOException ignored) { + } + } + startLevel.setStartLevel(99); + } + } + + public void process(final URL url) { + Document document; + InputStream is = null; + try { + is = url.openStream(); + document = DocumentBuilderFactory.newInstance() + .newDocumentBuilder().parse(is); + } + catch (final Exception ex) { + throw new IllegalStateException("Could not open " + url, ex); + } + finally { + IOUtils.closeQuietly(is); + } + + final Element rootElement = (Element) document.getFirstChild(); + final NodeList components = rootElement + .getElementsByTagName("scr:component"); + if (components == null || components.getLength() == 0) { + return; + } + + for (int i = 0; i < components.getLength(); i++) { + final Element component = (Element) components.item(i); + + // Is this component enabled? + if (!component.hasAttribute("enabled") + || !"true".equals(component.getAttribute("enabled"))) { + // Disabled, so skip it + continue; + } + + // Is this an immediate starter? + if (!component.hasAttribute("immediate") + || !"true".equals(component.getAttribute("immediate"))) { + // Not an immediate starter, so skip it + continue; + } + + // Calculate implementing class name correctly + String componentName = null; + final NodeList implementation = component + .getElementsByTagName("implementation"); + if (implementation != null && implementation.getLength() == 1) { + final Element impl = (Element) implementation.item(0); + if (impl.hasAttribute("class")) { + componentName = impl.getAttribute("class"); + } + } + + // Get its first implementing service + String serviceInterface = null; + final NodeList service = component.getElementsByTagName("service"); + if (service != null && service.getLength() == 1) { + final Element s = (Element) service.item(0); + final NodeList provide = s.getElementsByTagName("provide"); + if (provide != null && provide.getLength() > 0) { + final Element firstProvide = (Element) provide.item(0); + if (firstProvide.hasAttribute("interface")) { + serviceInterface = firstProvide + .getAttribute("interface"); + } + } + } + + if (componentName != null && serviceInterface != null) { + requiredImplementations.put(componentName, serviceInterface); + } + } + } + + public void start(final BundleContext context) throws Exception { + startLevelServiceReference = context + .getServiceReference(StartLevel.class.getName()); + startLevel = (StartLevel) context + .getService(startLevelServiceReference); + for (final Bundle bundle : context.getBundles()) { + final String value = bundle.getHeaders().get("Service-Component"); + if (value != null) { + List componentDescriptions = Arrays.asList(value.split("\\s*,\\s*")); + for (String desc : componentDescriptions) { + final URL url = bundle.getResource(desc); + process(url); + } + } + } + + // Ensure I'm notified of other services changes + final BundleContext myContext = context; + context.addServiceListener(new ServiceListener() { + public void serviceChanged(final ServiceEvent event) { + final ServiceReference sr = event.getServiceReference(); + final String className = getClassName(sr, myContext); + if (sr != null) { + myContext.ungetService(sr); + } + if (className == null) { + // Something went wrong + return; + } + if (event.getType() == ServiceEvent.REGISTERED) { + if (requiredImplementations.keySet().contains(className)) { + runningImplementations.add(className); + potentiallyChangeStartLevel(); + } + } + else if (event.getType() == ServiceEvent.UNREGISTERING) { + if (runningImplementations.contains(className)) { + runningImplementations.remove(className); + potentiallyChangeStartLevel(); + } + } + } + }); + + // Now identify if any services I was interested in are already running + for (final String requiredService : requiredImplementations.keySet()) { + final String correspondingInterface = requiredImplementations + .get(requiredService); + final ServiceReference[] srs = context.getServiceReferences( + correspondingInterface, null); + if (srs != null) { + for (final ServiceReference sr : srs) { + final String className = getClassName(sr, context); + if (className == null) { + // Something went wrong + continue; + } + if (requiredImplementations.keySet().contains(className)) { + runningImplementations.add(className); + } + } + } + } + + // Potentially change the start level, now that we've added all the + // known started services + potentiallyChangeStartLevel(); + } + + public void stop(final BundleContext context) throws Exception { + context.ungetService(startLevelServiceReference); + } +} diff --git a/support-osgi/pom.xml b/support-osgi/pom.xml new file mode 100644 index 000000000..880275e6f --- /dev/null +++ b/support-osgi/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.support.osgi + bundle + Spring Roo - Support for OSGi Features + Provides support services for Spring Roo's core and base add-ons. Includes OSGi dependencies. + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.springframework.roo + org.springframework.roo.support + + + \ No newline at end of file diff --git a/support-osgi/src/main/java/org/springframework/roo/support/osgi/BundleCallback.java b/support-osgi/src/main/java/org/springframework/roo/support/osgi/BundleCallback.java new file mode 100644 index 000000000..b3e9ad889 --- /dev/null +++ b/support-osgi/src/main/java/org/springframework/roo/support/osgi/BundleCallback.java @@ -0,0 +1,19 @@ +package org.springframework.roo.support.osgi; + +import org.osgi.framework.Bundle; + +/** + * Callback for operating upon OSGi {@link Bundle}s. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public interface BundleCallback { + + /** + * Executes this callback on the given OSGi bundle + * + * @param bundle the bundle to operate upon + */ + void execute(Bundle bundle); +} \ No newline at end of file diff --git a/support-osgi/src/main/java/org/springframework/roo/support/osgi/BundleFindingUtils.java b/support-osgi/src/main/java/org/springframework/roo/support/osgi/BundleFindingUtils.java new file mode 100644 index 000000000..c65838b64 --- /dev/null +++ b/support-osgi/src/main/java/org/springframework/roo/support/osgi/BundleFindingUtils.java @@ -0,0 +1,90 @@ +package org.springframework.roo.support.osgi; + +import java.net.URL; + +import org.apache.commons.lang3.Validate; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * Helper to locate bundle symbolic names used by Spring Roo. + * + * @author Ben Alex + * @since 1.1.1 + */ +public abstract class BundleFindingUtils { + + /** + * Locates the first bundle that contains the presented type name and return + * its bundle symbolic name. + * + * @param context that can be used to obtain bundles to search (required) + * @param typeNameInExternalForm a type name (eg com.foo.Bar) + * @return the bundle symbolic name, if found (or null if not found) + */ + public static String findFirstBundleForTypeName( + final BundleContext context, final String typeNameInExternalForm) { + Validate.notNull(context, + "Bundle context required to perform the search"); + Validate.notBlank(typeNameInExternalForm, + "Resource name to locate is required"); + final String resourceName = "/" + + typeNameInExternalForm.replace('.', '/') + ".class"; + + final Bundle[] bundles = context.getBundles(); + if (bundles == null) { + return null; + } + + for (final Bundle bundle : bundles) { + try { + final URL url = bundle.getEntry(resourceName); + if (url != null) { + return bundle.getSymbolicName(); + } + } + catch (final RuntimeException e) { + return null; + } + } + + return null; + } + + /** + * Locates the first bundle that contains the presented type name and return + * that class. + * + * @param context that can be used to obtain bundles to search (required) + * @param typeNameInExternalForm a type name (eg com.foo.Bar) + * @return the class, if found (or null if not found) + */ + public static Class findFirstBundleWithType(final BundleContext context, + final String typeNameInExternalForm) { + Validate.notNull(context, + "Bundle context required to perform the search"); + Validate.notBlank(typeNameInExternalForm, + "Resource name to locate is required"); + final String resourceName = "/" + + typeNameInExternalForm.replace('.', '/') + ".class"; + + final Bundle[] bundles = context.getBundles(); + if (bundles == null) { + return null; + } + + for (final Bundle bundle : bundles) { + try { + final URL url = bundle.getEntry(resourceName); + if (url != null) { + return bundle.loadClass(typeNameInExternalForm); + } + } + catch (final Throwable e) { + return null; + } + } + + return null; + } +} diff --git a/support-osgi/src/main/java/org/springframework/roo/support/osgi/OSGiUtils.java b/support-osgi/src/main/java/org/springframework/roo/support/osgi/OSGiUtils.java new file mode 100644 index 000000000..17b31b37d --- /dev/null +++ b/support-osgi/src/main/java/org/springframework/roo/support/osgi/OSGiUtils.java @@ -0,0 +1,173 @@ +package org.springframework.roo.support.osgi; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; + +import org.apache.commons.lang3.Validate; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.support.ant.AntPathMatcher; +import org.springframework.roo.support.ant.PathMatcher; + +/** + * Utility methods relating to OSGi + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class OSGiUtils { + + private static final PathMatcher PATH_MATCHER = new AntPathMatcher(); + + /** + * The name of the property that stores the Roo working directory. + */ + public static final String ROO_WORKING_DIRECTORY_PROPERTY = "roo.working.directory"; + + /** + * The root path within an OSGi bundle + */ + public static final String ROOT_PATH = "/"; + + /** + * Executes the given callback on any bundles in the given context + * + * @param callback can be null to do nothing + * @param context can be null to do nothing + */ + public static void execute(final BundleCallback callback, + final BundleContext context) { + if (callback == null || context == null) { + return; + } + final Bundle[] bundles = context.getBundles(); + if (bundles == null) { + return; + } + for (final Bundle bundle : bundles) { + callback.execute(bundle); + } + } + + /** + * Searches the bundles in the given context for entries with the given + * path. + * + * @param context that can be used to obtain bundles to search (can be + * null) + * @param path the path of the resource to locate (as per + * {@link Bundle#getEntry}, e.g. "/foo.txt" will find foo.txt in + * the root of each bundle) + * @return null if there was a failure or a set containing zero or more + * entries (zero entries means the search was successful but the + * resource was simply not found) + */ + public static Collection findEntriesByPath( + final BundleContext context, final String path) { + Validate.notBlank(path, "Path to locate is required"); + final Collection urls = new ArrayList(); + // We use a collection of URIs to avoid duplication in the collection of + // URLs; we can't simply use a Set of URLs because URL#equals is broken. + final Collection uris = new ArrayList(); + OSGiUtils.execute(new BundleCallback() { + public void execute(final Bundle bundle) { + try { + final URL url = bundle.getEntry(path); + if (url != null) { + final URI uri = url.toURI(); + if (!uris.contains(uri)) { + // We haven't seen this URL before; add it + urls.add(url); + uris.add(uri); + } + } + } + catch (final IllegalStateException e) { + // The bundle has been uninstalled - ignore it + } + catch (final URISyntaxException e) { + // The URL can't be converted to a URI - ignore it + } + } + }, context); + + return urls; + } + + /** + * Returns the URIs of any entries among the given bundles whose URLs match + * the given Ant-style path. + * + * @param context the context whose bundles to search (can be + * null) + * @param antPathExpression the pattern for matching URLs against (required) + * @return null if the search can't be performed, otherwise a + * non-null Set + * @see AntPathMatcher#match(String, String) + */ + @SuppressWarnings("unchecked") + public static Collection findEntriesByPattern( + final BundleContext context, final String antPathExpression) { + Validate.notBlank(antPathExpression, + "Ant path expression to match is required"); + final Collection urls = new ArrayList(); + // We use a collection of URIs to avoid duplication in the collection of + // URLs; we can't simply use a Set of URLs because URL#equals is broken. + final Collection uris = new ArrayList(); + OSGiUtils.execute(new BundleCallback() { + public void execute(final Bundle bundle) { + try { + final Enumeration enumeration = bundle.findEntries( + ROOT_PATH, "*", true); + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + final URL url = enumeration.nextElement(); + if (PATH_MATCHER.match(antPathExpression, + url.getPath())) { + try { + final URI uri = url.toURI(); + if (!uris.contains(uri)) { + urls.add(url); + uris.add(uri); + } + } + catch (final URISyntaxException e) { + // This URL can't be converted to a URI - + // ignore it + } + } + } + } + } + catch (final IllegalStateException e) { + // The bundle has been uninstalled - ignore it + } + } + }, context); + return urls; + } + + /** + * Returns the Roo working directory for the given OSGi component context + * + * @param componentContext the component context (required) + * @return the path of the Roo working directory + * @since 1.2.0 + */ + public static String getRooWorkingDirectory( + final ComponentContext componentContext) { + return componentContext.getBundleContext().getProperty( + ROO_WORKING_DIRECTORY_PROPERTY); + } + + /** + * Constructor is private to prevent instantiation + */ + private OSGiUtils() { + } +} diff --git a/support-osgi/src/main/java/org/springframework/roo/support/osgi/UrlFindingUtils.java b/support-osgi/src/main/java/org/springframework/roo/support/osgi/UrlFindingUtils.java new file mode 100644 index 000000000..ed0dfc78b --- /dev/null +++ b/support-osgi/src/main/java/org/springframework/roo/support/osgi/UrlFindingUtils.java @@ -0,0 +1,107 @@ +package org.springframework.roo.support.osgi; + +import java.net.URL; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.springframework.roo.support.ant.AntPathMatcher; +import org.springframework.roo.support.ant.PathMatcher; + +/** + * Utility methods for locating resources within OSGi bundles. + */ +public final class UrlFindingUtils { + + private static final PathMatcher PATH_MATCHER = new AntPathMatcher(); + private static final String ROOT_PATH = "/"; + + /** + * Returns the URLs of any entries among the given bundles whose URLs match + * the given Ant-style path. + * + * @param context the context whose bundles to search (can be + * null) + * @param antPathExpression the pattern for matching URLs against (required) + * @return null if the search can't be performed, otherwise a + * non-null Set + * @see AntPathMatcher#match(String, String) + * @deprecated sets of URLs are slow and unreliable; use + * {@link OSGiUtils#findEntriesByPattern(BundleContext, String)} + * instead + */ + @Deprecated + @SuppressWarnings("unchecked") + public static Set findMatchingClasspathResources( + final BundleContext context, final String antPathExpression) { + Validate.notBlank(antPathExpression, + "Ant path expression to match is required"); + final Set results = new HashSet(); + OSGiUtils.execute(new BundleCallback() { + public void execute(final Bundle bundle) { + try { + final Enumeration enumeration = bundle.findEntries( + ROOT_PATH, "*", true); + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + final URL url = enumeration.nextElement(); + if (PATH_MATCHER.match(antPathExpression, + url.getPath())) { + results.add(url); + } + } + } + } + catch (final IllegalStateException e) { + // The bundle has been uninstalled - ignore it + } + } + }, context); + return results; + } + + /** + * Searches the bundles in the given context for the given resource. + * + * @param context that can be used to obtain bundles to search (can be + * null) + * @param resourceName the path of the resource to locate (as per + * {@link Bundle#getEntry}, e.g. "/foo.txt" will find foo.txt in + * the root of each bundle) + * @return null if there was a failure or a set containing zero or more + * entries (zero entries means the search was successful but the + * resource was simply not found) + * @deprecated sets of URLs are slow and unreliable; use + * {@link OSGiUtils#findEntriesByPath(BundleContext, String)} + * instead + */ + @Deprecated + public static Set findUrls(final BundleContext context, + final String resourceName) { + Validate.notBlank(resourceName, "Resource name to locate is required"); + final Set results = new HashSet(); + OSGiUtils.execute(new BundleCallback() { + public void execute(final Bundle bundle) { + try { + final URL url = bundle.getEntry(resourceName); + if (url != null) { + results.add(url); + } + } + catch (final IllegalStateException e) { + // The bundle has been uninstalled - ignore it + } + } + }, context); + return results; + } + + /** + * Constructor is private to prevent instantiation + */ + private UrlFindingUtils() { + } +} \ No newline at end of file diff --git a/support-osgi/src/test/java/org/springframework/roo/support/osgi/OSGiUtilsTest.java b/support-osgi/src/test/java/org/springframework/roo/support/osgi/OSGiUtilsTest.java new file mode 100644 index 000000000..36b5ffcb2 --- /dev/null +++ b/support-osgi/src/test/java/org/springframework/roo/support/osgi/OSGiUtilsTest.java @@ -0,0 +1,39 @@ +package org.springframework.roo.support.osgi; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.roo.support.osgi.OSGiUtils.ROO_WORKING_DIRECTORY_PROPERTY; + +import org.junit.Test; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; + +/** + * Unit test of {@link OSGiUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class OSGiUtilsTest { + + private static final String ROO_WORKING_DIRECTORY = "/some/file/path"; + + @Test + public void testGetRooWorkingDirectory() { + // Set up + final BundleContext mockBundleContext = mock(BundleContext.class); + when(mockBundleContext.getProperty(ROO_WORKING_DIRECTORY_PROPERTY)) + .thenReturn(ROO_WORKING_DIRECTORY); + final ComponentContext mockComponentContext = mock(ComponentContext.class); + when(mockComponentContext.getBundleContext()).thenReturn( + mockBundleContext); + + // Invoke + final String rooWorkingDirectory = OSGiUtils + .getRooWorkingDirectory(mockComponentContext); + + // Check + assertEquals(ROO_WORKING_DIRECTORY, rooWorkingDirectory); + } +} diff --git a/support/legal-support.txt b/support/legal-support.txt new file mode 100644 index 000000000..ea8df9397 --- /dev/null +++ b/support/legal-support.txt @@ -0,0 +1,62 @@ +====================================================================== +LEGAL NOTICES +====================================================================== + +All code in this project is original work, except as disclosed below. + +----------------------------------------------------------------------- + +Licensed Software: Spring Framework +Software Web Site: http://www.springframework.org +Effective License: Apache License, Version 2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.html + +Statement as required by the license: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)" + +Spring Framework is not a dependency of this module. It is noted in the +legal section because the following files were originally derived from +Spring Framework: + +org.springframework.roo.support.ant.AntPatchStringMatcher +org.springframework.roo.support.ant.AntPathMatcher +org.springframework.roo.support.ant.PathMatcher +org.springframework.roo.support.style.DefaultToStringStyler +org.springframework.roo.support.style.DefaultValueStyler +org.springframework.roo.support.style.StylerUtils +org.springframework.roo.support.style.ToStringCreator +org.springframework.roo.support.style.ToStringStyler +org.springframework.roo.support.style.ValueStyler +org.springframework.roo.support.util.Assert +org.springframework.roo.support.util.ClassUtils +org.springframework.roo.support.util.CollectionUtils +org.springframework.roo.support.util.DomUtils +org.springframework.roo.support.util.CollectionUtils +org.springframework.roo.support.util.FileCopyUtils +org.springframework.roo.support.util.ObjectUtils +org.springframework.roo.support.util.ReflectionUtils +org.springframework.roo.support.util.StringUtils + +----------------------------------------------------------------------- + +Licensed Software: Spring Security +Software Web Site: http://www.springframework.org/security +Effective License: Apache License, Version 2.0 +License Info Page: http://www.apache.org/licenses/LICENSE-2.0.html + +Statement as required by the license: + + "This product includes software developed by the Spring Security + Project (http://www.springframework.org/security)" + +Spring Security is not a dependency of this module. It is noted in the +legal section because the following files were originally derived from +Spring Security: + +org.springframework.roo.support.util.HexUtils + +----------------------------------------------------------------------- + +[end] \ No newline at end of file diff --git a/support/pom.xml b/support/pom.xml new file mode 100644 index 000000000..415b8dac4 --- /dev/null +++ b/support/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-bundle + + org.springframework.roo.support + bundle + Spring Roo - Support + Provides support services for Spring Roo's core. Has no external dependencies (including no dependency on OSGi). + \ No newline at end of file diff --git a/support/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java b/support/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java new file mode 100644 index 000000000..3276999a2 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java @@ -0,0 +1,231 @@ +package org.springframework.roo.support.ant; + +import java.util.Map; + +/** + * Package-protected helper class for {@link AntPathMatcher}. Tests whether or + * not a string matches against a pattern. The pattern may contain special + * characters:
    + * '*' means zero or more characters
    + * '?' means one and only one character, '{' and '}' indicate a uri template + * pattern + * + * @author Arjen Poutsma + * @since 3.0 + */ +class AntPatchStringMatcher { + + private char ch; + private final char[] patArr; + private int patIdxEnd; + private int patIdxStart = 0; + private final char[] strArr; + private int strIdxEnd; + private int strIdxStart = 0; + private final Map uriTemplateVariables; + + /** Constructs a new instance of the AntPatchStringMatcher. */ + AntPatchStringMatcher(final String pattern, final String str, + final Map uriTemplateVariables) { + patArr = pattern.toCharArray(); + strArr = str.toCharArray(); + this.uriTemplateVariables = uriTemplateVariables; + patIdxEnd = patArr.length - 1; + strIdxEnd = strArr.length - 1; + } + + private void addTemplateVariable(final int curlyIdxStart, + final int curlyIdxEnd, final int valIdxStart, final int valIdxEnd) { + if (uriTemplateVariables != null) { + final String varName = new String(patArr, curlyIdxStart + 1, + curlyIdxEnd - curlyIdxStart - 1); + final String varValue = new String(strArr, valIdxStart, valIdxEnd + - valIdxStart + 1); + uriTemplateVariables.put(varName, varValue); + } + } + + private boolean allCharsUsed() { + return strIdxStart > strIdxEnd; + } + + private boolean consecutiveStars(final int patIdxTmp) { + if (patIdxTmp == patIdxStart + 1 && patArr[patIdxStart] == '*' + && patArr[patIdxTmp] == '*') { + // Two stars next to each other, skip the first one. + patIdxStart++; + return true; + } + return false; + } + + private boolean doShortcut() { + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?') { + if (ch != strArr[i]) { + return false;// Character mismatch + } + } + } + return true; // String matches against pattern + } + + private int findClosingCurly() { + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '}') { + return i; + } + } + return -1; + } + + private int findNextStarOrCurly() { + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*' || patArr[i] == '{') { + return i; + } + } + return -1; + } + + private boolean matchAfterLastStarOrCurly() { + while ((ch = patArr[patIdxEnd]) != '*' && ch != '}' + && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxEnd]) { + return false; + } + } + patIdxEnd--; + strIdxEnd--; + } + return true; + } + + private boolean matchBeforeFirstStarOrCurly() { + while ((ch = patArr[patIdxStart]) != '*' && ch != '{' + && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxStart]) { + return false; + } + } + patIdxStart++; + strIdxStart++; + } + return true; + } + + /** + * Main entry point. + * + * @return true if the string matches against the pattern, or + * false otherwise. + */ + boolean matchStrings() { + if (shortcutPossible()) { + return doShortcut(); + } + if (patternContainsOnlyStar()) { + return true; + } + if (patternContainsOneTemplateVariable()) { + addTemplateVariable(0, patIdxEnd, 0, strIdxEnd); + return true; + } + if (!matchBeforeFirstStarOrCurly()) { + return false; + } + if (allCharsUsed()) { + return onlyStarsLeft(); + } + if (!matchAfterLastStarOrCurly()) { + return false; + } + if (allCharsUsed()) { + return onlyStarsLeft(); + } + // Process pattern between stars. padIdxStart and patIdxEnd point always + // to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp; + if (patArr[patIdxStart] == '{') { + patIdxTmp = findClosingCurly(); + addTemplateVariable(patIdxStart, patIdxTmp, strIdxStart, + strIdxEnd); + patIdxStart = patIdxTmp + 1; + strIdxStart = strIdxEnd + 1; + continue; + } + patIdxTmp = findNextStarOrCurly(); + if (consecutiveStars(patIdxTmp)) { + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + final int patLength = patIdxTmp - patIdxStart - 1; + final int strLength = strIdxEnd - strIdxStart + 1; + int foundIdx = -1; + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?') { + if (ch != strArr[strIdxStart + i + j]) { + continue strLoop; + } + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + return onlyStarsLeft(); + } + + private boolean onlyStarsLeft() { + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + private boolean patternContainsOneTemplateVariable() { + if (patIdxEnd >= 2 && patArr[0] == '{' && patArr[patIdxEnd] == '}') { + for (int i = 1; i < patIdxEnd; i++) { + if (patArr[i] == '}') { + return false; + } + } + return true; + } + return false; + } + + private boolean patternContainsOnlyStar() { + return patIdxEnd == 0 && patArr[0] == '*'; + } + + private boolean shortcutPossible() { + for (final char ch : patArr) { + if (ch == '*' || ch == '{' || ch == '}') { + return false; + } + } + return true; + } +} diff --git a/support/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java b/support/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java new file mode 100644 index 000000000..87c05b483 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java @@ -0,0 +1,278 @@ +package org.springframework.roo.support.ant; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.text.StrTokenizer; + +/** + * PathMatcher implementation for Ant-style path patterns. Examples are provided + * below. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @author Rob Harrop + * @since 16.07.2003 + */ +public class AntPathMatcher implements PathMatcher { + + /** Default path separator: "/" */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private String pathSeparator = DEFAULT_PATH_SEPARATOR; + + /** + * Actually match the given path against the given + * pattern. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @param fullMatch whether a full pattern match is required (else a pattern + * match as far as the given base path goes is sufficient) + * @return true if the supplied path matched, + * false if it didn't + */ + protected boolean doMatch(final String pattern, final String path, + final boolean fullMatch, + final Map uriTemplateVariables) { + if (path.startsWith(pathSeparator) != pattern.startsWith(pathSeparator)) { + return false; + } + + final String[] patternDirs = new StrTokenizer(pattern, pathSeparator) + .setIgnoreEmptyTokens(true).getTokenArray(); + final String[] pathDirs = new StrTokenizer(path, pathSeparator) + .setIgnoreEmptyTokens(true).getTokenArray(); + + int pattIdxStart = 0; + int pattIdxEnd = patternDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + final String patDir = patternDirs[pattIdxStart]; + if ("**".equals(patDir)) { + break; + } + if (!matchStrings(patDir, pathDirs[pathIdxStart], + uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return pattern.endsWith(pathSeparator) ? path + .endsWith(pathSeparator) : !path + .endsWith(pathSeparator); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd + && patternDirs[pattIdxStart].equals("*") + && path.endsWith(pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternDirs[i].equals("**")) { + return false; + } + } + return true; + } + else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + else if (!fullMatch && "**".equals(patternDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // Up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + final String patDir = patternDirs[pattIdxEnd]; + if (patDir.equals("**")) { + break; + } + if (!matchStrings(patDir, pathDirs[pathIdxEnd], + uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (patternDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + final int patLength = patIdxTmp - pattIdxStart - 1; + final int strLength = pathIdxEnd - pathIdxStart + 1; + int foundIdx = -1; + + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + final String subPat = patternDirs[pattIdxStart + j + 1]; + final String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + *

    + * For example: + *

      + *
    • '/docs/cvs/commit.html' and ' + * /docs/cvs/commit.html -> ''
    • + *
    • '/docs/*' and '/docs/cvs/commit -> ' + * cvs/commit'
    • + *
    • '/docs/cvs/*.html' and ' + * /docs/cvs/commit.html -> 'commit.html'
    • + *
    • '/docs/**' and '/docs/cvs/commit -> ' + * cvs/commit'
    • + *
    • '/docs/**\/*.html' and ' + * /docs/cvs/commit.html -> 'cvs/commit.html'
    • + *
    • '/*.html' and '/docs/cvs/commit.html -> ' + * docs/cvs/commit.html'
    • + *
    • '*.html' and '/docs/cvs/commit.html -> ' + * /docs/cvs/commit.html'
    • + *
    • '*' and '/docs/cvs/commit.html -> ' + * /docs/cvs/commit.html'
    • + *
    + *

    + * Assumes that {@link #match} returns true for ' + * pattern' and 'path', but does + * not enforce this. + */ + public String extractPathWithinPattern(final String pattern, + final String path) { + final String[] patternParts = new StrTokenizer(pattern, pathSeparator) + .setIgnoreEmptyTokens(true).getTokenArray(); + final String[] pathParts = new StrTokenizer(path, pathSeparator) + .setIgnoreEmptyTokens(true).getTokenArray(); + + final StringBuilder builder = new StringBuilder(); + + // Add any path parts that have a wildcarded pattern part. + int puts = 0; + for (int i = 0; i < patternParts.length; i++) { + final String patternPart = patternParts[i]; + if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) + && pathParts.length >= i + 1) { + if (puts > 0 || i == 0 && !pattern.startsWith(pathSeparator)) { + builder.append(pathSeparator); + } + builder.append(pathParts[i]); + puts++; + } + } + + // Append any trailing path parts. + for (int i = patternParts.length; i < pathParts.length; i++) { + if (puts > 0 || i > 0) { + builder.append(pathSeparator); + } + builder.append(pathParts[i]); + } + + return builder.toString(); + } + + public Map extractUriTemplateVariables( + final String pattern, final String path) { + final Map variables = new LinkedHashMap(); + final boolean result = doMatch(pattern, path, true, variables); + Validate.validState(result, "Pattern \"%s\" is not a match for \"%s\"", + pattern, path); + return variables; + } + + public boolean isPattern(final String path) { + return path.indexOf('*') != -1 || path.indexOf('?') != -1; + } + + public boolean match(final String pattern, final String path) { + return doMatch(pattern, path, true, null); + } + + public boolean matchStart(final String pattern, final String path) { + return doMatch(pattern, path, false, null); + } + + /** + * Tests whether or not a string matches against a pattern. The pattern may + * contain two special characters:
    + * '*' means zero or more characters
    + * '?' means one and only one character + * + * @param pattern pattern to match against. Must not be null. + * @param str string which must be matched against the pattern. Must not be + * null. + * @return true if the string matches against the pattern, or + * false otherwise. + */ + private boolean matchStrings(final String pattern, final String str, + final Map uriTemplateVariables) { + final AntPatchStringMatcher matcher = new AntPatchStringMatcher( + pattern, str, uriTemplateVariables); + return matcher.matchStrings(); + } + + /** + * Set the path separator to use for pattern parsing. Default is "/", as in + * Ant. + */ + public void setPathSeparator(final String pathSeparator) { + this.pathSeparator = pathSeparator != null ? pathSeparator + : DEFAULT_PATH_SEPARATOR; + } +} diff --git a/support/src/main/java/org/springframework/roo/support/ant/PathMatcher.java b/support/src/main/java/org/springframework/roo/support/ant/PathMatcher.java new file mode 100644 index 000000000..95b9476f3 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/ant/PathMatcher.java @@ -0,0 +1,97 @@ +package org.springframework.roo.support.ant; + +import java.util.Map; + +/** + * Strategy interface for String-based path matching. + *

    + * The default implementation is {@link AntPathMatcher}, supporting the + * Ant-style pattern syntax. + * + * @author Juergen Hoeller + * @since 1.2.0 + * @see AntPathMatcher + */ +public interface PathMatcher { + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + *

    + * This method is supposed to find out which part of the path is matched + * dynamically through an actual pattern, that is, it strips off a + * statically defined leading path from the given full path, returning only + * the actually pattern-matched part of the path. + *

    + * For example: For "myroot/*.html" as pattern and "myroot/myfile.html" as + * full path, this method should return "myfile.html". The detailed + * determination rules are specified to this PathMatcher's matching + * strategy. + *

    + * A simple implementation may return the given full path as-is in case of + * an actual pattern, and the empty String in case of the pattern not + * containing any dynamic parts (i.e. the pattern parameter + * being a static path that wouldn't qualify as an actual {@link #isPattern + * pattern}). A sophisticated implementation will differentiate between the + * static parts and the dynamic parts of the given path pattern. + * + * @param pattern the path pattern + * @param path the full path to introspect + * @return the pattern-mapped part of the given path (never + * null) + */ + String extractPathWithinPattern(String pattern, String path); + + /** + * Given a pattern and a full path, extract the URI template variables. URI + * template variables are expressed through curly brackets ('{' and '}'). + *

    + * For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this + * method will return a map containing "hotel"->"1". + * + * @param pattern the path pattern, possibly containing URI templates + * @param path the full path to extract template variables from + * @return a map, containing variable names as keys; variables values as + * values + */ + Map extractUriTemplateVariables(String pattern, String path); + + /** + * Does the given path represent a pattern that can be matched + * by an implementation of this interface? + *

    + * If the return value is false, then the {@link #match} method + * does not have to be used because direct equality comparisons on the + * static path Strings will lead to the same result. + * + * @param path the path String to check + * @return true if the given path represents a + * pattern + */ + boolean isPattern(String path); + + /** + * Match the given path against the given pattern, + * according to this PathMatcher's matching strategy. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @return true if the supplied path matched, + * false if it didn't + */ + boolean match(String pattern, String path); + + /** + * Match the given path against the corresponding part of the + * given pattern, according to this PathMatcher's matching + * strategy. + *

    + * Determines whether the pattern at least matches as far as the given base + * path goes, assuming that a full path may then match as well. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @return true if the supplied path matched, + * false if it didn't + */ + boolean matchStart(String pattern, String path); +} diff --git a/support/src/main/java/org/springframework/roo/support/api/AddOnSearch.java b/support/src/main/java/org/springframework/roo/support/api/AddOnSearch.java new file mode 100644 index 000000000..303234313 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/api/AddOnSearch.java @@ -0,0 +1,45 @@ +package org.springframework.roo.support.api; + +import java.util.logging.Logger; + +/** + * Interface defining an add-on search service. + *

    + * This interface is included in the support module because several of Roo's + * core infrastructure modules require add-on search capabilities. + * + * @author Ben Alex + * @author Stefan Schmidt + * @since 1.1.1 + */ +public interface AddOnSearch { + + /** + * Search all add-ons presently known this Roo instance, including add-ons + * which have not been downloaded or installed by the user. + *

    + * Information is optionally emitted to the console via {@link Logger#info}. + * + * @param showFeedback if false will never output any messages to the + * console (required) + * @param searchTerms comma separated list of search terms (required) + * @param refresh attempt a fresh download of roobot.xml (optional) + * @param linesPerResult maximum number of lines per add-on (optional) + * @param maxResults maximum number of results to display (optional) + * @param trustedOnly display only trusted add-ons in search results + * (optional) + * @param compatibleOnly display only compatible add-ons in search results + * (optional) + * @param communityOnly display only community-provided add-ons in search + * results (optional) + * @param requiresCommand display only add-ons which offer the specified + * command (optional) + * @return the total number of matches found, even if only some of these are + * displayed due to maxResults (or null if the add-on list is + * unavailable for some reason, eg network problems etc) + */ + Integer searchAddOns(boolean showFeedback, String searchTerms, + boolean refresh, int linesPerResult, int maxResults, + boolean trustedOnly, boolean compatibleOnly, boolean communityOnly, + String requiresCommand); +} diff --git a/support/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java b/support/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java new file mode 100644 index 000000000..16f9d6715 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java @@ -0,0 +1,137 @@ +package org.springframework.roo.support.logging; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.apache.commons.lang3.Validate; + +/** + * Defers the publication of JDK {@link LogRecord} instances until a target + * {@link Handler} is registered. + *

    + * This class is useful if a target {@link Handler} cannot be instantiated + * before {@link LogRecord} instances are being published. This may be the case + * if the target {@link Handler} requires the establishment of complex + * publication infrastructure such as a GUI, message queue, IoC container and + * the establishment of that infrastructure may produce log messages that should + * ultimately be delivered to the target {@link Handler}. + *

    + * In recognition that sometimes the target {@link Handler} may never be + * registered (perhaps due to failures configuring its supporting + * infrastructure), this class supports a fallback mode. When in fallback mode, + * a fallback {@link Handler} will receive all previous and future + * {@link LogRecord} instances. Fallback mode is automatically triggered if a + * {@link LogRecord} is published at the fallback {@link Level}. Fallback mode + * is also triggered if the {@link #flush()} or {@link #close()} method is + * involved and the target {@link Handler} has never been registered. + * + * @author Ben Alex + * @since 1.0 + */ +public class DeferredLogHandler extends Handler { + + private final Handler fallbackHandler; + private boolean fallbackMode = false; + private final Level fallbackPushLevel; + private final List logRecords = Collections + .synchronizedList(new ArrayList()); + private Handler targetHandler; + + /** + * Creates an instance that will publish all recorded {@link LogRecord} + * instances to the specified fallback {@link Handler} if an event of the + * specified {@link Level} is received. + * + * @param fallbackHandler to publish events to (mandatory) + * @param fallbackPushLevel the level which will trigger an event + * publication (mandatory) + */ + public DeferredLogHandler(final Handler fallbackHandler, + final Level fallbackPushLevel) { + Validate.notNull(fallbackHandler, "Fallback handler required"); + Validate.notNull(fallbackPushLevel, "Fallback push level required"); + this.fallbackHandler = fallbackHandler; + this.fallbackPushLevel = fallbackPushLevel; + } + + @Override + public void close() throws SecurityException { + if (targetHandler == null) { + fallbackMode = true; + } + if (fallbackMode) { + publishLogRecordsTo(fallbackHandler); + fallbackHandler.close(); + return; + } + targetHandler.close(); + } + + @Override + public void flush() { + if (targetHandler == null) { + fallbackMode = true; + } + if (fallbackMode) { + publishLogRecordsTo(fallbackHandler); + fallbackHandler.flush(); + return; + } + targetHandler.flush(); + } + + /** + * @return the target {@link Handler}, or null if there is no target + * {@link Handler} defined so far + */ + public Handler getTargetHandler() { + return targetHandler; + } + + /** + * Stores the log record internally. + */ + @Override + public void publish(final LogRecord record) { + if (!isLoggable(record)) { + return; + } + if (fallbackMode) { + fallbackHandler.publish(record); + return; + } + if (targetHandler != null) { + targetHandler.publish(record); + return; + } + synchronized (logRecords) { + logRecords.add(record); + } + if (!fallbackMode + && record.getLevel().intValue() >= fallbackPushLevel.intValue()) { + fallbackMode = true; + publishLogRecordsTo(fallbackHandler); + } + } + + private void publishLogRecordsTo(final Handler destination) { + synchronized (logRecords) { + for (final LogRecord record : logRecords) { + destination.publish(record); + } + logRecords.clear(); + } + } + + public void setTargetHandler(final Handler targetHandler) { + Validate.notNull(targetHandler, "Must specify a target handler"); + this.targetHandler = targetHandler; + if (!fallbackMode) { + publishLogRecordsTo(this.targetHandler); + } + } +} diff --git a/support/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java b/support/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java new file mode 100644 index 000000000..04d72b772 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java @@ -0,0 +1,169 @@ +package org.springframework.roo.support.logging; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; + +/** + * Utility methods for dealing with {@link Handler} objects. + * + * @author Ben Alex + * @since 1.0 + */ +public final class HandlerUtils { + + /** + * Forces all {@link Handler} instances registered in the presented + * {@link Logger} to be flushed. + * + * @param logger to flush (required) + * @return the number of {@link Handler}s flushed (may be 0 or above) + */ + public static int flushAllHandlers(final Logger logger) { + Validate.notNull(logger, "Logger is required"); + + int flushed = 0; + final Handler[] handlers = logger.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (final Handler h : handlers) { + flushed++; + h.flush(); + } + } + + return flushed; + } + + /** + * Obtains a {@link Logger} that guarantees to set the {@link Level} to + * {@link Level#FINE} if it is part of org.springframework.roo. + * Unfortunately this is needed due to a regression in JDK 1.6.0_18 as per + * issue ROO-539. + * + * @param clazz to retrieve the logger for (required) + * @return the logger, which will at least of {@link Level#FINE} if no level + * was specified + */ + public static Logger getLogger(final Class clazz) { + Validate.notNull(clazz, "Class required"); + final Logger logger = Logger.getLogger(clazz.getName()); + if (logger.getLevel() == null + && clazz.getName().startsWith("org.springframework.roo")) { + logger.setLevel(Level.FINE); + } + return logger; + } + + /** + * Registers the presented target {@link Handler} against any + * {@link DeferredLogHandler} encountered in the presented {@link Logger}. + *

    + * Generally this method is used on {@link Logger} instances that have + * previously been presented to the + * {@link #wrapWithDeferredLogHandler(Logger, Level)} method. + *

    + * The method will return a count of how many {@link DeferredLogHandler} + * instances it detected. Note that no attempt is made to distinguish + * between instances already possessing the intended target {@link Handler} + * or those already possessing any target {@link Handler} at all. This + * method always overwrites the target {@link Handler} and the returned + * count represents how many overwrites took place. + * + * @param logger to introspect for {@link DeferredLogHandler} instances + * (required) + * @param target to set as the target {@link Handler} + * @return number of {@link DeferredLogHandler} instances detected and + * updated (may be 0 if none found) + */ + public static int registerTargetHandler(final Logger logger, + final Handler target) { + Validate.notNull(logger, "Logger is required"); + Validate.notNull(target, "Target handler is required"); + + int replaced = 0; + final Handler[] handlers = logger.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (final Handler h : handlers) { + if (h instanceof DeferredLogHandler) { + replaced++; + final DeferredLogHandler defLogger = (DeferredLogHandler) h; + defLogger.setTargetHandler(target); + } + } + } + + return replaced; + } + + /** + * Replaces each {@link Handler} defined against the presented + * {@link Logger} with {@link DeferredLogHandler}. + *

    + * This is useful for ensuring any {@link Handler} defaults defined by the + * user are preserved and treated as the {@link DeferredLogHandler} + * "fallback" {@link Handler} if the indicated severity {@link Level} is + * encountered. + *

    + * This method will create a {@link ConsoleHandler} if the presented + * {@link Logger} has no current {@link Handler}. + * + * @param logger to introspect and replace the {@link Handler}s for + * (required) + * @param fallbackSeverity to trigger fallback mode (required) + * @return the number of {@link DeferredLogHandler}s now registered against + * the {@link Logger} (guaranteed to be 1 or above) + */ + public static int wrapWithDeferredLogHandler(final Logger logger, + final Level fallbackSeverity) { + Validate.notNull(logger, "Logger is required"); + Validate.notNull(fallbackSeverity, "Fallback severity is required"); + + final List newHandlers = new ArrayList(); + + // Create DeferredLogHandlers for each Handler in presented Logger + final Handler[] handlers = logger.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (final Handler h : handlers) { + logger.removeHandler(h); + newHandlers.add(new DeferredLogHandler(h, fallbackSeverity)); + } + } + + // Create a default DeferredLogHandler if no Handler was defined in the + // presented Logger + if (newHandlers.isEmpty()) { + final ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setFormatter(new Formatter() { + @Override + public String format(final LogRecord record) { + return record.getMessage() + IOUtils.LINE_SEPARATOR; + } + }); + newHandlers.add(new DeferredLogHandler(consoleHandler, + fallbackSeverity)); + } + + // Add the new DeferredLogHandlers to the presented Logger + for (final DeferredLogHandler h : newHandlers) { + logger.addHandler(h); + } + + return newHandlers.size(); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private HandlerUtils() { + } +} diff --git a/support/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java b/support/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java new file mode 100644 index 000000000..4c6caeaaf --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java @@ -0,0 +1,76 @@ +package org.springframework.roo.support.logging; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; + +/** + * Wraps an {@link OutputStream} and automatically passes each line to the + * {@link Logger} when {@link OutputStream#flush()} or + * {@link OutputStream#close()} is called. + * + * @author Ben Alex + * @since 1.1 + */ +public class LoggingOutputStream extends OutputStream { + + protected static final Logger LOGGER = HandlerUtils + .getLogger(LoggingOutputStream.class); + + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private int count; + private final Level level; + private String sourceClassName = LoggingOutputStream.class.getName(); + + /** + * Constructor + * + * @param level the level at which to log (required) + */ + public LoggingOutputStream(final Level level) { + Validate.notNull(level, "A logging level is required"); + this.level = level; + } + + @Override + public void close() throws IOException { + flush(); + } + + @Override + public void flush() throws IOException { + if (count > 0) { + final String msg = new String(baos.toByteArray()); + final LogRecord record = new LogRecord(level, msg); + record.setSourceClassName(sourceClassName); + try { + LOGGER.log(record); + } + finally { + count = 0; + IOUtils.closeQuietly(baos); + baos = new ByteArrayOutputStream(); + } + } + } + + public String getSourceClassName() { + return sourceClassName; + } + + public void setSourceClassName(final String sourceClassName) { + this.sourceClassName = sourceClassName; + } + + @Override + public void write(final int b) throws IOException { + baos.write(b); + count++; + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java b/support/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java new file mode 100644 index 000000000..203ae2477 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java @@ -0,0 +1,65 @@ +package org.springframework.roo.support.util; + +import org.apache.commons.lang3.StringUtils; + +/** + * ANSI escape codes supported by JLine. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public enum AnsiEscapeCode { + + // These int literals are non-public constants in ANSIBuffer.ANSICodes + BLINK(5), BOLD(1), CONCEALED(8), FG_BLACK(30), FG_BLUE(34), FG_CYAN(36), FG_GREEN( + 32), FG_MAGENTA(35), FG_RED(31), FG_WHITE(37), FG_YELLOW(33), OFF(0), REVERSE( + 7), UNDERSCORE(4); + + // Constant for the escape character + private static final boolean ANSI_ENABLED = Boolean + .getBoolean("roo.console.ansi"); + private static final char ESC = 27; + + public static boolean isAnsiEnabled() { + return ANSI_ENABLED; + } + + /** + * Decorates the given text with the given escape codes (turning them off + * afterwards) + * + * @param text the text to decorate; can be null + * @param codes + * @return null if null is passed + */ + public static String decorate(final String text, + final AnsiEscapeCode... codes) { + if (StringUtils.isEmpty(text)) { + return text; + } + + final StringBuilder sb = new StringBuilder(); + if (ANSI_ENABLED) { + for (final AnsiEscapeCode code : codes) { + sb.append(code.code); + } + } + sb.append(text); + if (codes != null && codes.length > 0 && ANSI_ENABLED) { + sb.append(OFF.code); + } + return sb.toString(); + } + + final String code; + + /** + * Constructor + * + * @param code the numeric ANSI escape code + */ + private AnsiEscapeCode(final int code) { + // Copied from the method ANSIBuffer.ANSICodes#attrib(int) + this.code = ESC + "[" + code + "m"; + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/CollectionUtils.java b/support/src/main/java/org/springframework/roo/support/util/CollectionUtils.java new file mode 100644 index 000000000..c5a667918 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/CollectionUtils.java @@ -0,0 +1,136 @@ +package org.springframework.roo.support.util; + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Miscellaneous collection utility methods. Mainly for internal use within the + * framework. + * + * @author Alan Stewart + * @author Andrew Swan + * @since 1.1.3 + */ +public final class CollectionUtils { + + /** + * Convert the supplied array into a List. A primitive array gets converted + * into a List of the appropriate wrapper type. + *

    + * A null source value will be converted to an empty List. + * + * @param source the (potentially primitive) array + * @return the converted List result + * @see ObjectUtils#toObjectArray(Object) + */ + public static List arrayToList(final Object source) { + return Arrays.asList(ObjectUtils.toObjectArray(source)); + } + + /** + * Filters (removes elements from) the given {@link Iterable} using the + * given filter. + * + * @param the type of object being filtered + * @param unfiltered the iterable to filter; can be null + * @param filter the filter to apply; can be null for none + * @return a non-null list + */ + public static List filter(final Iterable unfiltered, + final Filter filter) { + final List filtered = new ArrayList(); + if (unfiltered != null) { + for (final T element : unfiltered) { + if (filter == null || filter.include(element)) { + filtered.add(element); + } + } + } + return filtered; + } + + /** + * Returns the first element of the given collection + * + * @param + * @param collection + * @return null if the first element is null or + * the collection is null or empty + */ + public static T firstElementOf(final Collection collection) { + if (isEmpty(collection)) { + return null; + } + return collection.iterator().next(); + } + + /** + * Return true if the supplied Collection is null + * or empty. Otherwise, return false. + * + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(final Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * Return true if the supplied Map is null or + * empty. Otherwise, return false. + * + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(final Map map) { + return map == null || map.isEmpty(); + } + + /** + * Populates the given collection by replacing any existing contents with + * the given elements, in a null-safe way. + * + * @param the type of element in the collection + * @param collection the collection to populate (can be null) + * @param items the items with which to populate the collection (can be + * null or empty for none) + * @return the given collection (useful if it was anonymous) + */ + public static Collection populate(final Collection collection, + final Collection items) { + if (collection != null) { + collection.clear(); + if (items != null) { + collection.addAll(items); + } + } + return collection; + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private CollectionUtils() { + } +} \ No newline at end of file diff --git a/support/src/main/java/org/springframework/roo/support/util/DomUtils.java b/support/src/main/java/org/springframework/roo/support/util/DomUtils.java new file mode 100644 index 000000000..7c5ac36f6 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/DomUtils.java @@ -0,0 +1,324 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.w3c.dom.CharacterData; +import org.w3c.dom.Comment; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.EntityReference; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Convenience methods for working with the DOM API, in particular for working + * with DOM Nodes and DOM Elements. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Costin Leau + * @author Alan Stewart + * @since 1.2.0 + * @see org.w3c.dom.Node + * @see org.w3c.dom.Element + */ +public final class DomUtils { + + /** + * Creates a child element with the given name and parent. Avoids the type + * of bug whereby the developer calls {@link Document#createElement(String)} + * but forgets to append it to the relevant parent. + * + * @param tagName the name of the new child (required) + * @param parent the parent node (required) + * @param document the document to which the parent and child belong + * (required) + * @return the created element + * @since 1.2.0 + */ + public static Element createChildElement(final String tagName, + final Node parent, final Document document) { + final Element child = document.createElement(tagName); + parent.appendChild(child); + return child; + } + + /** + * Returns the child node with the given tag name, creating it if it does + * not exist. + * + * @param tagName the child tag to look for and possibly create (required) + * @param parent the parent in which to look for the child (required) + * @param document the document containing the parent (required) + * @return the existing or created child (never null) + * @since 1.2.0 + */ + public static Element createChildIfNotExists(final String tagName, + final Node parent, final Document document) { + final Element existingChild = XmlUtils + .findFirstElement(tagName, parent); + if (existingChild != null) { + return existingChild; + } + // No such child; add it + return createChildElement(tagName, parent, document); + } + + /** + * Checks in under a given root element whether it can find a child element + * which matches the name supplied. Returns {@link Element} if exists. + * + * @param name the Element name (required) + * @param root the parent DOM element (required) + * @return the Element if discovered + */ + public static Element findFirstElementByName(final String name, + final Element root) { + Validate.notBlank(name, "Element name required"); + Validate.notNull(root, "Root element required"); + return (Element) root.getElementsByTagName(name).item(0); + } + + /** + * Returns the first child element identified by its name. + * + * @param element the DOM element to analyze + * @param childElementName the child element name to look for + * @return the org.w3c.dom.Element instance, or + * null if none found + */ + public static Element getChildElementByTagName(final Element element, + final String childElementName) { + Validate.notNull(element, "Element must not be null"); + Validate.notNull(childElementName, "Element name must not be null"); + final NodeList nl = element.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + final Node node = nl.item(i); + if (node instanceof Element + && nodeNameMatch(node, childElementName)) { + return (Element) node; + } + } + return null; + } + + /** + * Retrieve all child elements of the given DOM element that match the given + * element name. Only look at the direct child level of the given element; + * do not go into further depth (in contrast to the DOM API's + * getElementsByTagName method). + * + * @param element the DOM element to analyze + * @param childEleName the child element name to look for + * @return a List of child org.w3c.dom.Element instances + * @see org.w3c.dom.Element + * @see org.w3c.dom.Element#getElementsByTagName + */ + public static List getChildElementsByTagName( + final Element element, final String childEleName) { + return getChildElementsByTagName(element, new String[] { childEleName }); + } + + /** + * Retrieve all child elements of the given DOM element that match any of + * the given element names. Only look at the direct child level of the given + * element; do not go into further depth (in contrast to the DOM API's + * getElementsByTagName method). + * + * @param element the DOM element to analyze + * @param childElementNames the child element names to look for + * @return a List of child org.w3c.dom.Element instances + * @see org.w3c.dom.Element + * @see org.w3c.dom.Element#getElementsByTagName + */ + public static List getChildElementsByTagName( + final Element element, final String[] childElementNames) { + Validate.notNull(element, "Element must not be null"); + Validate.notNull(childElementNames, + "Element names collection must not be null"); + final List childEleNameList = Arrays.asList(childElementNames); + final NodeList nl = element.getChildNodes(); + final List childEles = new ArrayList(); + for (int i = 0; i < nl.getLength(); i++) { + final Node node = nl.item(i); + if (node instanceof Element + && nodeNameMatch(node, childEleNameList)) { + childEles.add((Element) node); + } + } + return childEles; + } + + /** + * Returns the first child element value identified by its name. + * + * @param element the DOM element to analyze + * @param childElementName the child element name to look for + * @return the extracted text value, or null if no child + * element found + */ + public static String getChildElementValueByTagName(final Element element, + final String childElementName) { + final Element child = getChildElementByTagName(element, + childElementName); + return child != null ? getTextValue(child) : null; + } + + /** + * Returns the text content of the first child of the given parent that has + * the given tag name, if any. + * + * @param parent the parent in which to search (required) + * @param child the child name for which to search (required) + * @return null if there is no such child, otherwise the first + * such child's text content + */ + public static String getChildTextContent(final Element parent, + final String child) { + final List children = XmlUtils.findElements(child, parent); + if (children.isEmpty()) { + return null; + } + return getTextContent(children.get(0), null); + } + + /** + * Returns the text content of the given {@link Node}, null safe. + * + * @param node can be null + * @param defaultValue the value to return if the node is null + * @return the given default value if the node is null + * @see Node#getTextContent() + * @since 1.2.0 + */ + public static String getTextContent(final Node node, + final String defaultValue) { + if (node == null) { + return defaultValue; + } + return node.getTextContent(); + } + + /** + * Extract the text value from the given DOM element, ignoring XML comments. + *

    + * Appends all CharacterData nodes and EntityReference nodes into a single + * String value, excluding Comment nodes. + * + * @see CharacterData + * @see EntityReference + * @see Comment + */ + public static String getTextValue(final Element valueElement) { + Validate.notNull(valueElement, "Element must not be null"); + final StringBuilder sb = new StringBuilder(); + final NodeList nl = valueElement.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + final Node item = nl.item(i); + if (item instanceof CharacterData && !(item instanceof Comment) + || item instanceof EntityReference) { + sb.append(item.getNodeValue()); + } + } + return sb.toString(); + } + + /** + * Namespace-aware equals comparison. Returns true if either + * {@link Node#getLocalName} or {@link Node#getNodeName} equals + * desiredName, otherwise returns false. + * + * @param node (required) + * @param desiredName (required) + * @return + */ + public static boolean nodeNameEquals(final Node node, + final String desiredName) { + Validate.notNull(node, "Node must not be null"); + Validate.notNull(desiredName, "Desired name must not be null"); + return nodeNameMatch(node, desiredName); + } + + /** + * Matches the given node's name and local name against the given desired + * names. + * + * @param node + * @param desiredNames + * @return + */ + private static boolean nodeNameMatch(final Node node, + final Collection desiredNames) { + return desiredNames.contains(node.getNodeName()) + || desiredNames.contains(node.getLocalName()); + } + + /** + * Matches the given node's name and local name against the given desired + * name. + * + * @param node + * @param desiredName + * @return + */ + private static boolean nodeNameMatch(final Node node, + final String desiredName) { + return desiredName.equals(node.getNodeName()) + || desiredName.equals(node.getLocalName()); + } + + /** + * Removes any elements matching the given XPath expression, relative to the + * given Element + * + * @param xPath the XPath of the element(s) to remove (can be blank) + * @param searchBase the element to which the XPath expression is relative + */ + public static void removeElements(final String xPath, + final Element searchBase) { + for (final Element elementToDelete : XmlUtils.findElements(xPath, + searchBase)) { + final Node parentNode = elementToDelete.getParentNode(); + parentNode.removeChild(elementToDelete); + removeTextNodes(parentNode); + } + } + + /** + * Removes empty text nodes from the specified node. + * + * @param node the element where empty text nodes will be removed + */ + public static void removeTextNodes(final Node node) { + if (node == null) { + return; + } + + final NodeList children = node.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + final Node child = children.item(i); + switch (child.getNodeType()) { + case Node.ELEMENT_NODE: + removeTextNodes(child); + break; + case Node.CDATA_SECTION_NODE: + case Node.TEXT_NODE: + if (StringUtils.isBlank(child.getNodeValue())) { + node.removeChild(child); + } + break; + } + } + } + + /** + * Constructor is private to prevent instantiation + */ + private DomUtils() { + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/FileUtils.java b/support/src/main/java/org/springframework/roo/support/util/FileUtils.java new file mode 100644 index 000000000..48caeeab0 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/FileUtils.java @@ -0,0 +1,272 @@ +package org.springframework.roo.support.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.roo.support.ant.AntPathMatcher; +import org.springframework.roo.support.ant.PathMatcher; + +/** + * Utilities for handling {@link File} instances. + * + * @author Ben Alex + * @since 1.0 + */ +public final class FileUtils { + + private static final String BACKSLASH = "\\"; + + /** + * The relative file path to the current directory. Should be valid on all + * platforms that Roo supports. + */ + public static final String CURRENT_DIRECTORY = "."; + + private static final String ESCAPED_BACKSLASH = "\\\\"; + private static final PathMatcher PATH_MATCHER; + + static { + PATH_MATCHER = new AntPathMatcher(); + ((AntPathMatcher) PATH_MATCHER).setPathSeparator(File.separator); + } + + /** + * Returns the given file system path minus its last element + * + * @param fileIdentifier + * @return + * @since 1.2.0 + */ + public static String backOneDirectory(String fileIdentifier) { + fileIdentifier = StringUtils.removeEnd(fileIdentifier, File.separator); + fileIdentifier = fileIdentifier.substring(0, + fileIdentifier.lastIndexOf(File.separator)); + return StringUtils.removeEnd(fileIdentifier, File.separator); + } + + /** + * Copies the specified source directory to the destination. + *

    + * Both the source must exist. If the destination does not already exist, it + * will be created. If the destination does exist, it must be a directory + * (not a file). + * + * @param source the already-existing source directory (required) + * @param destination the destination directory (required) + * @param deleteDestinationOnExit indicates whether to mark any created + * destinations for deletion on exit + * @return true if the copy was successful + */ + public static boolean copyRecursively(final File source, + final File destination, final boolean deleteDestinationOnExit) { + Validate.notNull(source, "Source directory required"); + Validate.notNull(destination, "Destination directory required"); + Validate.isTrue(source.exists(), "Source directory '%s' must exist", + source); + Validate.isTrue(source.isDirectory(), + "Source directory '%s' must be a directory", source); + if (destination.exists()) { + Validate.isTrue(destination.isDirectory(), + "Destination directory '%s' must be a directory", + destination); + } + else { + destination.mkdirs(); + if (deleteDestinationOnExit) { + destination.deleteOnExit(); + } + } + for (final File s : source.listFiles()) { + final File d = new File(destination, s.getName()); + if (deleteDestinationOnExit) { + d.deleteOnExit(); + } + if (s.isFile()) { + try { + org.apache.commons.io.FileUtils.copyFile(s, d); + } + catch (final IOException ioe) { + return false; + } + } + else { + // It's a sub-directory, so copy it + d.mkdir(); + if (!copyRecursively(s, d, deleteDestinationOnExit)) { + return false; + } + } + } + return true; + } + + /** + * Ensures that the given path has exactly one trailing + * {@link File#separator} + * + * @param path the path to modify (can't be null) + * @return the normalised path + * @since 1.2.0 + */ + public static String ensureTrailingSeparator(final String path) { + Validate.notNull(path); + return StringUtils.stripEnd(path, File.separator) + File.separatorChar; + } + + /** + * Returns the canonical path of the given {@link File}. + * + * @param file the file for which to find the canonical path (can be + * null) + * @return the canonical path, or null if a null + * file is given + * @since 1.2.0 + */ + public static String getCanonicalPath(final File file) { + if (file == null) { + return null; + } + try { + return file.getCanonicalPath(); + } + catch (final IOException ioe) { + throw new IllegalStateException( + "Cannot determine canonical path for '" + file + "'", ioe); + } + } + + /** + * Returns the platform-specific file separator as a regular expression. + * + * @return a non-blank regex + * @since 1.2.0 + */ + public static String getFileSeparatorAsRegex() { + final String fileSeparator = File.separator; + if (fileSeparator.contains(BACKSLASH)) { + // Escape the backslashes + return fileSeparator.replace(BACKSLASH, ESCAPED_BACKSLASH); + } + return fileSeparator; + } + + /** + * Returns the part of the given path that represents a directory, in other + * words the given path if it's already a directory, or the parent directory + * if it's a file. + * + * @param fileIdentifier the path to parse (required) + * @return see above + * @since 1.2.0 + */ + public static String getFirstDirectory(String fileIdentifier) { + fileIdentifier = StringUtils.stripEnd(fileIdentifier, File.separator); + if (new File(fileIdentifier).isDirectory()) { + return fileIdentifier; + } + return backOneDirectory(fileIdentifier); + } + + // + /** + * Loads the given file from the classpath. + * + * @param loadingClass the class from whose package to load the file + * (required) + * @param filename the name of the file to load, relative to that package + * (required) + * @return the file's input stream (never null) + * @throws NullPointerException if the given file cannot be found + */ + public static InputStream getInputStream(final Class loadingClass, + final String filename) { + final InputStream inputStream = loadingClass + .getResourceAsStream(filename); + Validate.notNull(inputStream, + "Could not locate '%s' in classpath of %s", filename, + loadingClass.getName()); + return inputStream; + } + + /** + * Determines the path to the requested file, relative to the given class. + * + * @param loadingClass the class to whose package the given file is relative + * (required) + * @param relativeFilename the name of the file relative to that package + * (required) + * @return the full classloader-specific path to the file (never + * null) + * @since 1.2.0 + */ + public static String getPath(final Class loadingClass, + final String relativeFilename) { + Validate.notNull(loadingClass, "Loading class required"); + Validate.notBlank(relativeFilename, "Filename required"); + Validate.isTrue(!relativeFilename.startsWith("/"), + "Filename shouldn't start with a slash"); + // Slashes instead of File.separatorChar is correct here, as these are + // classloader paths (not file system paths) + return "/" + loadingClass.getPackage().getName().replace('.', '/') + + "/" + relativeFilename; + } + + /** + * Returns an operating-system-dependent path consisting of the given + * elements, separated by {@link File#separator}. + * + * @param pathElements the path elements from uppermost downwards (can't be + * empty) + * @return a non-blank string + * @since 1.2.0 + */ + public static String getSystemDependentPath( + final Collection pathElements) { + Validate.notEmpty(pathElements); + return StringUtils.join(pathElements, File.separator); + } + + /** + * Returns an operating-system-dependent path consisting of the given + * elements, separated by {@link File#separator}. + * + * @param pathElements the path elements from uppermost downwards (can't be + * empty) + * @return a non-blank string + * @since 1.2.0 + */ + public static String getSystemDependentPath(final String... pathElements) { + Validate.notEmpty(pathElements); + return getSystemDependentPath(Arrays.asList(pathElements)); + } + + // + /** + * Indicates whether the given canonical path matches the given Ant-style + * pattern + * + * @param antPattern the pattern to check against (can't be blank) + * @param canonicalPath the path to check (can't be blank) + * @return see above + * @since 1.2.0 + */ + public static boolean matchesAntPath(final String antPattern, + final String canonicalPath) { + Validate.notBlank(antPattern, "Ant pattern required"); + Validate.notBlank(canonicalPath, "Canonical path required"); + return PATH_MATCHER.match(antPattern, canonicalPath); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private FileUtils() { + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/Filter.java b/support/src/main/java/org/springframework/roo/support/util/Filter.java new file mode 100644 index 000000000..6e50c1a54 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/Filter.java @@ -0,0 +1,19 @@ +package org.springframework.roo.support.util; + +/** + * Allows filtering of objects of type T. + * + * @author Andrew Swan + * @since 1.2.0 + * @param the type of object to be filtered + */ +public interface Filter { + + /** + * Indicates whether to include the given instance in the filtered results + * + * @param type the type to evaluate; can be null + * @return false to exclude the given type + */ + boolean include(T instance); +} \ No newline at end of file diff --git a/support/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java b/support/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java new file mode 100644 index 000000000..420473824 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java @@ -0,0 +1,72 @@ +package org.springframework.roo.support.util; + +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.io.IOUtils; +import org.springframework.roo.support.logging.HandlerUtils; + +/** + * Retrieves text files from the classloader and displays them on-screen. + *

    + * Respects normal Roo conventions such as all resources should appear under the + * same package as the bundle itself etc. + * + * @author Ben Alex + * @since 1.1.1 + */ +public abstract class MessageDisplayUtils { + + private static Logger LOGGER = HandlerUtils + .getLogger(MessageDisplayUtils.class); + + /** + * Same as {@link #displayFile(String, Class, boolean)} except it passes + * false as the final argument. + * + * @param fileName the simple filename (required) + * @param owner the class which owns the file (required) + */ + public static void displayFile(final String fileName, final Class owner) { + displayFile(fileName, owner, false); + } + + /** + * Displays the requested file via the LOGGER API. + *

    + * Each file must available from the classloader of the "owner". It must + * also be in the same package as the class of the "owner". So if the owner + * is com.foo.Bar, and the file is called "hello.txt", the file must appear + * in the same bundle as com.foo.Bar and be available from the resource path + * "/com/foo/Hello.txt". + * + * @param fileName the simple filename (required) + * @param owner the class which owns the file (required) + * @param important if true, it will display with a higher importance color + * where possible + */ + public static void displayFile(final String fileName, final Class owner, + final boolean important) { + final Level level = important ? Level.SEVERE : Level.FINE; + final String owningPackage = owner.getPackage().getName() + .replace('.', '/'); + final String fullResourceName = "/" + owningPackage + "/" + fileName; + final InputStream inputStream = owner.getClassLoader() + .getResourceAsStream(fullResourceName); + if (inputStream == null) { + throw new IllegalStateException("Could not locate '" + fileName + + "'"); + } + try { + final String message = IOUtils.toString(inputStream); + LOGGER.log(level, message); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/ObjectUtils.java b/support/src/main/java/org/springframework/roo/support/util/ObjectUtils.java new file mode 100644 index 000000000..96787d8bf --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/ObjectUtils.java @@ -0,0 +1,115 @@ +package org.springframework.roo.support.util; + +import java.lang.reflect.Array; +import java.util.Arrays; + +/** + * Miscellaneous object utility methods. Mainly for internal use within the + * framework; consider Jakarta's Commons Lang for a more comprehensive suite of + * object utilities. + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rod Johnson + * @author Rob Harrop + * @author Alex Ruiz + * @see org.apache.commons.ObjectUtils.ObjectUtils + */ +public final class ObjectUtils { + + /** + * Determine if the given objects are equal, returning true if + * both are null or false if only one is + * null. + *

    + * Compares arrays with Arrays.equals, performing an equality + * check based on the array elements rather than the array reference. + * + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see java.util.Arrays#equals + */ + public static boolean nullSafeEquals(final Object o1, final Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + } + return false; + } + + /** + * Convert the given array (which may be a primitive array) to an object + * array (if necessary of primitive wrapper objects). + *

    + * A null source value will be converted to an empty Object + * array. + * + * @param source the (potentially primitive) array + * @return the corresponding object array (never null) + * @throws IllegalArgumentException if the parameter is not an array + */ + public static Object[] toObjectArray(final Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return new Object[0]; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + + source); + } + final int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + final Class wrapperType = Array.get(source, 0).getClass(); + final Object[] newArray = (Object[]) Array.newInstance(wrapperType, + length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + /** + * Constructor is private to prevent instantiation + */ + private ObjectUtils() { + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/PairList.java b/support/src/main/java/org/springframework/roo/support/util/PairList.java new file mode 100644 index 000000000..0c13db25e --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/PairList.java @@ -0,0 +1,112 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.MutablePair; + +/** + * A {@link List} of {@link ImmutablePair}s. Unlike a {@link java.util.Map}, it + * can have duplicate and/or null keys. + * + * @author Andrew Swan + * @since 1.2.0 + * @param the type of key + * @param the type of value + */ +public class PairList extends ArrayList> { + + // For serialisation + private static final long serialVersionUID = 5990417235907246300L; + + /** + * Constructor for an empty list of pairs + */ + public PairList() { + // Empty + } + + /** + * Constructor for building a list of the given key-value pairs + * + * @param keys the keys (can be null) + * @param values the values (must be null if the keys are null, otherwise + * must be non-null and of the same size as the keys) + */ + public PairList(final List keys, final List values) { + Validate.isTrue(!(keys == null ^ values == null), + "Parameter types and names must either both be null or both be not null"); + if (keys == null) { + Validate.isTrue(values == null, + "Parameter names must be null if types are null"); + } + else { + Validate.isTrue(values != null, + "Parameter names are required if types are provided"); + Validate.isTrue(keys.size() == values.size(), + "Expected %d values but found %d", keys.size(), + values.size()); + for (int i = 0; i < keys.size(); i++) { + add(keys.get(i), values.get(i)); + } + } + } + + /** + * Returns the given array of pairs as a modifiable list. + * + * @param the type of key + * @param the type of value + * @param pairs the pairs to put in a list + * @return a non-null list + */ + public PairList(final MutablePair... pairs) { + addAll(Arrays.asList(pairs)); + } + + /** + * Adds a new pair to this list with the given key and value + * + * @param key the key to add; can be null + * @param value the value to add; can be null + * @return true (as specified by Collection.add(E)) + */ + public boolean add(final K key, final V value) { + return add(new MutablePair(key, value)); + } + + /** + * Returns the keys of each {@link MutablePair} in this list + * + * @return a non-null list + */ + public List getKeys() { + final List keys = new ArrayList(); + for (final MutablePair pair : this) { + keys.add(pair.getKey()); + } + return keys; + } + + /** + * Returns the values of each {@link MutablePair} in this list + * + * @return a non-null modifiable copy of this list + */ + public List getValues() { + final List values = new ArrayList(); + for (final MutablePair pair : this) { + values.add(pair.getValue()); + } + return values; + } + + @SuppressWarnings("unchecked") + @Override + public MutablePair[] toArray() { + return super.toArray(new MutablePair[size()]); + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java b/support/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java new file mode 100644 index 000000000..52f882ceb --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java @@ -0,0 +1,696 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.springframework.roo.support.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.Validate; + +/** + * Simple utility class for working with the reflection API and handling + * reflection exceptions. + *

    + * Only intended for internal use. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rod Johnson + * @author Costin Leau + * @author Sam Brannen + * @since 1.2.2 + */ +@Deprecated +public abstract class ReflectionUtils { + + /** + * Callback interface invoked on each field in the hierarchy. + */ + public static interface FieldCallback { + + /** + * Perform an operation using the given field. + * + * @param field the field to operate on + */ + void doWith(Field field) throws IllegalArgumentException, + IllegalAccessException; + } + + /** + * Callback optionally used to filter fields to be operated on by a field + * callback. + */ + public static interface FieldFilter { + + /** + * Determine whether the given field matches. + * + * @param field the field to check + */ + boolean matches(Field field); + } + + /** + * Action to take on each method. + */ + public static interface MethodCallback { + + /** + * Perform an operation using the given method. + * + * @param method the method to operate on + */ + void doWith(Method method) throws IllegalArgumentException, + IllegalAccessException; + } + + /** + * Callback optionally used to method fields to be operated on by a method + * callback. + */ + public static interface MethodFilter { + + /** + * Determine whether the given method matches. + * + * @param method the method to check + */ + boolean matches(Method method); + } + + /** + * Pre-built FieldFilter that matches all non-static, non-final fields. + */ + public static FieldFilter COPYABLE_FIELDS = new FieldFilter() { + public boolean matches(final Field field) { + return !(Modifier.isStatic(field.getModifiers()) || Modifier + .isFinal(field.getModifiers())); + } + }; + + /** + * Determine whether the given method explicitly declares the given + * exception or one of its superclasses, which means that an exception of + * that type can be propagated as-is within a reflective invocation. + * + * @param method the declaring method + * @param exceptionType the exception to throw + * @return true if the exception can be thrown as-is; + * false if it needs to be wrapped + */ + public static boolean declaresException(final Method method, + final Class exceptionType) { + Validate.notNull(method, "Method must not be null"); + final Class[] declaredExceptions = method.getExceptionTypes(); + for (final Class declaredException : declaredExceptions) { + if (declaredException.isAssignableFrom(exceptionType)) { + return true; + } + } + return false; + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * + * @param targetClass the target class to analyze + * @param fc the callback to invoke for each field + */ + public static void doWithFields(final Class targetClass, + final FieldCallback fc) throws IllegalArgumentException { + doWithFields(targetClass, fc, null); + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * + * @param targetClass the target class to analyze + * @param fc the callback to invoke for each field + * @param ff the filter that determines the fields to apply the callback to + */ + public static void doWithFields(Class targetClass, + final FieldCallback fc, final FieldFilter ff) + throws IllegalArgumentException { + // Keep backing up the inheritance hierarchy. + do { + // Copy each field declared on this class unless it's static or + // file. + final Field[] fields = targetClass.getDeclaredFields(); + for (final Field field : fields) { + // Skip static and final fields. + if (ff != null && !ff.matches(field)) { + continue; + } + try { + fc.doWith(field); + } + catch (final IllegalAccessException ex) { + throw new IllegalStateException( + "Shouldn't be illegal to access field '" + + field.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } while (targetClass != null && targetClass != Object.class); + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses. + *

    + * The same named method occurring on subclass and superclass will appear + * twice, unless excluded by a {@link MethodFilter}. + * + * @param targetClass class to start looking at + * @param mc the callback to invoke for each method + * @see #doWithMethods(Class, MethodCallback, MethodFilter) + */ + public static void doWithMethods(final Class targetClass, + final MethodCallback mc) throws IllegalArgumentException { + doWithMethods(targetClass, mc, null); + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses. + *

    + * The same named method occurring on subclass and superclass will appear + * twice, unless excluded by the specified {@link MethodFilter}. + * + * @param targetClass class to start looking at + * @param mc the callback to invoke for each method + * @param mf the filter that determines the methods to apply the callback to + */ + public static void doWithMethods(Class targetClass, + final MethodCallback mc, final MethodFilter mf) + throws IllegalArgumentException { + // Keep backing up the inheritance hierarchy. + do { + final Method[] methods = targetClass.getDeclaredMethods(); + for (final Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + mc.doWith(method); + } + catch (final IllegalAccessException ex) { + throw new IllegalStateException( + "Shouldn't be illegal to access method '" + + method.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } while (targetClass != null); + } + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with + * the supplied name. Searches all superclasses up to + * {@link Object}. + * + * @param clazz the class to introspect + * @param name the name of the field + * @return the corresponding Field object, or null if not found + */ + public static Field findField(final Class clazz, final String name) { + return findField(clazz, name, null); + } + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with + * the supplied name and/or {@link Class type}. Searches all + * superclasses up to {@link Object}. + * + * @param clazz the class to introspect + * @param name the name of the field (may be null if type is + * specified) + * @param type the type of the field (may be null if name is + * specified) + * @return the corresponding Field object, or null if not found + */ + public static Field findField(final Class clazz, final String name, + final Class type) { + Validate.notNull(clazz, "Class must not be null"); + Validate.isTrue(name != null || type != null, + "Either name or type of the field must be specified"); + Class searchType = clazz; + while (!Object.class.equals(searchType) && searchType != null) { + final Field[] fields = searchType.getDeclaredFields(); + for (final Field field : fields) { + if ((name == null || name.equals(field.getName())) + && (type == null || type.equals(field.getType()))) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied + * name and no parameters. Searches all superclasses up to + * Object. + *

    + * Returns null if no {@link Method} can be found. + * + * @param clazz the class to introspect + * @param name the name of the method + * @return the Method object, or null if none found + */ + public static Method findMethod(final Class clazz, final String name) { + return findMethod(clazz, name, new Class[0]); + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied + * name and parameter types. Searches all superclasses up to + * Object. + *

    + * Returns null if no {@link Method} can be found. + * + * @param clazz the class to introspect + * @param name the name of the method + * @param parameterTypes the parameter types of the method (may be + * null to indicate any signature) + * @return the Method object, or null if none found + */ + public static Method findMethod(final Class clazz, final String name, + final Class[] parameterTypes) { + Validate.notNull(clazz, "Class must not be null"); + Validate.notNull(name, "Method name must not be null"); + Class searchType = clazz; + while (!Object.class.equals(searchType) && searchType != null) { + final Method[] methods = searchType.isInterface() ? searchType + .getMethods() : searchType.getDeclaredMethods(); + for (final Method method : methods) { + if (name.equals(method.getName()) + && (parameterTypes == null || Arrays.equals( + parameterTypes, method.getParameterTypes()))) { + return method; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Get all declared methods on the leaf class and all superclasses. Leaf + * class methods are included first. + */ + public static Method[] getAllDeclaredMethods(final Class leafClass) + throws IllegalArgumentException { + final List methods = new ArrayList(32); + doWithMethods(leafClass, new MethodCallback() { + public void doWith(final Method method) { + methods.add(method); + } + }); + return methods.toArray(new Method[methods.size()]); + } + + /** + * Get the field represented by the supplied {@link Field field object} on + * the specified {@link Object target object}. In accordance with + * {@link Field#get(Object)} semantics, the returned value is automatically + * wrapped if the underlying field has a primitive type. + *

    + * Thrown exceptions are handled via a call to + * {@link #handleReflectionException(Exception)}. + * + * @param field the field to get + * @param target the target object from which to get the field + * @return the field's current value + */ + public static Object getField(final Field field, final Object target) { + try { + return field.get(target); + } + catch (final IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException( + "Unexpected reflection exception - " + + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * Handle the given invocation target exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + *

    + * Throws the underlying RuntimeException or Error in case of such a root + * cause. Throws an IllegalStateException else. + * + * @param ex the invocation target exception to handle + */ + public static void handleInvocationTargetException( + final InvocationTargetException ex) { + rethrowRuntimeException(ex.getTargetException()); + } + + /** + * Handle the given reflection exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + *

    + * Throws the underlying RuntimeException or Error in case of an + * InvocationTargetException with such a root cause. Throws an + * IllegalStateException with an appropriate message else. + * + * @param ex the reflection exception to handle + */ + public static void handleReflectionException(final Exception ex) { + if (ex instanceof NoSuchMethodException) { + throw new IllegalStateException("Method not found: " + + ex.getMessage()); + } + if (ex instanceof IllegalAccessException) { + throw new IllegalStateException("Could not access method: " + + ex.getMessage()); + } + if (ex instanceof InvocationTargetException) { + handleInvocationTargetException((InvocationTargetException) ex); + } + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + handleUnexpectedException(ex); + } + + /** + * Throws an IllegalStateException with the given exception as root cause. + * + * @param ex the unexpected exception + */ + private static void handleUnexpectedException(final Throwable ex) { + throw new IllegalStateException("Unexpected exception thrown", ex); + } + + /** + * Invoke the specified JDBC API {@link Method} against the supplied target + * object with no arguments. + * + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @throws SQLException the JDBC API SQLException to rethrow (if any) + * @see #invokeJdbcMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeJdbcMethod(final Method method, + final Object target) throws SQLException { + return invokeJdbcMethod(method, target, null); + } + + /** + * Invoke the specified JDBC API {@link Method} against the supplied target + * object with the supplied arguments. + * + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be null) + * @return the invocation result, if any + * @throws SQLException the JDBC API SQLException to rethrow (if any) + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeJdbcMethod(final Method method, + final Object target, final Object[] args) throws SQLException { + try { + return method.invoke(target, args); + } + catch (final IllegalAccessException ex) { + handleReflectionException(ex); + } + catch (final InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + handleInvocationTargetException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Invoke the specified {@link Method} against the supplied target object + * with no arguments. The target object can be null when + * invoking a static {@link Method}. + *

    + * Thrown exceptions are handled via a call to + * {@link #handleReflectionException}. + * + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeMethod(final Method method, final Object target) { + return invokeMethod(method, target, null); + } + + /** + * Invoke the specified {@link Method} against the supplied target object + * with the supplied arguments. The target object can be null + * when invoking a static {@link Method}. + *

    + * Thrown exceptions are handled via a call to + * {@link #handleReflectionException}. + * + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be null) + * @return the invocation result, if any + */ + public static Object invokeMethod(final Method method, final Object target, + final Object[] args) { + try { + return method.invoke(target, args); + } + catch (final Exception ex) { + handleReflectionException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Determine whether the given method is an "equals" method. + * + * @see java.lang.Object#equals + */ + public static boolean isEqualsMethod(final Method method) { + if (method == null || !method.getName().equals("equals")) { + return false; + } + final Class[] parameterTypes = method.getParameterTypes(); + return parameterTypes.length == 1 && parameterTypes[0] == Object.class; + } + + /** + * Determine whether the given method is a "hashCode" method. + * + * @see java.lang.Object#hashCode + */ + public static boolean isHashCodeMethod(final Method method) { + return method != null && method.getName().equals("hashCode") + && method.getParameterTypes().length == 0; + } + + /** + * Determine whether the given field is a "public static final" constant. + * + * @param field the field to check + */ + public static boolean isPublicStaticFinal(final Field field) { + final int modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) + && Modifier.isFinal(modifiers); + } + + /** + * Determine whether the given method is a "toString" method. + * + * @see java.lang.Object#toString() + */ + public static boolean isToStringMethod(final Method method) { + return method != null && method.getName().equals("toString") + && method.getParameterTypes().length == 0; + } + + /** + * Make the given constructor accessible, explicitly setting it accessible + * if necessary. The setAccessible(true) method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * + * @param ctor the constructor to make accessible + * @see java.lang.reflect.Constructor#setAccessible + */ + public static void makeAccessible(final Constructor ctor) { + if (!Modifier.isPublic(ctor.getModifiers()) + || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) { + ctor.setAccessible(true); + } + } + + /** + * Make the given field accessible, explicitly setting it accessible if + * necessary. The setAccessible(true) method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * + * @param field the field to make accessible + * @see java.lang.reflect.Field#setAccessible + */ + public static void makeAccessible(final Field field) { + if (!Modifier.isPublic(field.getModifiers()) + || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) { + field.setAccessible(true); + } + } + + /** + * Make the given method accessible, explicitly setting it accessible if + * necessary. The setAccessible(true) method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * + * @param method the method to make accessible + * @see java.lang.reflect.Method#setAccessible + */ + public static void makeAccessible(final Method method) { + if (!Modifier.isPublic(method.getModifiers()) + || !Modifier + .isPublic(method.getDeclaringClass().getModifiers())) { + method.setAccessible(true); + } + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * target exception of an {@link InvocationTargetException}. Should + * only be called if no checked exception is expected to be thrown by the + * target method. + *

    + * Rethrows the underlying exception cast to an {@link Exception} or + * {@link Error} if appropriate; otherwise, throws an + * {@link IllegalStateException}. + * + * @param ex the exception to rethrow + * @throws Exception the rethrown exception (in case of a checked exception) + */ + public static void rethrowException(final Throwable ex) throws Exception { + if (ex instanceof Exception) { + throw (Exception) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + handleUnexpectedException(ex); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * target exception of an {@link InvocationTargetException}. Should + * only be called if no checked exception is expected to be thrown by the + * target method. + *

    + * Rethrows the underlying exception cast to an {@link RuntimeException} or + * {@link Error} if appropriate; otherwise, throws an + * {@link IllegalStateException}. + * + * @param ex the exception to rethrow + * @throws RuntimeException the rethrown exception + */ + public static void rethrowRuntimeException(final Throwable ex) { + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + handleUnexpectedException(ex); + } + + /** + * Set the field represented by the supplied {@link Field field object} on + * the specified {@link Object target object} to the specified + * value. In accordance with {@link Field#set(Object, Object)} + * semantics, the new value is automatically unwrapped if the underlying + * field has a primitive type. + *

    + * Thrown exceptions are handled via a call to + * {@link #handleReflectionException(Exception)}. + * + * @param field the field to set + * @param target the target object on which to set the field + * @param value the value to set; may be null + */ + public static void setField(final Field field, final Object target, + final Object value) { + try { + field.set(target, value); + } + catch (final IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException( + "Unexpected reflection exception - " + + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * Given the source object and the destination, which must be the same class + * or a subclass, copy all fields, including inherited fields. Designed to + * work on objects with public no-arg constructors. + * + * @throws IllegalArgumentException if the arguments are incompatible + */ + public static void shallowCopyFieldState(final Object src, final Object dest) + throws IllegalArgumentException { + if (src == null) { + throw new IllegalArgumentException( + "Source for field copy cannot be null"); + } + if (dest == null) { + throw new IllegalArgumentException( + "Destination for field copy cannot be null"); + } + if (!src.getClass().isAssignableFrom(dest.getClass())) { + throw new IllegalArgumentException("Destination class [" + + dest.getClass().getName() + + "] must be same or subclass as source class [" + + src.getClass().getName() + "]"); + } + doWithFields(src.getClass(), new FieldCallback() { + public void doWith(final Field field) + throws IllegalArgumentException, IllegalAccessException { + makeAccessible(field); + final Object srcValue = field.get(src); + field.set(dest, srcValue); + } + }, COPYABLE_FIELDS); + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/WebXmlUtils.java b/support/src/main/java/org/springframework/roo/support/util/WebXmlUtils.java new file mode 100644 index 000000000..741747a85 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/WebXmlUtils.java @@ -0,0 +1,825 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.Pair; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Helper util class to allow more convenient handling of web.xml file in Web + * projects. + * + * @author Stefan Schmidt + * @since 1.1 + */ +public final class WebXmlUtils { + + /** + * Enum to define dispatcher + * + * @author Stefan Schmidt + * @since 1.1.1 + */ + public static enum Dispatcher { + ERROR, FORWARD, INCLUDE, REQUEST; + } + + /** + * Enum to define filter position + * + * @author Stefan Schmidt + * @since 1.1 + */ + public static enum FilterPosition { + AFTER, BEFORE, BETWEEN, FIRST, LAST; + } + + /** + * Convenience class for passing a web-resource-collection element's details + * + * @since 1.1.1 + */ + public static class WebResourceCollection { + private final String description; + private final List httpMethods; + private final List urlPatterns; + private final String webResourceName; + + public WebResourceCollection(final String webResourceName, + final String description, final List urlPatterns, + final List httpMethods) { + this.webResourceName = webResourceName; + this.description = description; + this.urlPatterns = urlPatterns; + this.httpMethods = httpMethods; + } + + public String getDescription() { + return description; + } + + public List getHttpMethods() { + return httpMethods; + } + + public List getUrlPatterns() { + return urlPatterns; + } + + public String getWebResourceName() { + return webResourceName; + } + } + + /** + * Value object that holds init-param style information + * + * @author Stefan Schmidt + * @since 1.1 + */ + public static class WebXmlParam extends Pair { + + private static final long serialVersionUID = -1134907409024055399L; + private final String name; + private final String value; + + public WebXmlParam(final String name, final String value) { + this.name = name; + this.value = value; + } + + @Override + public String getLeft() { + return name; + } + + @Override + public String getRight() { + return value; + } + + public String getName() { + return getLeft(); + } + + public String setValue(final String value) { + throw new UnsupportedOperationException(); + } + } + + private static final String WEB_APP_XPATH = "/web-app/"; + + private static final String WHITESPACE = "[ \t\r\n]"; + + private static void addCommentBefore(final Element element, + final String comment, final Document document) { + if (null == XmlUtils.findNode("//comment()[.=' " + comment + " ']", + document.getDocumentElement())) { + document.getDocumentElement().insertBefore( + document.createComment(" " + comment + " "), element); + addLineBreakBefore(element, document); + } + } + + /** + * Add a context param to the web.xml document + * + * @param contextParam (required) + * @param document the web.xml document (required) + * @param comment (optional) + */ + public static void addContextParam(final WebXmlParam contextParam, + final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notNull(contextParam, "Context param required"); + + Element contextParamElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "context-param[param-name = '" + contextParam.getName() + + "']", document.getDocumentElement()); + if (contextParamElement == null) { + contextParamElement = new XmlElementBuilder("context-param", + document).addChild( + new XmlElementBuilder("param-name", document).setText( + contextParam.getName()).build()).build(); + insertBetween(contextParamElement, "description[last()]", "filter", + document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(contextParamElement, comment, document); + } + } + appendChildIfNotPresent(contextParamElement, new XmlElementBuilder( + "param-value", document).setText(contextParam.getValue()) + .build()); + } + + /** + * Add error code to web.xml document + * + * @param errorCode (required) + * @param location (required) + * @param document (required) + * @param comment (optional) + */ + public static void addErrorCode(final Integer errorCode, + final String location, final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notNull(errorCode, "Error code required"); + Validate.notBlank(location, "Location required"); + + Element errorPageElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "error-page[error-code = '" + errorCode.toString() + "']", + document.getDocumentElement()); + if (errorPageElement == null) { + errorPageElement = new XmlElementBuilder("error-page", document) + .addChild( + new XmlElementBuilder("error-code", document) + .setText(errorCode.toString()).build()) + .build(); + insertBetween(errorPageElement, "welcome-file-list[last()]", + "the-end", document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(errorPageElement, comment, document); + } + } + appendChildIfNotPresent(errorPageElement, new XmlElementBuilder( + "location", document).setText(location).build()); + } + + /** + * Add exception type to web.xml document + * + * @param exceptionType fully qualified exception type name (required) + * @param location (required) + * @param document (required) + * @param comment (optional) + */ + public static void addExceptionType(final String exceptionType, + final String location, final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notBlank(exceptionType, + "Fully qualified exception type name required"); + Validate.notBlank(location, "location required"); + + Element errorPageElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "error-page[exception-type = '" + exceptionType + "']", + document.getDocumentElement()); + if (errorPageElement == null) { + errorPageElement = new XmlElementBuilder("error-page", document) + .addChild( + new XmlElementBuilder("exception-type", document) + .setText(exceptionType).build()).build(); + insertBetween(errorPageElement, "welcome-file-list[last()]", + "the-end", document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(errorPageElement, comment, document); + } + } + appendChildIfNotPresent(errorPageElement, new XmlElementBuilder( + "location", document).setText(location).build()); + } + + /** + * Add a new filter definition to web.xml document. The filter will be added + * AFTER (FilterPosition.LAST) all existing filters. + * + * @param filterName (required) + * @param filterClass the fully qualified name of the filter type (required) + * @param urlPattern (required) + * @param document the web.xml document (required) + * @param comment (optional) + * @param initParams a vararg of initial parameters (optional) + */ + public static void addFilter(final String filterName, + final String filterClass, final String urlPattern, + final Document document, final String comment, + final WebXmlParam... initParams) { + addFilterAtPosition(FilterPosition.LAST, null, null, filterName, + filterClass, urlPattern, document, comment, initParams); + } + + /** + * Add a new filter definition to web.xml document. The filter will be added + * at the FilterPosition specified. + * + * @param filterPosition Filter position (required) + * @param beforeFilterName (optional for filter position FIRST and LAST, + * required for BEFORE and AFTER) + * @param filterName (required) + * @param filterClass the fully qualified name of the filter type (required) + * @param urlPattern (required) + * @param document the web.xml document (required) + * @param comment (optional) + * @param initParams (optional) + * @param dispatchers (optional) + */ + public static void addFilterAtPosition(final FilterPosition filterPosition, + final String afterFilterName, final String beforeFilterName, + final String filterName, final String filterClass, + final String urlPattern, final Document document, + final String comment, List initParams, + final List dispatchers) { + Validate.notNull(document, "Web XML document required"); + Validate.notBlank(filterName, "Filter name required"); + Validate.notBlank(filterClass, "Filter class required"); + Validate.notNull(urlPattern, "Filter URL mapping pattern required"); + + if (initParams == null) { + initParams = new ArrayList(); + } + + // Creating filter + Element filterElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "filter[filter-name = '" + filterName + "']", + document.getDocumentElement()); + if (filterElement == null) { + filterElement = new XmlElementBuilder("filter", document).addChild( + new XmlElementBuilder("filter-name", document).setText( + filterName).build()).build(); + if (filterPosition.equals(FilterPosition.FIRST)) { + insertBetween(filterElement, "context-param", "filter", + document); + } + else if (filterPosition.equals(FilterPosition.BEFORE)) { + Validate.notBlank(beforeFilterName, + "The filter position filter name is required when using FilterPosition.BEFORE"); + insertBefore(filterElement, "filter[filter-name = '" + + beforeFilterName + "']", document); + } + else if (filterPosition.equals(FilterPosition.AFTER)) { + Validate.notBlank(afterFilterName, + "The filter position filter name is required when using FilterPosition.AFTER"); + insertAfter(filterElement, "filter[filter-name = '" + + afterFilterName + "']", document); + } + else if (filterPosition.equals(FilterPosition.BETWEEN)) { + Validate.notBlank(beforeFilterName, + "The 'before' filter name is required when using FilterPosition.BETWEEN"); + Validate.notBlank(afterFilterName, + "The 'after' filter name is required when using FilterPosition.BETWEEN"); + insertBetween(filterElement, "filter[filter-name = '" + + afterFilterName + "']", "filter[filter-name = '" + + beforeFilterName + "']", document); + } + else { + insertBetween(filterElement, "context-param[last()]", + "filter-mapping", document); + } + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(filterElement, comment, document); + } + } + appendChildIfNotPresent(filterElement, new XmlElementBuilder( + "filter-class", document).setText(filterClass).build()); + for (final WebXmlParam initParam : initParams) { + appendChildIfNotPresent( + filterElement, + new XmlElementBuilder("init-param", document) + .addChild( + new XmlElementBuilder("param-name", + document).setText( + initParam.getName()).build()) + .addChild( + new XmlElementBuilder("param-value", + document).setText( + initParam.getValue()).build()) + .build()); + } + + // Creating filter mapping + Element filterMappingElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "filter-mapping[filter-name = '" + filterName + "']", + document.getDocumentElement()); + if (filterMappingElement == null) { + filterMappingElement = new XmlElementBuilder("filter-mapping", + document).addChild( + new XmlElementBuilder("filter-name", document).setText( + filterName).build()).build(); + if (filterPosition.equals(FilterPosition.FIRST)) { + insertBetween(filterMappingElement, "filter", "filter-mapping", + document); + } + else if (filterPosition.equals(FilterPosition.BEFORE)) { + insertBefore(filterMappingElement, + "filter-mapping[filter-name = '" + beforeFilterName + + "']", document); + } + else if (filterPosition.equals(FilterPosition.AFTER)) { + insertAfter(filterMappingElement, + "filter-mapping[filter-name = '" + beforeFilterName + + "']", document); + } + else if (filterPosition.equals(FilterPosition.BETWEEN)) { + insertBetween(filterMappingElement, + "filter-mapping[filter-name = '" + afterFilterName + + "']", "filter-mapping[filter-name = '" + + beforeFilterName + "']", document); + } + else { + insertBetween(filterMappingElement, "filter-mapping[last()]", + "listener", document); + } + } + appendChildIfNotPresent(filterMappingElement, new XmlElementBuilder( + "url-pattern", document).setText(urlPattern).build()); + for (final Dispatcher dispatcher : dispatchers) { + appendChildIfNotPresent( + filterMappingElement, + new XmlElementBuilder("dispatcher", document).setText( + dispatcher.name()).build()); + } + } + + /** + * Add a new filter definition to web.xml document. The filter will be added + * at the FilterPosition specified. + * + * @param filterPosition Filter position (required) + * @param beforeFilterName (optional for filter position FIRST and LAST, + * required for BEFORE and AFTER) + * @param filterName (required) + * @param filterClass the fully qualified name of the filter type (required) + * @param urlPattern (required) + * @param document the web.xml document (required) + * @param comment (optional) + * @param initParams (optional) + */ + public static void addFilterAtPosition(final FilterPosition filterPosition, + final String afterFilterName, final String beforeFilterName, + final String filterName, final String filterClass, + final String urlPattern, final Document document, + final String comment, final WebXmlParam... initParams) { + addFilterAtPosition( + filterPosition, + afterFilterName, + beforeFilterName, + filterName, + filterClass, + urlPattern, + document, + comment, + initParams == null ? new ArrayList() : Arrays + .asList(initParams), new ArrayList()); + } + + private static void addLineBreakBefore(final Element element, + final Document document) { + document.getDocumentElement().insertBefore( + document.createTextNode("\n "), element); + } + + /** + * Add listener element to web.xml document + * + * @param className the fully qualified name of the listener type (required) + * @param document (required) + * @param comment (optional) + */ + public static void addListener(final String className, + final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notBlank(className, "Class name required"); + + Element listenerElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "listener[listener-class = '" + className + "']", + document.getDocumentElement()); + if (listenerElement == null) { + listenerElement = new XmlElementBuilder("listener", document) + .addChild( + new XmlElementBuilder("listener-class", document) + .setText(className).build()).build(); + insertBetween(listenerElement, "filter-mapping[last()]", "servlet", + document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(listenerElement, comment, document); + } + } + } + + /** + * Add a security constraint to a web.xml document + * + * @param displayName (optional) + * @param webResourceCollections (required) + * @param roleNames (optional) + * @param transportGuarantee (optional) + * @param document (required) + * @param comment (optional) + */ + public static void addSecurityConstraint(final String displayName, + final List webResourceCollections, + final List roleNames, final String transportGuarantee, + final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.isTrue( + !CollectionUtils.isEmpty(webResourceCollections), + "A security-constraint element must contain at least one web-resource-collection"); + + Element securityConstraintElement = XmlUtils.findFirstElement( + "security-constraint", document.getDocumentElement()); + if (securityConstraintElement == null) { + securityConstraintElement = document + .createElement("security-constraint"); + insertAfter(securityConstraintElement, "session-config[last()]", + document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(securityConstraintElement, comment, document); + } + } + + if (StringUtils.isNotBlank(displayName)) { + appendChildIfNotPresent( + securityConstraintElement, + new XmlElementBuilder("display-name", document).setText( + displayName).build()); + } + + for (final WebResourceCollection webResourceCollection : webResourceCollections) { + final XmlElementBuilder webResourceCollectionBuilder = new XmlElementBuilder( + "web-resource-collection", document); + Validate.notBlank(webResourceCollection.getWebResourceName(), + "web-resource-name is required"); + webResourceCollectionBuilder.addChild(new XmlElementBuilder( + "web-resource-name", document).setText( + webResourceCollection.getWebResourceName()).build()); + if (StringUtils.isNotBlank(webResourceCollection.getDescription())) { + webResourceCollectionBuilder.addChild(new XmlElementBuilder( + "description", document).setText( + webResourceCollection.getWebResourceName()).build()); + } + for (final String urlPattern : webResourceCollection + .getUrlPatterns()) { + if (StringUtils.isNotBlank(urlPattern)) { + webResourceCollectionBuilder + .addChild(new XmlElementBuilder("url-pattern", + document).setText(urlPattern).build()); + } + } + for (final String httpMethod : webResourceCollection + .getHttpMethods()) { + if (StringUtils.isNotBlank(httpMethod)) { + webResourceCollectionBuilder + .addChild(new XmlElementBuilder("http-method", + document).setText(httpMethod).build()); + } + } + appendChildIfNotPresent(securityConstraintElement, + webResourceCollectionBuilder.build()); + } + + if (roleNames != null && roleNames.size() > 0) { + final XmlElementBuilder authConstraintBuilder = new XmlElementBuilder( + "auth-constraint", document); + for (final String roleName : roleNames) { + if (StringUtils.isNotBlank(roleName)) { + authConstraintBuilder.addChild(new XmlElementBuilder( + "role-name", document).setText(roleName).build()); + } + } + appendChildIfNotPresent(securityConstraintElement, + authConstraintBuilder.build()); + } + + if (StringUtils.isNotBlank(transportGuarantee)) { + final XmlElementBuilder userDataConstraintBuilder = new XmlElementBuilder( + "user-data-constraint", document); + userDataConstraintBuilder.addChild(new XmlElementBuilder( + "transport-guarantee", document) + .setText(transportGuarantee).build()); + appendChildIfNotPresent(securityConstraintElement, + userDataConstraintBuilder.build()); + } + } + + /** + * Add servlet element to the web.xml document + * + * @param servletName (required) + * @param className the fully qualified name of the servlet type (required) + * @param urlPattern this can be set to null in which case the servletName + * will be used for mapping (optional) + * @param loadOnStartup (optional) + * @param document (required) + * @param comment (optional) + * @param initParams (optional) + */ + public static void addServlet(final String servletName, + final String className, final String urlPattern, + final Integer loadOnStartup, final Document document, + final String comment, final WebXmlParam... initParams) { + Validate.notNull(document, "Web XML document required"); + Validate.notBlank(servletName, "Servlet name required"); + Validate.notBlank(className, "Fully qualified class name required"); + + // Create servlet + Element servletElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "servlet[servlet-name = '" + servletName + "']", + document.getDocumentElement()); + if (servletElement == null) { + servletElement = new XmlElementBuilder("servlet", document) + .addChild( + new XmlElementBuilder("servlet-name", document) + .setText(servletName).build()).build(); + insertBetween(servletElement, "listener[last()]", + "servlet-mapping", document); + if (comment != null && comment.length() > 0) { + addCommentBefore(servletElement, comment, document); + } + } + appendChildIfNotPresent(servletElement, new XmlElementBuilder( + "servlet-class", document).setText(className).build()); + for (final WebXmlParam initParam : initParams) { + appendChildIfNotPresent( + servletElement, + new XmlElementBuilder("init-param", document) + .addChild( + new XmlElementBuilder("param-name", + document).setText( + initParam.getName()).build()) + .addChild( + new XmlElementBuilder("param-value", + document).setText( + initParam.getValue()).build()) + .build()); + } + if (loadOnStartup != null) { + appendChildIfNotPresent( + servletElement, + new XmlElementBuilder("load-on-startup", document).setText( + loadOnStartup.toString()).build()); + } + + // Create servlet mapping + Element servletMappingElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "servlet-mapping[servlet-name = '" + servletName + "']", + document.getDocumentElement()); + if (servletMappingElement == null) { + servletMappingElement = new XmlElementBuilder("servlet-mapping", + document).addChild( + new XmlElementBuilder("servlet-name", document).setText( + servletName).build()).build(); + insertBetween(servletMappingElement, "servlet[last()]", + "session-config", document); + } + if (StringUtils.isNotBlank(urlPattern)) { + appendChildIfNotPresent( + servletMappingElement, + new XmlElementBuilder("url-pattern", document).setText( + urlPattern).build()); + } + else { + appendChildIfNotPresent( + servletMappingElement, + new XmlElementBuilder("servlet-name", document).setText( + servletName).build()); + } + } + + /** + * Add a welcome file definition to web.xml document + * + * @param path (required) + * @param document (required) + * @param comment (optional) + */ + public static void addWelcomeFile(final String path, + final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notBlank("Path required"); + + Element welcomeFileElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "welcome-file-list", document.getDocumentElement()); + if (welcomeFileElement == null) { + welcomeFileElement = document.createElement("welcome-file-list"); + insertBetween(welcomeFileElement, "session-config[last()]", + "error-page", document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(welcomeFileElement, comment, document); + } + } + appendChildIfNotPresent(welcomeFileElement, new XmlElementBuilder( + "welcome-file", document).setText(path).build()); + } + + /** + * Adds the given child to the given parent if it's not already there + * + * @param parent the parent to which to add a child (required) + * @param child the child to add if not present (required) + */ + private static void appendChildIfNotPresent(final Node parent, + final Element child) { + final NodeList existingChildren = parent.getChildNodes(); + for (int i = 0; i < existingChildren.getLength(); i++) { + final Node existingChild = existingChildren.item(i); + if (existingChild instanceof Element) { + // Attempt matching of possibly nested structures by using of + // 'getTextContent' as 'isEqualNode' does not match due to line + // returns, etc + // Note, this does not work if child nodes are appearing in a + // different order than expected + if (existingChild.getNodeName().equals(child.getNodeName()) + && existingChild + .getTextContent() + .replaceAll(WHITESPACE, "") + .trim() + .equals(child.getTextContent().replaceAll( + WHITESPACE, ""))) { + // If we found a match, there is no need to append the child + // element + return; + } + } + } + parent.appendChild(child); + } + + private static void insertAfter(final Element element, + final String afterElementName, final Document document) { + final Element afterElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + afterElementName, document.getDocumentElement()); + if (afterElement != null && afterElement.getNextSibling() != null + && afterElement.getNextSibling() instanceof Element) { + document.getDocumentElement().insertBefore(element, + afterElement.getNextSibling()); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + return; + } + document.getDocumentElement().appendChild(element); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + } + + private static void insertBefore(final Element element, + final String beforeElementName, final Document document) { + final Element beforeElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + beforeElementName, document.getDocumentElement()); + if (beforeElement != null) { + document.getDocumentElement().insertBefore(element, beforeElement); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + return; + } + document.getDocumentElement().appendChild(element); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + } + + private static void insertBetween(final Element element, + final String afterElementName, final String beforeElementName, + final Document document) { + final Element beforeElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + beforeElementName, document.getDocumentElement()); + if (beforeElement != null) { + document.getDocumentElement().insertBefore(element, beforeElement); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + return; + } + + final Element afterElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + afterElementName, document.getDocumentElement()); + if (afterElement != null && afterElement.getNextSibling() != null + && afterElement.getNextSibling() instanceof Element) { + document.getDocumentElement().insertBefore(element, + afterElement.getNextSibling()); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + return; + } + + document.getDocumentElement().appendChild(element); + addLineBreakBefore(element, document); + addLineBreakBefore(element, document); + } + + /** + * Set the description element in the web.xml document. + * + * @param description (required) + * @param document the web.xml document (required) + * @param comment (optional) + */ + public static void setDescription(final String description, + final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notBlank(description, "Description required"); + + Element descriptionElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "description", document.getDocumentElement()); + if (descriptionElement == null) { + descriptionElement = document.createElement("description"); + insertBetween(descriptionElement, "display-name[last()]", + "context-param", document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(descriptionElement, comment, document); + } + } + descriptionElement.setTextContent(description); + } + + /** + * Set the display-name element in the web.xml document. + * + * @param displayName (required) + * @param document the web.xml document (required) + * @param comment (optional) + */ + public static void setDisplayName(final String displayName, + final Document document, final String comment) { + Validate.notBlank(displayName, "display name required"); + Validate.notNull(document, "Web XML document required"); + + Element displayNameElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "display-name", document.getDocumentElement()); + if (displayNameElement == null) { + displayNameElement = document.createElement("display-name"); + insertBetween(displayNameElement, "the-start", "description", + document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(displayNameElement, comment, document); + } + } + displayNameElement.setTextContent(displayName); + } + + /** + * Set session timeout in web.xml document + * + * @param timeout + * @param document (required) + * @param comment (optional) + */ + public static void setSessionTimeout(final int timeout, + final Document document, final String comment) { + Validate.notNull(document, "Web XML document required"); + Validate.notNull(timeout, "Timeout required"); + + Element sessionConfigElement = XmlUtils.findFirstElement(WEB_APP_XPATH + + "session-config", document.getDocumentElement()); + if (sessionConfigElement == null) { + sessionConfigElement = document.createElement("session-config"); + insertBetween(sessionConfigElement, "servlet-mapping[last()]", + "welcome-file-list", document); + if (StringUtils.isNotBlank(comment)) { + addCommentBefore(sessionConfigElement, comment, document); + } + } + appendChildIfNotPresent(sessionConfigElement, new XmlElementBuilder( + "session-timeout", document).setText(String.valueOf(timeout)) + .build()); + } + + /** + * Constructor is private to prevent instantiation + */ + private WebXmlUtils() { + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/XmlElementBuilder.java b/support/src/main/java/org/springframework/roo/support/util/XmlElementBuilder.java new file mode 100644 index 000000000..0a451d9d4 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/XmlElementBuilder.java @@ -0,0 +1,77 @@ +package org.springframework.roo.support.util; + +import org.apache.commons.lang3.Validate; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Very simple convenience Builder for XML {@code Element}s + * + * @author Stefan Schmidt + * @since 1.0 + */ +public class XmlElementBuilder { + + private final Element element; + + /** + * Create a new Element instance. + * + * @param name The name of the element (required, not empty) + * @param document The parent document (required) + */ + public XmlElementBuilder(final String name, final Document document) { + Validate.notBlank(name, "Element name required"); + Validate.notNull(document, "Owner document required"); + element = document.createElement(name); + } + + /** + * Add an attribute to the current element. + * + * @param qName The attribute name (required, not empty) + * @param value The value of the attribute (required) + * @return the current XmlElementBuilder + */ + public XmlElementBuilder addAttribute(final String qName, final String value) { + Validate.notBlank(qName, "Attribute qName required"); + Validate.notNull(value, "Attribute value required"); + element.setAttribute(qName, value); + return this; + } + + /** + * Add a child node to the current element. + * + * @param node The new node (required) + * @return The builder for the current element + */ + public XmlElementBuilder addChild(final Node node) { + Validate.notNull(node, "Node required"); + element.appendChild(node); + return this; + } + + /** + * Get the element instance. + * + * @return The element. + */ + public Element build() { + return element; + } + + /** + * Add text contents to the current element. This will overwrite any + * previous text content. + * + * @param text The text content (required, not empty) + * @return The builder for the current element + */ + public XmlElementBuilder setText(final String text) { + Validate.notBlank(text, "Text content required"); + element.setTextContent(text); + return this; + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/XmlRoundTripUtils.java b/support/src/main/java/org/springframework/roo/support/util/XmlRoundTripUtils.java new file mode 100644 index 000000000..4f82ea1de --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/XmlRoundTripUtils.java @@ -0,0 +1,317 @@ +package org.springframework.roo.support.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.Validate; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Utilities related to round-tripping XML documents + * + * @author Stefan Schmidt + * @since 1.1 + */ +public final class XmlRoundTripUtils { + + private static MessageDigest digest; + + static { + try { + digest = MessageDigest.getInstance("sha-1"); + } + catch (final NoSuchAlgorithmException e) { + throw new IllegalStateException( + "Could not create hash key for identifier"); + } + } + + private static boolean addOrUpdateElements(final Element original, + final Element proposed, boolean originalDocumentChanged) { + final NodeList proposedChildren = proposed.getChildNodes(); + // Check proposed elements and compare to originals to find out if we + // need to add or replace elements + for (int i = 0, n = proposedChildren.getLength(); i < n; i++) { + final Node node = proposedChildren.item(i); + if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { + final Element proposedElement = (Element) node; + final String proposedId = proposedElement.getAttribute("id"); + // Only proposed elements with + // an id will be considered + if (proposedId.length() != 0) { + final Element originalElement = XmlUtils.findFirstElement( + "//*[@id='" + proposedId + "']", original); + // Insert proposed element given the original document has + // no element with a matching id + if (null == originalElement) { + final Element placeHolder = DomUtils + .findFirstElementByName("util:placeholder", + original); + if (placeHolder != null) { // Insert right before place + // holder if we can find it + placeHolder.getParentNode().insertBefore( + original.getOwnerDocument().importNode( + proposedElement, false), + placeHolder); + } + // Find the best place to insert the element + else { + // Try to find the id of the proposed element's + // parent id in the original document + if (proposed.getAttribute("id").length() != 0) { + final Element originalParent = XmlUtils + .findFirstElement("//*[@id='" + + proposed.getAttribute("id") + + "']", original); + // Found parent with the same id, so we can just + // add it as new child + if (originalParent != null) { + originalParent.appendChild(original + .getOwnerDocument().importNode( + proposedElement, false)); + } + // No parent found so we add it as a + // child of the root element (last + // resort) + else { + original.appendChild(original + .getOwnerDocument().importNode( + proposedElement, false)); + } + } + // No parent found so we add it as a child of + // the root element (last resort) + else { + original.appendChild(original + .getOwnerDocument().importNode( + proposedElement, false)); + } + } + originalDocumentChanged = true; + } + // We found an element in the original document with + // a matching id + else { + final String originalElementHashCode = originalElement + .getAttribute("z"); + // Only actif a hash code exists + if (originalElementHashCode.length() > 0) { + // Only act if hash codes match (no user changes in + // the element) or the user requests for the hash + // code to be regenerated + if ("?".equals(originalElementHashCode) + || originalElementHashCode + .equals(calculateUniqueKeyFor(originalElement))) { + // Check if the elements have equal contents + if (!equalElements(originalElement, + proposedElement)) { + // Replace the original with the proposed + // element + originalElement + .getParentNode() + .replaceChild( + original.getOwnerDocument() + .importNode( + proposedElement, + false), + originalElement); + originalDocumentChanged = true; + } + // Replace z if the user sets its value to '?' + // as an indication that roo should take over + // the management of this element again + if ("?".equals(originalElementHashCode)) { + originalElement + .setAttribute( + "z", + calculateUniqueKeyFor(proposedElement)); + originalDocumentChanged = true; + } + } + // If hash codes don't match we will mark the + // element as z="user-managed" + else { + // Mark the element as 'user-managed' if the + // hash codes don't match any more + if (!originalElementHashCode + .equals("user-managed")) { + originalElement.setAttribute("z", + "user-managed"); + originalDocumentChanged = true; + } + } + } + } + } + // Walk through the document tree recursively + originalDocumentChanged = addOrUpdateElements(original, + proposedElement, originalDocumentChanged); + } + } + return originalDocumentChanged; + } + + /** + * Create a base 64 encoded SHA1 hash key for a given XML element. The key + * is based on the element name, the attribute names and their values. Child + * elements are ignored. Attributes named 'z' are not concluded since they + * contain the hash key itself. + * + * @param element The element to create the base 64 encoded hash key for + * @return the unique key + */ + public static String calculateUniqueKeyFor(final Element element) { + final StringBuilder sb = new StringBuilder(); + sb.append(element.getTagName()); + final NamedNodeMap attributes = element.getAttributes(); + final SortedMap attrKVStore = Collections + .synchronizedSortedMap(new TreeMap()); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + final Node attr = attributes.item(i); + if (!"z".equals(attr.getNodeName()) + && !attr.getNodeName().startsWith("_")) { + attrKVStore.put(attr.getNodeName(), attr.getNodeValue()); + } + } + for (final Entry entry : attrKVStore.entrySet()) { + sb.append(entry.getKey()).append(entry.getValue()); + } + return Base64.encodeBase64String(sha1(sb.toString().getBytes())); + } + + /** + * Compare necessary namespace declarations between original and proposed + * document, if namespaces in the original are missing compared to the + * proposed, we add them to the original. + * + * @param original document as read from the file system + * @param proposed document as determined by the JspViewManager + * @return true if the document was adjusted, otherwise false + */ + private static boolean checkNamespaces(final Document original, + final Document proposed) { + boolean originalDocumentChanged = false; + final NamedNodeMap nsNodes = proposed.getDocumentElement() + .getAttributes(); + for (int i = 0; i < nsNodes.getLength(); i++) { + if (0 == original.getDocumentElement() + .getAttribute(nsNodes.item(i).getNodeName()).length()) { + original.getDocumentElement().setAttribute( + nsNodes.item(i).getNodeName(), + nsNodes.item(i).getNodeValue()); + originalDocumentChanged = true; + } + } + return originalDocumentChanged; + } + + /** + * This method will compare the original document with the proposed document + * and return true if adjustments to the original document were necessary. + * Adjustments are only made if new elements or attributes are proposed. + * Changes to the order of attributes or elements in the original document + * will not result in an adjustment. + * + * @param original document as read from the file system + * @param proposed document as determined by the JspViewManager + * @return true if the document was adjusted, otherwise false + */ + public static boolean compareDocuments(final Document original, + final Document proposed) { + boolean originalDocumentAdjusted = checkNamespaces(original, proposed); + originalDocumentAdjusted |= addOrUpdateElements( + original.getDocumentElement(), proposed.getDocumentElement(), + originalDocumentAdjusted); + originalDocumentAdjusted |= removeElements( + original.getDocumentElement(), proposed.getDocumentElement(), + originalDocumentAdjusted); + return originalDocumentAdjusted; + } + + private static boolean equalElements(final Element a, final Element b) { + if (!a.getTagName().equals(b.getTagName())) { + return false; + } + final NamedNodeMap attributes = a.getAttributes(); + int customAttributeCounter = 0; + for (int i = 0, n = attributes.getLength(); i < n; i++) { + final Node node = attributes.item(i); + if (node != null && !node.getNodeName().startsWith("_")) { + if (!node.getNodeName().equals("z") + && (b.getAttribute(node.getNodeName()).length() == 0 || !b + .getAttribute(node.getNodeName()).equals( + node.getNodeValue()))) { + return false; + } + } + else { + customAttributeCounter++; + } + } + if (a.getAttributes().getLength() - customAttributeCounter != b + .getAttributes().getLength()) { + return false; + } + return true; + } + + private static boolean removeElements(final Element original, + final Element proposed, boolean originalDocumentChanged) { + final NodeList originalChildren = original.getChildNodes(); + // Check original elements and compare to proposed to find out if we + // need to remove elements + for (int i = 0, n = originalChildren.getLength(); i < n; i++) { + final Node node = originalChildren.item(i); + if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { + final Element originalElement = (Element) node; + final String originalId = originalElement.getAttribute("id"); + if (originalId.length() != 0) { + // Only proposed elements with + // an id will be considered + final Element proposedElement = XmlUtils.findFirstElement( + "//*[@id='" + originalId + "']", proposed); + if (null == proposedElement + && (originalElement.getAttribute("z").equals( + calculateUniqueKeyFor(originalElement)) || originalElement + .getAttribute("z").equals("?"))) { + // Remove original element given the proposed document + // has no element with a matching id + originalElement.getParentNode().removeChild( + originalElement); + originalDocumentChanged = true; + } + } + // Walk through the document tree recursively + originalDocumentChanged = removeElements(originalElement, + proposed, originalDocumentChanged); + } + } + return originalDocumentChanged; + } + + /** + * Creates a sha-1 hash value for the given data byte array. + * + * @param data to hash + * @return byte[] hash of the input data + */ + private static byte[] sha1(final byte[] data) { + Validate.notNull(digest, "Could not create hash key for identifier"); + return digest.digest(data); + } + + /** + * Constructor is private to prevent instantiation + */ + private XmlRoundTripUtils() { + } +} diff --git a/support/src/main/java/org/springframework/roo/support/util/XmlUtils.java b/support/src/main/java/org/springframework/roo/support/util/XmlUtils.java new file mode 100644 index 000000000..12385fef4 --- /dev/null +++ b/support/src/main/java/org/springframework/roo/support/util/XmlUtils.java @@ -0,0 +1,653 @@ +package org.springframework.roo.support.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.w3c.dom.DOMConfiguration; +import org.w3c.dom.DOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.bootstrap.DOMImplementationRegistry; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSException; +import org.w3c.dom.ls.LSOutput; +import org.w3c.dom.ls.LSSerializer; +import org.xml.sax.SAXException; + +/** + * Utilities related to XML usage. + * + * @author Stefan Schmidt + * @author Ben Alex + * @author Alan Stewart + * @author Andrew Swan + * @since 1.0 + */ +public final class XmlUtils { + + private static final Map COMPILED_EXPRESSION_CACHE = new HashMap(); + private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory + .newInstance(); + private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory + .newInstance(); + private static final XPath XPATH = XPathFactory.newInstance().newXPath(); + + /** + * Checks the presented element for illegal characters that could cause + * malformed XML. + * + * @param element the content of the XML element + * @throws IllegalArgumentException if the element is null, has no text or + * contains illegal characters + */ + public static void assertElementLegal(final String element) { + if (StringUtils.isBlank(element)) { + throw new IllegalArgumentException("Element required"); + } + + // Note regular expression for legal characters found to be x5 slower in + // profiling than this approach + final char[] value = element.toCharArray(); + for (final char c : value) { + if (' ' == c || '>' == c || '<' == c || '!' == c + || '@' == c || '%' == c || '^' == c || '?' == c || '(' == c + || ')' == c || '~' == c || '`' == c || '{' == c || '}' == c + || '[' == c || ']' == c || '|' == c || '\\' == c + || '\'' == c || '+' == c) { + throw new IllegalArgumentException("Illegal name '" + element + + "' (illegal character)"); + } + } + } + + /** + * Compares two DOM {@link Node nodes} by comparing the representations of + * the nodes as XML strings + * + * @param node1 the first node + * @param node2 the second node + * @return true if the XML representation node1 is the same as the XML + * representation of node2, otherwise false + */ + public static boolean compareNodes(Node node1, Node node2) { + Validate.notNull(node1, "First node required"); + Validate.notNull(node2, "Second node required"); + // The documents need to be cloned as normalization has side-effects + node1 = node1.cloneNode(true); + node2 = node2.cloneNode(true); + // The documents need to be normalized before comparison takes place to + // remove any formatting that interfere with comparison + if (node1 instanceof Document && node2 instanceof Document) { + ((Document) node1).normalizeDocument(); + ((Document) node2).normalizeDocument(); + } + else { + node1.normalize(); + node2.normalize(); + } + return nodeToString(node1).equals(nodeToString(node2)); + } + + /** + * Converts a XHTML compliant id (used in jspx) to a CSS3 selector spec + * compliant id. In that it will replace all '.,:,-' to '_' + * + * @param proposed Id + * @return cleaned up Id + */ + public static String convertId(final String proposed) { + return proposed.replaceAll("[:\\.-]", "_"); + } + + /** + * @return a transformer that indents entries by 4 characters (never null) + */ + public static Transformer createIndentingTransformer() { + Transformer transformer; + try { + TRANSFORMER_FACTORY.setAttribute("indent-number", 4); + transformer = TRANSFORMER_FACTORY.newTransformer(); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty( + "{http://xml.apache.org/xslt}indent-amount", "4"); + return transformer; + } + + /** + * Creates an {@link Element} containing the given text + * + * @param document the document to contain the new element + * @param tagName the element's tag name (required) + * @param text the text to set; can be null for none + * @return a non-null element + * @since 1.2.0 + */ + public static Element createTextElement(final Document document, + final String tagName, final String text) { + final Element element = document.createElement(tagName); + element.setTextContent(text); + return element; + } + + /** + * Creates a {@link StreamResult} by wrapping the given outputStream in an + * {@link OutputStreamWriter} that transforms Windows line endings (\r\n) + * into Unix line endings (\n) on Windows for consistency with Roo's + * templates. + * + * @param outputStream + * @return StreamResult + * @throws UnsupportedEncodingException + */ + private static StreamResult createUnixStreamResultForEntry( + final OutputStream outputStream) + throws UnsupportedEncodingException { + final Writer writer; + if (IOUtils.LINE_SEPARATOR.equals("\r\n")) { + writer = new OutputStreamWriter(outputStream, "ISO-8859-1") { + @Override + public void write(final char[] cbuf, final int off, + final int len) throws IOException { + for (int i = off; i < off + len; i++) { + if (cbuf[i] != '\r' || i < cbuf.length - 1 + && cbuf[i + 1] != '\n') { + super.write(cbuf[i]); + } + } + } + + @Override + public void write(final int c) throws IOException { + if (c != '\r') { + super.write(c); + } + } + + @Override + public void write(final String str, final int off, final int len) + throws IOException { + final String orig = str.substring(off, off + len); + final String filtered = orig.replace("\r\n", "\n"); + final int lengthDiff = orig.length() - filtered.length(); + if (filtered.endsWith("\r")) { + super.write( + filtered.substring(0, filtered.length() - 1), + 0, len - lengthDiff - 1); + } + else { + super.write(filtered, 0, len - lengthDiff); + } + } + }; + } + else { + writer = new OutputStreamWriter(outputStream, "ISO-8859-1"); + } + return new StreamResult(writer); + } + + /** + * Checks in under a given root element whether it can find a child elements + * which match the XPath expression supplied. Returns a {@link List} of + * {@link Element} if they exist. Please note that the XPath parser used is + * NOT namespace aware. So if you want to find a element + * you need to use the following XPath expression '/beans/http'. + * + * @param xPathExpression the xPathExpression + * @param root the parent DOM element + * @return a {@link List} of type {@link Element} if discovered, otherwise + * an empty list (never null) + */ + public static List findElements(final String xPathExpression, + final Element root) { + final List elements = new ArrayList(); + NodeList nodes = null; + + try { + XPathExpression expr = COMPILED_EXPRESSION_CACHE + .get(xPathExpression); + if (expr == null) { + expr = XPATH.compile(xPathExpression); + COMPILED_EXPRESSION_CACHE.put(xPathExpression, expr); + } + nodes = (NodeList) expr.evaluate(root, XPathConstants.NODESET); + } + catch (final XPathExpressionException e) { + throw new IllegalArgumentException( + "Unable evaluate xpath expression", e); + } + + for (int i = 0, n = nodes.getLength(); i < n; i++) { + elements.add((Element) nodes.item(i)); + } + return elements; + } + + /** + * Checks for a given element whether it can find an attribute which matches + * the XPath expression supplied. Returns {@link Node} if exists. + * + * @param xPathExpression the xPathExpression (required) + * @param element (required) + * @return the Node if discovered (null if not found) + */ + public static Node findFirstAttribute(final String xPathExpression, + final Element element) { + Node attr = null; + try { + XPathExpression expr = COMPILED_EXPRESSION_CACHE + .get(xPathExpression); + if (expr == null) { + expr = XPATH.compile(xPathExpression); + COMPILED_EXPRESSION_CACHE.put(xPathExpression, expr); + } + attr = (Node) expr.evaluate(element, XPathConstants.NODE); + } + catch (final XPathExpressionException e) { + throw new IllegalArgumentException( + "Unable evaluate xpath expression", e); + } + return attr; + } + + /** + * Searches the given parent element for a child element matching the given + * XPath expression. Please note that the XPath parser used is NOT namespace + * aware. So if you want to find an element + * <beans><sec:http>, you need to use the following + * XPath expression '/beans/http'. + * + * @param xPathExpression the xPathExpression (required) + * @param parent the parent DOM element (required) + * @return the Element if discovered (null if no such {@link Element} found) + */ + public static Element findFirstElement(final String xPathExpression, + final Node parent) { + final Node node = findNode(xPathExpression, parent); + if (node instanceof Element) { + return (Element) node; + } + return null; + } + + /** + * Checks in under a given root element whether it can find a child element + * which matches the name supplied. Returns {@link Element} if exists. + * + * @param name the Element name (required) + * @param root the parent DOM element (required) + * @return the Element if discovered + * @deprecated use {@link DomUtils#findFirstElementByName(String, Element)} + * instead + */ + @Deprecated + public static Element findFirstElementByName(final String name, + final Element root) { + return DomUtils.findFirstElementByName(name, root); + } + + /** + * Checks in under a given root element whether it can find a child node + * which matches the XPath expression supplied. Returns {@link Node} if + * exists. Please note that the XPath parser used is NOT namespace aware. So + * if you want to find a element <beans><sec:http>, + * you need to use the XPath expression '/beans/http'. + * + * @param xPathExpression the XPath expression (required) + * @param root the parent DOM element (required) + * @return the Node if discovered (null if not found) + */ + public static Node findNode(final String xPathExpression, final Node root) { + Validate.notBlank(xPathExpression, "XPath expression required"); + Validate.notNull(root, "Root element required"); + Node node = null; + try { + XPathExpression expr = COMPILED_EXPRESSION_CACHE + .get(xPathExpression); + if (expr == null) { + expr = XPATH.compile(xPathExpression); + COMPILED_EXPRESSION_CACHE.put(xPathExpression, expr); + } + node = (Node) expr.evaluate(root, XPathConstants.NODE); + } + catch (final XPathExpressionException e) { + throw new IllegalArgumentException( + "Unable evaluate XPath expression '" + xPathExpression + + "'", e); + } + return node; + } + + /** + * Checks in under a given root element whether it can find a child element + * which matches the XPath expression supplied. The {@link Element} must + * exist. Returns {@link Element} if exists. Please note that the XPath + * parser used is NOT namespace aware. So if you want to find a element + * you need to use the following XPath expression + * '/beans/http'. + * + * @param xPathExpression the XPath expression (required) + * @param root the parent DOM element (required) + * @return the Element if discovered (never null; an exception is thrown if + * cannot be found) + */ + public static Element findRequiredElement(final String xPathExpression, + final Element root) { + Validate.notBlank(xPathExpression, "XPath expression required"); + Validate.notNull(root, "Root element required"); + final Element element = findFirstElement(xPathExpression, root); + Validate.notNull(element, "Unable to obtain required element '" + + xPathExpression + "' from element '" + root + "'"); + return element; + } + + /** + * Returns the root element of an addon's configuration file. + * + * @param clazz which owns the configuration + * @return the configuration root element + */ + public static Element getConfiguration(final Class clazz) { + return getRootElement(clazz, "configuration.xml"); + } + + /** + * @return a new document builder (never null) + */ + public static DocumentBuilder getDocumentBuilder() { + // factory.setNamespaceAware(true); + try { + return FACTORY.newDocumentBuilder(); + } + catch (final ParserConfigurationException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns the root element of the given XML file. + * + * @param clazz the class from whose package to open the file (required) + * @param xmlFilePath the path of the XML file relative to the given class' + * package (required) + * @return a non-null element + * @see Document#getDocumentElement() + */ + public static Element getRootElement(final Class clazz, + final String xmlFilePath) { + final InputStream inputStream = FileUtils.getInputStream(clazz, + xmlFilePath); + Validate.notNull(inputStream, "Could not open the file '%s'", + xmlFilePath); + return readXml(inputStream).getDocumentElement(); + } + + public static String getTextContent(final String path, + final Element parentElement) { + return getTextContent(path, parentElement, null); + } + + public static String getTextContent(final String path, + final Element parentElement, final String valueIfNull) { + final Element element = XmlUtils.findFirstElement(path, parentElement); + if (element != null) { + return element.getTextContent(); + } + return valueIfNull; + } + + /** + * Converts a {@link Node node} to an XML string + * + * @param node the first element + * @return the XML String representation of the node, never null + */ + public static String nodeToString(final Node node) { + try { + final StringWriter writer = new StringWriter(); + createIndentingTransformer().transform(new DOMSource(node), + new StreamResult(writer)); + return writer.toString(); + } + catch (final TransformerException e) { + throw new IllegalStateException(e); + } + } + + /** + * Read an XML document from the supplied input stream and return a + * document. + * + * @param inputStream the input stream to read from (required). The stream + * is closed upon completion. + * @return a document. + * @throws IllegalStateException if the stream could not be read or parsed + */ + public static Document readXml(InputStream inputStream) { + Validate.notNull(inputStream, "InputStream required"); + try { + if (!(inputStream instanceof BufferedInputStream)) { + inputStream = new BufferedInputStream(inputStream); + } + return getDocumentBuilder().parse(inputStream); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(inputStream); + } + } + + /** + * Removes empty text nodes from the specified node + * + * @param node the element where empty text nodes will be removed + * @deprecated use {@link DomUtils#removeTextNodes(Node)} instead + */ + @Deprecated + public static void removeTextNodes(final Node node) { + DomUtils.removeTextNodes(node); + } + + /** + * Returns the given XML as the root {@link Element} of a new + * {@link Document} + * + * @param xml the XML to convert; can be blank + * @return null if the given XML is blank + * @since 1.2.0 + */ + public static Element stringToElement(final String xml) { + if (StringUtils.isBlank(xml)) { + return null; + } + try { + return FACTORY.newDocumentBuilder() + .parse(new ByteArrayInputStream(xml.getBytes())) + .getDocumentElement(); + } + catch (final IOException e) { + throw new IllegalStateException(e); + } + catch (final ParserConfigurationException e) { + throw new IllegalStateException(e); + } + catch (final SAXException e) { + throw new IllegalStateException(e); + } + } + + /** + * Write an XML document to the OutputStream provided. This method will + * detect if the JDK supports the DOM Level 3 "format-pretty-print" + * configuration and make use of it. If not found it will fall back to using + * formatting offered by TrAX. + * + * @param outputStream the output stream to write to. The stream is closed + * upon completion. + * @param document the document to write. + */ + public static void writeFormattedXml(final OutputStream outputStream, + final Document document) { + // Note that the "format-pretty-print" DOM configuration parameter can + // only be set in JDK 1.6+. + final DOMImplementation domImplementation = document + .getImplementation(); + if (domImplementation.hasFeature("LS", "3.0") + && domImplementation.hasFeature("Core", "2.0")) { + DOMImplementationLS domImplementationLS = null; + try { + domImplementationLS = (DOMImplementationLS) domImplementation + .getFeature("LS", "3.0"); + } + catch (final NoSuchMethodError nsme) { + // Fall back to default LS + DOMImplementationRegistry registry = null; + try { + registry = DOMImplementationRegistry.newInstance(); + } + catch (final Exception e) { + // DOMImplementationRegistry not available. Falling back to + // TrAX. + writeXml(outputStream, document); + return; + } + if (registry != null) { + domImplementationLS = (DOMImplementationLS) registry + .getDOMImplementation("LS"); + } + else { + // DOMImplementationRegistry not available. Falling back to + // TrAX. + writeXml(outputStream, document); + } + } + if (domImplementationLS != null) { + final LSSerializer lsSerializer = domImplementationLS + .createLSSerializer(); + final DOMConfiguration domConfiguration = lsSerializer + .getDomConfig(); + if (domConfiguration.canSetParameter("format-pretty-print", + Boolean.TRUE)) { + lsSerializer.getDomConfig().setParameter( + "format-pretty-print", Boolean.TRUE); + final LSOutput lsOutput = domImplementationLS + .createLSOutput(); + lsOutput.setEncoding("UTF-8"); + lsOutput.setByteStream(outputStream); + try { + lsSerializer.write(document, lsOutput); + } + catch (final LSException lse) { + throw new IllegalStateException(lse); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + else { + // DOMConfiguration 'format-pretty-print' parameter not + // available. Falling back to TrAX. + writeXml(outputStream, document); + } + } + else { + // DOMImplementationLS not available. Falling back to TrAX. + writeXml(outputStream, document); + } + } + else { + // DOM 3.0 LS and/or DOM 2.0 Core not supported. Falling back to + // TrAX. + writeXml(outputStream, document); + } + } + + /** + * Write an XML document to the OutputStream provided. This will use the + * pre-configured Roo provided Transformer. + * + * @param outputStream the output stream to write to. The stream is closed + * upon completion. + * @param document the document to write. + */ + public static void writeXml(final OutputStream outputStream, + final Document document) { + writeXml(createIndentingTransformer(), outputStream, document); + } + + /** + * Write an XML document to the OutputStream provided. This will use the + * provided Transformer. + * + * @param transformer the transformer (can be obtained from + * XmlUtils.createIndentingTransformer()) + * @param outputStream the output stream to write to. The stream is closed + * upon completion. + * @param document the document to write. + */ + public static void writeXml(final Transformer transformer, + OutputStream outputStream, final Document document) { + Validate.notNull(transformer, "Transformer required"); + Validate.notNull(outputStream, "OutputStream required"); + Validate.notNull(document, "Document required"); + + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + try { + if (!(outputStream instanceof BufferedOutputStream)) { + outputStream = new BufferedOutputStream(outputStream); + } + final StreamResult streamResult = createUnixStreamResultForEntry(outputStream); + transformer.transform(new DOMSource(document), streamResult); + } + catch (final Exception e) { + throw new IllegalStateException(e); + } + finally { + IOUtils.closeQuietly(outputStream); + } + } + + /** + * Constructor is private to prevent instantiation + */ + private XmlUtils() { + } +} \ No newline at end of file diff --git a/support/src/test/java/org/springframework/roo/support/util/AnsiEscapeCodeTest.java b/support/src/test/java/org/springframework/roo/support/util/AnsiEscapeCodeTest.java new file mode 100644 index 000000000..2f60b67c8 --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/AnsiEscapeCodeTest.java @@ -0,0 +1,56 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for the {@link AnsiEscapeCode} enum. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class AnsiEscapeCodeTest { + + @Before + public void init() { + System.setProperty("roo.console.ansi", Boolean.TRUE.toString()); + } + + @Test + public void testCodesAreUnique() { + // Set up + final Set codes = new HashSet(); + + // Invoke + for (final AnsiEscapeCode escapeCode : AnsiEscapeCode.values()) { + codes.add(escapeCode.code); + } + + // Check + assertEquals(AnsiEscapeCode.values().length, codes.size()); + } + + @Test + public void testDecorateEmptyText() { + assertEquals("", + AnsiEscapeCode.decorate("", AnsiEscapeCode.values()[0])); + } + + @Test + public void testDecorateNullText() { + assertNull(AnsiEscapeCode.decorate(null, AnsiEscapeCode.values()[0])); + } + + @Test + public void testDecorateWhitespace() { + final AnsiEscapeCode effect = AnsiEscapeCode.values()[0]; // Arbitrary + assertEquals(effect.code + " " + AnsiEscapeCode.OFF.code, + AnsiEscapeCode.decorate(" ", effect)); + } +} diff --git a/support/src/test/java/org/springframework/roo/support/util/CollectionUtilsTest.java b/support/src/test/java/org/springframework/roo/support/util/CollectionUtilsTest.java new file mode 100644 index 000000000..a3f7a0086 --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/CollectionUtilsTest.java @@ -0,0 +1,128 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +/** + * Unit test of {@link CollectionUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class CollectionUtilsTest { + + private static class Child extends Parent { + } + + private static class Parent { + @Override + public String toString() { + return getClass().getSimpleName(); + } + } + + // A simple filter for testing the filtering methods + private static final Filter NON_BLANK_FILTER = new Filter() { + public boolean include(final String instance) { + return StringUtils.isNotBlank(instance); + } + }; + + @Test + public void testFilterNonNullIterableWithNonNullFilter() { + // Set up + final Iterable inputs = Arrays.asList("a", "", null, "b"); + + // Invoke + final List results = CollectionUtils.filter(inputs, + NON_BLANK_FILTER); + + // Check + assertEquals(Arrays.asList("a", "b"), results); + } + + @Test + public void testFilterNonNullIterableWithNullFilter() { + // Set up + final Iterable inputs = Arrays.asList("a", ""); + + // Invoke + final List results = CollectionUtils.filter(inputs, + null); + + // Check + assertEquals(inputs, results); + } + + @Test + public void testFilterNullCollection() { + assertEquals(0, CollectionUtils.filter(null, NON_BLANK_FILTER).size()); + } + + @Test + public void testFirstElementOfEmptyCollection() { + assertNull(CollectionUtils.firstElementOf(Collections.emptySet())); + } + + @Test + public void testFirstElementOfMultiElementCollection() { + final String[] members = { "x", "y", "z" }; + assertEquals(members[0], + CollectionUtils.firstElementOf(Arrays.asList(members))); + } + + @Test + public void testFirstElementOfNullCollection() { + assertNull(CollectionUtils.firstElementOf(null)); + } + + @Test + public void testFirstElementOfSingleElementCollection() { + final String member = "x"; + assertEquals(member, + CollectionUtils.firstElementOf(Collections.singleton(member))); + } + + @Test + public void testPopulateNonNullCollectionWithNonNullCollection() { + // Set up + final Collection originalCollection = new ArrayList(); + originalCollection.add(new Parent()); + final Child child = new Child(); + + // Invoke + final Collection result = CollectionUtils.populate( + originalCollection, Arrays.asList(child)); + + // Check + assertEquals(Collections.singletonList(child), result); + } + + @Test + public void testPopulateNonNullCollectionWithNullCollection() { + // Set up + final Collection collection = new ArrayList(); + collection.add(new Parent()); + + // Invoke + final Collection result = CollectionUtils.populate(collection, + null); + + // Check + assertEquals(0, result.size()); + } + + @Test + public void testPopulateNullCollectionWithNullCollection() { + assertNull(CollectionUtils.populate(null, null)); + } +} diff --git a/support/src/test/java/org/springframework/roo/support/util/DomUtilsTest.java b/support/src/test/java/org/springframework/roo/support/util/DomUtilsTest.java new file mode 100644 index 000000000..c4736abe2 --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/DomUtilsTest.java @@ -0,0 +1,55 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Unit test of {@link DomUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class DomUtilsTest { + + private static final String DEFAULT_TEXT = "foo"; + private static final String NODE_TEXT = "bar"; + private static final String XML_AFTER_REMOVAL = ""; + private static final String XML_BEFORE_REMOVAL = ""; + + @Test + public void testGetTextContentOfNonNullNode() { + // Set up + final Node mockNode = mock(Node.class); + when(mockNode.getTextContent()).thenReturn(NODE_TEXT); + + assertEquals(NODE_TEXT, DomUtils.getTextContent(mockNode, DEFAULT_TEXT)); + } + + @Test + public void testGetTextContentOfNullNode() { + assertEquals(DEFAULT_TEXT, DomUtils.getTextContent(null, DEFAULT_TEXT)); + } + + @Test + public void testRemoveElements() throws Exception { + // Set up + final Element root = XmlUtils.stringToElement(XML_BEFORE_REMOVAL); + final Element middle = DomUtils + .getChildElementByTagName(root, "middle"); + + // Invoke + DomUtils.removeElements("bottom", middle); + + // Check + assertEquals(XmlUtils.nodeToString(XmlUtils + .stringToElement(XML_AFTER_REMOVAL)), + XmlUtils.nodeToString(root)); + } +} diff --git a/support/src/test/java/org/springframework/roo/support/util/FileUtilsTest.java b/support/src/test/java/org/springframework/roo/support/util/FileUtilsTest.java new file mode 100644 index 000000000..c80fba557 --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/FileUtilsTest.java @@ -0,0 +1,186 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.springframework.roo.support.util.loader.Loader; + +/** + * Unit test of {@link FileUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class FileUtilsTest { + + private static final String MISSING_FILE = "no-such-file.txt"; + private static final String TEST_FILE = "sub" + File.separator + + "file-utils-test.txt"; + + private void assertFirstDirectory(final String path, + final String expectedFirstDirectory) { + // Invoke + final String firstDirectory = FileUtils.getFirstDirectory(path); + + // Check + assertEquals(expectedFirstDirectory, firstDirectory); + } + + @Test + public void testBackOneDirectory() { + assertEquals( + "foo" + File.separator + "bar", + FileUtils.backOneDirectory("foo" + File.separator + "bar" + + File.separator + "baz" + File.separator)); + } + + @Test + public void testEnsureTrailingSeparatorForEmptyPath() { + assertEquals(File.separator, FileUtils.ensureTrailingSeparator("")); + } + + @Test(expected = NullPointerException.class) + public void testEnsureTrailingSeparatorForNullPath() { + FileUtils.ensureTrailingSeparator(null); + } + + @Test + public void testEnsureTrailingSeparatorForPathWithNoTrailingSeparator() { + final String path = "foo"; + assertEquals(path + File.separator, + FileUtils.ensureTrailingSeparator(path)); + } + + @Test + public void testEnsureTrailingSeparatorForPathWithOneTrailingSeparator() { + final String path = "foo" + File.separator; + assertEquals(path, FileUtils.ensureTrailingSeparator(path)); + } + + @Test + public void testEnsureTrailingSeparatorFromPathWithMultipleTrailingSeparators() { + final String path = "foo" + StringUtils.repeat(File.separator, 3); + assertEquals("foo" + File.separator, + FileUtils.ensureTrailingSeparator(path)); + } + + @Test(expected = IllegalStateException.class) + public void testGetCanonicalPathForInvalidFile() throws Exception { + // Set up + final File invalidFile = mock(File.class); + when(invalidFile.getCanonicalPath()) + .thenThrow(new IOException("dummy")); + + // Invoke + FileUtils.getCanonicalPath(invalidFile); + } + + @Test + public void testGetCanonicalPathForNullFile() { + assertNull(FileUtils.getCanonicalPath(null)); + } + + @Test + public void testGetCanonicalPathForValidFile() throws Exception { + // Set up + final File validFile = mock(File.class); + final String canonicalPath = "the_path"; + when(validFile.getCanonicalPath()).thenReturn(canonicalPath); + + // Invoke + final String actualPath = FileUtils.getCanonicalPath(validFile); + + // Check + assertEquals(canonicalPath, actualPath); + } + + @Test + public void testGetFileSeparatorAsRegex() throws Exception { + // Set up + final String regex = FileUtils.getFileSeparatorAsRegex(); + final String currentDirectory = new File(FileUtils.CURRENT_DIRECTORY) + .getCanonicalPath(); + + // Invoke + final String[] pathElements = currentDirectory.split(regex); + + // Check + assertTrue(pathElements.length > 0); + } + + @Test + public void testGetFirstDirectoryOfExistingDirectory() throws Exception { + // Set up + + final URL url = Loader.class.getResource(TEST_FILE); + final File file = org.apache.commons.io.FileUtils.toFile(url); + final String directory = file.getParent(); + + // Invoke + final String firstDirectory = FileUtils.getFirstDirectory(directory); + + // Check + assertTrue(firstDirectory.endsWith("sub")); + } + + @Test + public void testGetFirstDirectoryOfExistingFile() { + assertFirstDirectory(TEST_FILE, "sub"); + } + + @Test + public void testGetInputStreamOfFileInSubDirectory() throws Exception { + // Invoke + final InputStream inputStream = FileUtils.getInputStream(Loader.class, + TEST_FILE); + + // Check + final String contents = IOUtils.toString(inputStream); + assertEquals("This file is required for FileUtilsTest.", contents); + } + + @Test(expected = NullPointerException.class) + public void testGetInputStreamOfInvalidFile() throws Exception { + FileUtils.getInputStream(Loader.class, MISSING_FILE); + } + + @Test + public void testGetPath() { + assertEquals( + "/org/springframework/roo/support/util/loader/sub/file-utils-test.txt", + FileUtils.getPath(Loader.class, "sub/file-utils-test.txt")); + } + + @Test + public void testGetSystemDependentPathFromMultipleElements() { + final String expectedPath = "foo" + File.separator + "bar"; + assertEquals(expectedPath, + FileUtils.getSystemDependentPath("foo", "bar")); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetSystemDependentPathFromNoElements() { + FileUtils.getSystemDependentPath(); + } + + @Test(expected = NullPointerException.class) + public void testGetSystemDependentPathFromNullArray() { + FileUtils.getSystemDependentPath((String[]) null); + } + + @Test + public void testGetSystemDependentPathFromOneElement() { + assertEquals("foo", FileUtils.getSystemDependentPath("foo")); + } +} diff --git a/support/src/test/java/org/springframework/roo/support/util/PairListTest.java b/support/src/test/java/org/springframework/roo/support/util/PairListTest.java new file mode 100644 index 000000000..273ea97c3 --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/PairListTest.java @@ -0,0 +1,65 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Test; + +/** + * Unit test of {@link PairList} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PairListTest { + + private static final int KEY_1 = 10; + private static final int KEY_2 = 20; + private static final String VALUE_1 = "a"; + private static final String VALUE_2 = "b"; + private static final MutablePair PAIR_1 = new MutablePair( + KEY_1, VALUE_1); + private static final MutablePair PAIR_2 = new MutablePair( + KEY_2, VALUE_2); + + @Test + public void testConstructFromListsOfKeysAndValues() { + // Invoke + final PairList pairs = new PairList( + Arrays.asList(KEY_1, KEY_2), Arrays.asList(VALUE_1, VALUE_2)); + + // Check + assertEquals(2, pairs.size()); + assertEquals(PAIR_1, pairs.get(0)); + assertEquals(PAIR_2, pairs.get(1)); + } + + @Test + public void testConstructFromNulListsOfKeysAndValues() { + // Invoke + final PairList pairs = new PairList( + null, null); + + // Check + assertEquals(0, pairs.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testConstructFromVarargArrayOfPairs() { + // Invoke + final PairList pairs = new PairList( + PAIR_1, PAIR_2); + + // Check + assertEquals(2, pairs.size()); + assertEquals(Arrays.asList(KEY_1, KEY_2), pairs.getKeys()); + assertEquals(Arrays.asList(VALUE_1, VALUE_2), pairs.getValues()); + final Pair[] array = pairs.toArray(); + assertEquals(pairs.size(), array.length); + assertEquals(pairs, Arrays.asList(array)); + } +} diff --git a/support/src/test/java/org/springframework/roo/support/util/WebXmlUtilsTest.java b/support/src/test/java/org/springframework/roo/support/util/WebXmlUtilsTest.java new file mode 100644 index 000000000..641e1b563 --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/WebXmlUtilsTest.java @@ -0,0 +1,343 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; + +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.roo.support.util.WebXmlUtils.WebXmlParam; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Unit tests for {@link WebXmlUtils} + * + * @author Stefan Schmidt + * @since 1.1.1 + */ +public class WebXmlUtilsTest { + + private static Document webXml; + + @BeforeClass + public static void setUp() throws Exception { + final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + webXml = builder.newDocument(); + webXml.appendChild(webXml.createElement("web-app")); + } + + @Test + public void testSetDisplayName() { + WebXmlUtils.setDisplayName("display", webXml, null); + + final Element displayElement = XmlUtils.findFirstElement( + "display-name", webXml.getDocumentElement()); + assertNotNull(displayElement); + assertEquals("display", displayElement.getTextContent()); + } + + @Test + public void testSetDescription() { + WebXmlUtils.setDescription("test desc", webXml, null); + + final Element descriptionElement = XmlUtils.findFirstElement( + "description", webXml.getDocumentElement()); + assertNotNull(descriptionElement); + assertEquals("test desc", descriptionElement.getTextContent()); + } + + @Test + public void testAddContextParam() { + WebXmlUtils.addContextParam( + new WebXmlUtils.WebXmlParam("key", "value"), webXml, null); + + final Element contextParamElement = XmlUtils.findFirstElement( + "context-param", webXml.getDocumentElement()); + assertNotNull(contextParamElement); + assertEquals(2, contextParamElement.getChildNodes().getLength()); + assertEquals("key", + XmlUtils.findFirstElement("param-name", contextParamElement) + .getTextContent()); + assertEquals("value", + XmlUtils.findFirstElement("param-value", contextParamElement) + .getTextContent()); + } + + @Test + public void testAddFilter() { + WebXmlUtils.addFilter("filter1", String.class.getName(), "/*", webXml, + null, new WebXmlUtils.WebXmlParam("key", "value"), + new WebXmlUtils.WebXmlParam("key2", "value2")); + + final Element filterElement = XmlUtils.findFirstElement("filter", + webXml.getDocumentElement()); + assertNotNull(filterElement); + assertEquals("filter1", + XmlUtils.findFirstElement("filter-name", filterElement) + .getTextContent()); + assertEquals(String.class.getName(), + XmlUtils.findFirstElement("filter-class", filterElement) + .getTextContent()); + final Element filterMapping = XmlUtils.findFirstElement( + "filter-mapping", webXml.getDocumentElement()); + assertNotNull(filterMapping); + assertEquals("filter1", + XmlUtils.findFirstElement("filter-name", filterMapping) + .getTextContent()); + assertEquals("/*", + XmlUtils.findFirstElement("url-pattern", filterMapping) + .getTextContent()); + final List initParams = XmlUtils.findElements("init-param", + filterElement); + assertEquals(2, initParams.size()); + assertEquals(2, initParams.get(0).getChildNodes().getLength()); + assertEquals("key", + XmlUtils.findFirstElement("param-name", initParams.get(0)) + .getTextContent()); + assertEquals("value", + XmlUtils.findFirstElement("param-value", initParams.get(0)) + .getTextContent()); + assertEquals("key2", + XmlUtils.findFirstElement("param-name", initParams.get(1)) + .getTextContent()); + assertEquals("value2", + XmlUtils.findFirstElement("param-value", initParams.get(1)) + .getTextContent()); + } + + @Test + public void testAddFilterAtPositionWithDispatcher() { + WebXmlUtils.addFilterAtPosition(WebXmlUtils.FilterPosition.BEFORE, + null, "filter1", "filter2", Object.class.getName(), "/test", + webXml, null, null, Arrays.asList(WebXmlUtils.Dispatcher.ERROR, + WebXmlUtils.Dispatcher.INCLUDE, + WebXmlUtils.Dispatcher.FORWARD, + WebXmlUtils.Dispatcher.REQUEST)); + + final Element filterElement = XmlUtils.findFirstElement("filter", + webXml.getDocumentElement()); + assertNotNull(filterElement); + assertEquals("filter2", + XmlUtils.findFirstElement("filter-name", filterElement) + .getTextContent()); + assertEquals(Object.class.getName(), + XmlUtils.findFirstElement("filter-class", filterElement) + .getTextContent()); + final Element filterMapping = XmlUtils.findFirstElement( + "filter-mapping", webXml.getDocumentElement()); + assertNotNull(filterMapping); + assertEquals("filter2", + XmlUtils.findFirstElement("filter-name", filterMapping) + .getTextContent()); + assertEquals("/test", + XmlUtils.findFirstElement("url-pattern", filterMapping) + .getTextContent()); + final List dispatchers = XmlUtils.findElements("dispatcher", + filterMapping); + assertEquals(4, dispatchers.size()); + assertEquals(WebXmlUtils.Dispatcher.ERROR.name(), dispatchers.get(0) + .getTextContent()); + assertEquals(WebXmlUtils.Dispatcher.INCLUDE.name(), dispatchers.get(1) + .getTextContent()); + assertEquals(WebXmlUtils.Dispatcher.FORWARD.name(), dispatchers.get(2) + .getTextContent()); + assertEquals(WebXmlUtils.Dispatcher.REQUEST.name(), dispatchers.get(3) + .getTextContent()); + } + + @Test + public void testAddFilterAtPosition() { + WebXmlUtils.addFilterAtPosition(WebXmlUtils.FilterPosition.BETWEEN, + "filter2", "filter1", "filter3", Integer.class.getName(), + "/test2", webXml, null, (WebXmlParam[]) null); + + final List filterElements = XmlUtils.findElements("filter", + webXml.getDocumentElement()); + assertEquals(3, filterElements.size()); + assertEquals("filter2", + XmlUtils.findFirstElement("filter-name", filterElements.get(0)) + .getTextContent()); + assertEquals("filter3", + XmlUtils.findFirstElement("filter-name", filterElements.get(1)) + .getTextContent()); + assertEquals("filter1", + XmlUtils.findFirstElement("filter-name", filterElements.get(2)) + .getTextContent()); + assertEquals( + Integer.class.getName(), + XmlUtils.findFirstElement("filter-class", filterElements.get(1)) + .getTextContent()); + final List filterMappings = XmlUtils.findElements( + "filter-mapping", webXml.getDocumentElement()); + assertEquals(3, filterMappings.size()); + assertEquals("filter2", + XmlUtils.findFirstElement("filter-name", filterMappings.get(0)) + .getTextContent()); + assertEquals("filter3", + XmlUtils.findFirstElement("filter-name", filterMappings.get(1)) + .getTextContent()); + assertEquals("filter1", + XmlUtils.findFirstElement("filter-name", filterMappings.get(2)) + .getTextContent()); + assertEquals("/test2", + XmlUtils.findFirstElement("url-pattern", filterMappings.get(1)) + .getTextContent()); + } + + @Test + public void testAddListener() { + WebXmlUtils.addListener(String.class.getName(), webXml, null); + + final Element listenerElement = XmlUtils.findFirstElement("listener", + webXml.getDocumentElement()); + assertNotNull(listenerElement); + assertEquals(String.class.getName(), + XmlUtils.findFirstElement("listener-class", listenerElement) + .getTextContent()); + } + + @Test + public void testAddServlet() { + WebXmlUtils.addServlet("servlet1", Object.class.getName(), "/servlet1", + 1, webXml, null, new WebXmlUtils.WebXmlParam("key1", "value1"), + new WebXmlUtils.WebXmlParam("key2", "value2")); + + final Element servletElement = XmlUtils.findFirstElement("servlet", + webXml.getDocumentElement()); + assertNotNull(servletElement); + assertEquals("servlet1", + XmlUtils.findFirstElement("servlet-name", servletElement) + .getTextContent()); + assertEquals(Object.class.getName(), + XmlUtils.findFirstElement("servlet-class", servletElement) + .getTextContent()); + final Element servletMapping = XmlUtils.findFirstElement( + "servlet-mapping", webXml.getDocumentElement()); + assertNotNull(servletMapping); + assertEquals("servlet1", + XmlUtils.findFirstElement("servlet-name", servletMapping) + .getTextContent()); + assertEquals("/servlet1", + XmlUtils.findFirstElement("url-pattern", servletMapping) + .getTextContent()); + final List initParams = XmlUtils.findElements("init-param", + servletElement); + assertEquals(2, initParams.size()); + assertEquals(2, initParams.get(0).getChildNodes().getLength()); + assertEquals("key1", + XmlUtils.findFirstElement("param-name", initParams.get(0)) + .getTextContent()); + assertEquals("value1", + XmlUtils.findFirstElement("param-value", initParams.get(0)) + .getTextContent()); + assertEquals("key2", + XmlUtils.findFirstElement("param-name", initParams.get(1)) + .getTextContent()); + assertEquals("value2", + XmlUtils.findFirstElement("param-value", initParams.get(1)) + .getTextContent()); + } + + @Test + public void testSetSessionTimeout() { + WebXmlUtils.setSessionTimeout(1000, webXml, null); + + final Element timeElement = XmlUtils.findFirstElement( + "session-config/session-timeout", webXml.getDocumentElement()); + assertNotNull(timeElement); + assertEquals("1000", timeElement.getTextContent()); + } + + @Test + public void testAddWelcomeFile() { + WebXmlUtils.addWelcomeFile("/welcome", webXml, null); + + final Element welcomeFileElement = XmlUtils.findFirstElement( + "welcome-file-list/welcome-file", webXml.getDocumentElement()); + assertNotNull(welcomeFileElement); + assertEquals("/welcome", welcomeFileElement.getTextContent()); + } + + @Test + public void testAddExceptionType() { + WebXmlUtils.addExceptionType(IllegalStateException.class.getName(), + "/illegal", webXml, null); + + final Element errorPageElement = XmlUtils.findFirstElement( + "error-page", webXml.getDocumentElement()); + assertNotNull(errorPageElement); + assertEquals(2, errorPageElement.getChildNodes().getLength()); + assertEquals(IllegalStateException.class.getName(), XmlUtils + .findFirstElement("exception-type", errorPageElement) + .getTextContent()); + assertEquals("/illegal", + XmlUtils.findFirstElement("location", errorPageElement) + .getTextContent()); + } + + @Ignore + @Test + public void testAddErrorCode() { + WebXmlUtils.addErrorCode(404, "/404", webXml, null); + + final Element errorPageElement = (Element) webXml + .getDocumentElement() + .getChildNodes() + .item(webXml.getDocumentElement().getChildNodes().getLength() - 1); + assertNotNull(errorPageElement); + assertEquals(2, errorPageElement.getChildNodes().getLength()); + assertEquals("404", + XmlUtils.findFirstElement("error-code", errorPageElement) + .getTextContent()); + assertEquals("/404", + XmlUtils.findFirstElement("location", errorPageElement) + .getTextContent()); + } + + @Test + public void testAddSecurityConstraint() { + WebXmlUtils.addSecurityConstraint("displayName", Arrays + .asList(new WebXmlUtils.WebResourceCollection( + "web-resource-name", "description", Arrays.asList("/", + "/2"), Arrays.asList("POST", "GET"))), Arrays + .asList("user", "supervisor"), "transportGuarantee", webXml, + null); + + final Element securityConstraintElement = XmlUtils.findFirstElement( + "security-constraint", webXml.getDocumentElement()); + assertNotNull(securityConstraintElement); + assertEquals( + "displayName", + XmlUtils.findFirstElement("display-name", + securityConstraintElement).getTextContent()); + final Element webResourceCollection = XmlUtils.findFirstElement( + "web-resource-collection", securityConstraintElement); + assertNotNull(webResourceCollection); + assertEquals( + "web-resource-name", + XmlUtils.findFirstElement("web-resource-name", + webResourceCollection).getTextContent()); + assertEquals(2, + XmlUtils.findElements("url-pattern", webResourceCollection) + .size()); + assertEquals(2, + XmlUtils.findElements("http-method", webResourceCollection) + .size()); + final Element authConstraint = XmlUtils.findFirstElement( + "auth-constraint", securityConstraintElement); + assertNotNull(authConstraint); + assertEquals(2, authConstraint.getChildNodes().getLength()); + final Element userDataConstraint = XmlUtils.findFirstElement( + "user-data-constraint", securityConstraintElement); + assertNotNull(userDataConstraint); + assertEquals("transportGuarantee", userDataConstraint + .getElementsByTagName("transport-guarantee").item(0) + .getTextContent()); + } +} diff --git a/support/src/test/java/org/springframework/roo/support/util/loader/Loader.java b/support/src/test/java/org/springframework/roo/support/util/loader/Loader.java new file mode 100644 index 000000000..394e4328d --- /dev/null +++ b/support/src/test/java/org/springframework/roo/support/util/loader/Loader.java @@ -0,0 +1,12 @@ +package org.springframework.roo.support.util.loader; + +import org.springframework.roo.support.util.FileUtilsTest; + +/** + * Required for {@link FileUtilsTest}. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class Loader { +} diff --git a/support/src/test/resources/org/springframework/roo/support/util/loader/sub/file-utils-test.txt b/support/src/test/resources/org/springframework/roo/support/util/loader/sub/file-utils-test.txt new file mode 100644 index 000000000..501d1441e --- /dev/null +++ b/support/src/test/resources/org/springframework/roo/support/util/loader/sub/file-utils-test.txt @@ -0,0 +1 @@ +This file is required for FileUtilsTest. \ No newline at end of file diff --git a/uaa/pom.xml b/uaa/pom.xml new file mode 100644 index 000000000..d679aa078 --- /dev/null +++ b/uaa/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.uaa + bundle + Spring Roo - User Agent Analysis (UAA) Integration + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + org.springframework.roo + org.springframework.roo.metadata + + + org.springframework.roo + org.springframework.roo.shell + + + + org.springframework.uaa + org.springframework.uaa.client + + + \ No newline at end of file diff --git a/uaa/src/main/java/org/springframework/roo/uaa/MetadataPollingUaaRegistrationFacility.java b/uaa/src/main/java/org/springframework/roo/uaa/MetadataPollingUaaRegistrationFacility.java new file mode 100644 index 000000000..e19c0755a --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/MetadataPollingUaaRegistrationFacility.java @@ -0,0 +1,90 @@ +package org.springframework.roo.uaa; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.metadata.MetadataLogger; +import org.springframework.roo.metadata.MetadataTimingStatistic; +import org.springframework.roo.support.osgi.BundleFindingUtils; + +/** + * Regularly polls {@link MetadataLogger#getTimings()} and incorporates all + * timings into UAA feature use statistics. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Component(enabled = true) +public class MetadataPollingUaaRegistrationFacility { + + private class MetadataTimerTask extends TimerTask { + @Override + public void run() { + // Try..catch used to avoid unexpected problems terminating the + // timer thread + try { + // Deal with modules being used via the add-on infrastructure + for (final MetadataTimingStatistic stat : metadataLogger + .getTimings()) { + final String typeName = stat.getName(); + String bundleSymbolicName = typeToBsnMap.get(typeName); + if (bundleSymbolicName == null) { + // Try to look it up and cache the outcome + bundleSymbolicName = BundleFindingUtils + .findFirstBundleForTypeName(bundleContext, + typeName); + if (bundleSymbolicName == null) { + bundleSymbolicName = NOT_FOUND; + } + // Cache to avoid the lookup cost in the future + typeToBsnMap.put(typeName, bundleSymbolicName); + } + + if (NOT_FOUND.equals(bundleSymbolicName)) { + continue; + } + + // Only notify the UAA service if we haven't previously told + // it about this BSN (UAA service handles buffering + // internally) + if (!previouslyNotifiedBsns.contains(bundleSymbolicName)) { + // UaaRegistrationService deals with determining if the + // BSN is public (non-public BSNs are not registered) + uaaRegistrationService.registerBundleSymbolicNameUse( + bundleSymbolicName, null); + previouslyNotifiedBsns.add(bundleSymbolicName); + } + + } + } + catch (final RuntimeException ignored) { + } + } + } + + private static final String NOT_FOUND = "___NOT_FOUND___"; + private BundleContext bundleContext; + @Reference private MetadataLogger metadataLogger; + private final Set previouslyNotifiedBsns = new HashSet(); + private final Timer timer = new Timer(); + private final Map typeToBsnMap = new HashMap(); + + @Reference private UaaRegistrationService uaaRegistrationService; + + protected void activate(final ComponentContext context) { + bundleContext = context.getBundleContext(); + timer.scheduleAtFixedRate(new MetadataTimerTask(), 0, 5 * 1000); + } + + protected void deactivate(final ComponentContext context) { + timer.cancel(); + } +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/PublicFeatureResolver.java b/uaa/src/main/java/org/springframework/roo/uaa/PublicFeatureResolver.java new file mode 100644 index 000000000..9492a6115 --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/PublicFeatureResolver.java @@ -0,0 +1,25 @@ +package org.springframework.roo.uaa; + +/** + * Encapsulates the ability to determine if a given bundle symbolic name or type + * name is part of a "public" feature. This is important in ensuring UAA does + * not accidentally log details related to non-public features (as this might + * identify the user, which we do not want to happen). + * + * @author Ben Alex + * @since 1.1.1 + */ +public interface PublicFeatureResolver { + + /** + * Indicates whether the presented bundle symbolic name or type name is + * believed to be a "public" feature. Both bundle symbolic name and type + * names are represented as package names with an optional type name at the + * end. + * + * @param bundleSymbolicNameOrTypeName the type name or bundle name + * (required) + * @return true if the bundle is public, false otherwise + */ + boolean isPublic(String bundleSymbolicNameOrTypeName); +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/ShellListeningUaaRegistrationFacility.java b/uaa/src/main/java/org/springframework/roo/uaa/ShellListeningUaaRegistrationFacility.java new file mode 100644 index 000000000..876252b49 --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/ShellListeningUaaRegistrationFacility.java @@ -0,0 +1,64 @@ +package org.springframework.roo.uaa; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.ParseResult; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.event.ShellStatus; +import org.springframework.roo.shell.event.ShellStatusListener; +import org.springframework.roo.support.osgi.BundleFindingUtils; +import org.springframework.uaa.client.UaaService; + +/** + * A {@link ShellStatusListener} which determines the bundle symbolic name an + * executed shell command was provided by and registers the use of that feature + * in UAA. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Component +public class ShellListeningUaaRegistrationFacility implements + ShellStatusListener { + + private BundleContext bundleContext; + @Reference private Shell shell; + @Reference private UaaRegistrationService uaaRegistrationService; + @Reference UaaService uaaService; + + protected void activate(final ComponentContext context) { + bundleContext = context.getBundleContext(); + shell.addShellStatusListener(this); + } + + protected void deactivate(final ComponentContext context) { + shell.removeShellStatusListener(this); + } + + public void onShellStatusChange(final ShellStatus oldStatus, + final ShellStatus newStatus) { + // Handle registering use of a BSN + final ParseResult parseResult = newStatus.getParseResult(); + if (parseResult == null) { + return; + } + // We use the target instance as opposed to the declaring method as we + // don't want + // the fact an add-on type inherited from another type to prevent using + // of that add-on + // from being detected + final String typeName = parseResult.getInstance().getClass().getName(); + final String bundleSymbolicName = BundleFindingUtils + .findFirstBundleForTypeName(bundleContext, typeName); + if (bundleSymbolicName == null) { + return; + } + + // UaaRegistrationService deals with determining if the BSN is public + // (non-public BSNs are not registered) + uaaRegistrationService.registerBundleSymbolicNameUse( + bundleSymbolicName, null); + } +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/UaaCommands.java b/uaa/src/main/java/org/springframework/roo/uaa/UaaCommands.java new file mode 100644 index 000000000..77f6467f7 --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/UaaCommands.java @@ -0,0 +1,108 @@ +package org.springframework.roo.uaa; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CliOption; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.roo.shell.converters.StaticFieldConverter; +import org.springframework.roo.support.util.MessageDisplayUtils; +import org.springframework.uaa.client.UaaService; +import org.springframework.uaa.client.protobuf.UaaClient.Privacy.PrivacyLevel; + +/** + * Provides shell commands for end user interaction with the Spring User Agent + * Analysis (UAA) system. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Service +@Component +public class UaaCommands implements CommandMarker { + + @Reference private StaticFieldConverter staticFieldConverter; + @Reference private UaaRegistrationService uaaRegistrationService; + @Reference private UaaService uaaService; + + @CliCommand(value = "download accept terms of use", help = "Accepts the Spring User Agent Analysis (UAA) Terms of Use") + public void acceptTou() { + uaaService.setPrivacyLevel(PrivacyLevel.ENABLE_UAA); + uaaRegistrationService.flushIfPossible(); + MessageDisplayUtils.displayFile("accepted_tou.txt", UaaCommands.class); + } + + protected void activate(final ComponentContext context) { + staticFieldConverter.add(PrivacyLevel.class); + } + + protected void deactivate(final ComponentContext context) { + staticFieldConverter.remove(PrivacyLevel.class); + } + + @CliCommand(value = "download privacy level", help = "Changes the Spring User Agent Analysis (UAA) privacy level") + public String privacyLevel( + @CliOption(key = "privacyLevel", mandatory = true, help = "The new UAA privacy level to use") final PrivacyLevel privacyLevel) { + uaaService.setPrivacyLevel(privacyLevel); + return "UAA privacy level updated " + + uaaService.getPrivacyLevelLastChanged() + + " (use 'download view' to view the new data)"; + } + + @CliCommand(value = "download reject terms of use", help = "Rejects the Spring User Agent Analysis (UAA) Terms of Use") + public void rejectTou() { + uaaService.setPrivacyLevel(PrivacyLevel.DECLINE_TOU); + MessageDisplayUtils.displayFile("declined_tou.txt", UaaCommands.class); + } + + @CliCommand(value = "download status", help = "Provides a summary of the Spring User Agent Analysis (UAA) status and commands") + public void uaaStatus() { + final PrivacyLevel privacyLevel = uaaService.getPrivacyLevel(); + if (privacyLevel == PrivacyLevel.DECLINE_TOU) { + MessageDisplayUtils.displayFile("status_declined.txt", + UaaCommands.class); + } + else if (privacyLevel == PrivacyLevel.UNDECIDED_TOU) { + MessageDisplayUtils.displayFile("status_undecided.txt", + UaaCommands.class); + } + else { + MessageDisplayUtils.displayFile("status_accepted.txt", + UaaCommands.class); + } + } + + @CliCommand(value = "download view", help = "Displays the Spring User Agent Analysis (UAA) header content in plain text") + public String view( + @CliOption(key = "file", mandatory = false, help = "The file to save the UAA JSON content to") final File file) { + final String readablePayload = uaaService.getReadablePayload(); + + final StringBuilder sb = new StringBuilder(); + sb.append("Output for privacy level ") + .append(uaaService.getPrivacyLevel()).append(" (last changed ") + .append(uaaService.getPrivacyLevelLastChanged()).append(")") + .append(LINE_SEPARATOR).append(LINE_SEPARATOR); + + sb.append(readablePayload); + sb.append(LINE_SEPARATOR).append(LINE_SEPARATOR); + + if (file != null) { + try { + FileUtils.write(file, sb.toString()); + } + catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + return sb.toString(); + } +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/UaaRegistrationService.java b/uaa/src/main/java/org/springframework/roo/uaa/UaaRegistrationService.java new file mode 100644 index 000000000..7df1be63d --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/UaaRegistrationService.java @@ -0,0 +1,93 @@ +package org.springframework.roo.uaa; + +import org.springframework.uaa.client.UaaService; +import org.springframework.uaa.client.VersionHelper; +import org.springframework.uaa.client.protobuf.UaaClient.Product; + +/** + * Provides an API for any other Roo modules or add-ons to use to record UAA + * data. + *

    + * This API ensures UAA conventions used by Roo are observed. + *

    + * Implementations should perform the initial registration of the + * {@link #SPRING_ROO} product. + *

    + * Implementations are required to buffer all notifications until such time as + * the {@link UaaService} reaches a privacy level where they can be successfully + * persisted. An implementation can rely on an invocation of + * {@link #flushIfPossible()} or attempt to write notifications on a subsequent + * call to a standard registration method. Implementations are therefore not + * required to establish a thread to handle flushing themselves, although they + * should make a final attempt on component deactivation. + * + * @author Ben Alex + * @since 1.1.1 + */ +public interface UaaRegistrationService { + + /** + * A HTTP URL of an "empty file" that add-ons can request if they wish to + * eagerly perform a UAA upload. + */ + String EMPTY_FILE_URL = "http://spring-roo-repository.springsource.org/empty_file.html"; + + /** + * Static representation of the Spring Roo product that should be used by + * any modules requiring a product representation. + */ + Product SPRING_ROO = VersionHelper.getProductFromManifest( + UaaRegistrationServiceImpl.class, "Spring Roo"); + + /** + * Indicates to attempt to flush the buffered notifications. If the + * {@link UaaService} is at a privacy level where it will accept + * registrations, the buffered notifications should be sent to the service. + * If the privacy level does not support this, the buffer should be + * preserved. + */ + void flushIfPossible(); + + /** + * Registers a new "feature use" within UAA. This method requires every + * feature to be a bundle symbolic name. This method permits (but does not + * require) the presentation of UTF-8 encoded custom JSON that will be + * stored as feature_data in the resulting UAA payload. + *

    + * This method may be invoked without determining if the bundle symbolic + * name is public or not. This determination will be automatically made by + * implementations. Non-public bundle symbolic names will not be used. + * + * @param bundleSymbolicName a BSN to register the use of (required) + * @param customJson an optional JSON payload (can be null or an empty + * string if required) + */ + void registerBundleSymbolicNameUse(String bundleSymbolicName, + String customJson); + + /** + * Registers a new "project" within UAA against the presented product. Note + * that UAA will use SHA-256 encoding for the project ID and it is never + * stored or transmitted in a non-hashed form. + *

    + * A product is mandatory because a caller requiring a fallback product may + * use {@link #SPRING_ROO}. A project ID is mandatory because if low-level + * product information is available, this indicates a project configuration + * of some description is also available and therefore a project ID should + * also be available. + * + * @param product the product (required) + * @param projectId the project name to register (required) + */ + void registerProject(Product product, String projectId); + + /** + * Attempts to transmit the data immediately to the server. This will only + * occur if the privacy level is acceptable and the {@link UaaService} is + * capable of transmission. Note this method should very rarely be + * necessary. It is only useful if an immediate transmission is desirable + * for some special reason (eg UAA is being used to convey user + * contributions to the server). + */ + void requestTransmission(); +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/UaaRegistrationServiceImpl.java b/uaa/src/main/java/org/springframework/roo/uaa/UaaRegistrationServiceImpl.java new file mode 100644 index 000000000..6a5dc4ca8 --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/UaaRegistrationServiceImpl.java @@ -0,0 +1,280 @@ +package org.springframework.roo.uaa; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Version; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.support.osgi.BundleFindingUtils; +import org.springframework.uaa.client.TransmissionAwareUaaService; +import org.springframework.uaa.client.TransmissionEventListener; +import org.springframework.uaa.client.UaaService; +import org.springframework.uaa.client.protobuf.UaaClient.FeatureUse; +import org.springframework.uaa.client.protobuf.UaaClient.FeatureUse.Builder; +import org.springframework.uaa.client.protobuf.UaaClient.Product; + +/** + * Default implementation of {@link UaaRegistrationService}. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Service +@Component +public class UaaRegistrationServiceImpl implements UaaRegistrationService, + TransmissionEventListener { + + /** key: bundleSymbolicName, value: customJson */ + private final Map bsnBuffer = new HashMap(); + /** key: BSN, value: git commit hash, if available */ + private final Map bsnCommitHashCache = new HashMap(); + + /** key: BSN, value: version */ + private final Map bsnVersionCache = new HashMap(); + private BundleContext bundleContext; + /** key: projectId, value: list of products */ + private final Map> projectIdBuffer = new HashMap>(); + @Reference private PublicFeatureResolver publicFeatureResolver; + @Reference private UaaService uaaService; + + protected void activate(final ComponentContext context) { + // Attempt to store the SPRING_ROO Product (via the registerBSN method + // so as to be included via possibly-active buffering) + bundleContext = context.getBundleContext(); + final String bundleSymbolicName = BundleFindingUtils + .findFirstBundleForTypeName(context.getBundleContext(), + UaaRegistrationServiceImpl.class.getName()); + registerBundleSymbolicNameUse(bundleSymbolicName, null); + if (uaaService instanceof TransmissionAwareUaaService) { + ((TransmissionAwareUaaService) uaaService) + .addTransmissionEventListener(this); + } + } + + public void afterTransmission(final TransmissionType type, + final boolean successful) { + } + + public void beforeTransmission(final TransmissionType type) { + if (type == TransmissionType.UPLOAD) { + // Good time to flush through to UAA API, so the latest data is + // included in the upload + flushIfPossible(); + } + } + + protected void deactivate(final ComponentContext context) { + // Last effort to store the data given we're shutting down + flushIfPossible(); + if (uaaService instanceof TransmissionAwareUaaService) { + ((TransmissionAwareUaaService) uaaService) + .removeTransmissionEventListener(this); + } + } + + public void flushIfPossible() { + if (bsnBuffer.isEmpty() && projectIdBuffer.isEmpty()) { + // Nothing to flush + return; + } + + if (!uaaService.isUaaTermsOfUseAccepted()) { + // We can't flush yet + return; + } + + // Flush the features + for (final String bundleSymbolicName : bsnBuffer.keySet()) { + final String customJson = bsnBuffer.get(bundleSymbolicName); + registerBundleSymbolicNameUse(bundleSymbolicName, customJson, false); + } + bsnBuffer.clear(); + + // Flush the projects + for (final String projectId : projectIdBuffer.keySet()) { + for (final Product product : projectIdBuffer.get(projectId)) { + registerProject(product, projectId, false); + } + } + + projectIdBuffer.clear(); + } + + /** + * Populates the version information in the passed {@link Builder}. This + * information is obtained by locating the bundle and using its version + * metadata. The Git hash code is acquired from the manifest. + *

    + * The method returns without error if the bundle could not be found. + * + * @param featureUseBuilder to insert feature use information into + * (required) + * @param bundleSymbolicName to locate (required) + */ + private void populateVersionInfoIfPossible(final Builder featureUseBuilder, + final String bundleSymbolicName) { + Version version = bsnVersionCache.get(bundleSymbolicName); + String commitHash = bsnCommitHashCache.get(bundleSymbolicName); + + if (version == null) { + for (final Bundle b : bundleContext.getBundles()) { + if (bundleSymbolicName.equals(b.getSymbolicName())) { + version = b.getVersion(); + bsnVersionCache.put(bundleSymbolicName, version); + final Object manifestResult = b.getHeaders().get( + "Git-Commit-Hash"); + if (manifestResult != null) { + commitHash = manifestResult.toString(); + bsnCommitHashCache.put(bundleSymbolicName, commitHash); + } + break; + } + } + } + + if (version == null) { + // Can't acquire OSGi version information for this bundle, so give + // up now + return; + } + + featureUseBuilder.setMajorVersion(version.getMajor()); + featureUseBuilder.setMinorVersion(version.getMinor()); + featureUseBuilder.setPatchVersion(version.getMicro()); + featureUseBuilder.setReleaseQualifier(version.getQualifier()); + if (commitHash != null && commitHash.length() > 0) { + featureUseBuilder.setSourceControlIdentifier(commitHash); + } + } + + public void registerBundleSymbolicNameUse(final String bundleSymbolicName, + final String customJson) { + registerBundleSymbolicNameUse(bundleSymbolicName, customJson, true); + } + + private void registerBundleSymbolicNameUse(final String bundleSymbolicName, + String customJson, final boolean flushWhenDone) { + Validate.notBlank(bundleSymbolicName, "Bundle symbolic name required"); + + // Ensure it's a public feature (we do not want to log or buffer private + // features) + if (!publicFeatureResolver.isPublic(bundleSymbolicName)) { + return; + } + + // Turn a null custom JSON into "" for simplicity later, including + // buffering + if (customJson == null) { + customJson = ""; + } + + // If we cannot persist it at present, buffer it for potential + // persistence later on + if (!uaaService.isUaaTermsOfUseAccepted()) { + bsnBuffer.put(bundleSymbolicName, customJson); + return; + } + + // Create feature data bytes if possible + byte[] featureData = null; + if (!"".equals(customJson)) { + try { + featureData = customJson.getBytes("UTF-8"); + } + catch (final Exception ignore) { + } + } + + // Go and register it + final FeatureUse.Builder featureUseBuilder = FeatureUse.newBuilder(); + featureUseBuilder.setName(bundleSymbolicName); + populateVersionInfoIfPossible(featureUseBuilder, bundleSymbolicName); + + if (featureData == null) { + // Use this UaaService method, as we want to preserve any feature + // data we might have presented previously but hasn't yet been + // communicated (important since UAA 1.0.1 due to its delayed + // uploads) + uaaService.registerFeatureUsage(SPRING_ROO, + featureUseBuilder.build()); + } + else { + // New feature data is available, so treat this as overwriting any + // existing feature data we might have stored previously + uaaService.registerFeatureUsage(SPRING_ROO, + featureUseBuilder.build(), featureData); + } + + // Try to flush the buffer while we're at it, given persistence seems to + // be OK at present + if (flushWhenDone) { + flushIfPossible(); + } + } + + public void registerProject(final Product product, final String projectId) { + registerProject(product, projectId, true); + } + + private void registerProject(final Product product, final String projectId, + final boolean flushWhenDone) { + Validate.notNull(product, "Product required"); + Validate.notBlank(projectId, "Project ID required"); + + // If we cannot persist it at present, buffer it for potential + // persistence later on + if (!uaaService.isUaaTermsOfUseAccepted()) { + List value = projectIdBuffer.get(projectId); + if (value == null) { + value = new ArrayList(); + projectIdBuffer.put(projectId, value); + } + // We don't buffer it if there's an "identical" (by name and + // version) product in there already + boolean add = true; + for (final Product existing : value) { + if (existing.getName().equals(product.getName()) + && existing.getMajorVersion() == product + .getMajorVersion() + && existing.getMinorVersion() == product + .getMinorVersion() + && existing.getPatchVersion() == product + .getPatchVersion() + && existing.getReleaseQualifier().equals( + product.getReleaseQualifier()) + && existing.getSourceControlIdentifier().equals( + product.getSourceControlIdentifier())) { + add = false; + break; + } + } + if (add) { + value.add(product); + } + return; + } + + uaaService.registerProductUsage(product, projectId); + + // Try to flush the buffer while we're at it, given persistence seems to + // be OK at present + if (flushWhenDone) { + flushIfPossible(); + } + } + + public void requestTransmission() { + if (uaaService instanceof TransmissionAwareUaaService) { + final TransmissionAwareUaaService ta = (TransmissionAwareUaaService) uaaService; + ta.requestTransmission(); + } + } +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/UaaRelatedComponentRegistrationHelper.java b/uaa/src/main/java/org/springframework/roo/uaa/UaaRelatedComponentRegistrationHelper.java new file mode 100644 index 000000000..54987a2ae --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/UaaRelatedComponentRegistrationHelper.java @@ -0,0 +1,46 @@ +package org.springframework.roo.uaa; + +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; + +import org.apache.felix.scr.annotations.Component; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.springframework.uaa.client.ProxyService; +import org.springframework.uaa.client.UaaDetectedProducts; +import org.springframework.uaa.client.UaaService; +import org.springframework.uaa.client.UaaServiceFactory; + +/** + * Registers various UAA-provided instances as OSGi components. This uses the + * UAA factories and conventions, plus simplifies replacement by STS. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Component +public class UaaRelatedComponentRegistrationHelper { + + private final Set registrations = new HashSet(); + + protected void activate(final ComponentContext context) { + registrations.add(context.getBundleContext().registerService( + UaaService.class.getName(), UaaServiceFactory.getUaaService(), + new Hashtable())); + registrations.add(context.getBundleContext().registerService( + UaaDetectedProducts.class.getName(), + UaaServiceFactory.getUaaDetectedProducts(), + new Hashtable())); + registrations.add(context.getBundleContext().registerService( + ProxyService.class.getName(), + UaaServiceFactory.getProxyService(), + new Hashtable())); + } + + protected void deactivate(final ComponentContext context) { + for (final ServiceRegistration registration : registrations) { + registration.unregister(); + } + } +} diff --git a/uaa/src/main/java/org/springframework/roo/uaa/UaaShellStatusListener.java b/uaa/src/main/java/org/springframework/roo/uaa/UaaShellStatusListener.java new file mode 100644 index 000000000..32d954d53 --- /dev/null +++ b/uaa/src/main/java/org/springframework/roo/uaa/UaaShellStatusListener.java @@ -0,0 +1,64 @@ +package org.springframework.roo.uaa; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.osgi.service.component.ComponentContext; +import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.event.ShellStatus; +import org.springframework.roo.shell.event.ShellStatus.Status; +import org.springframework.roo.shell.event.ShellStatusListener; +import org.springframework.roo.support.util.MessageDisplayUtils; +import org.springframework.uaa.client.UaaService; +import org.springframework.uaa.client.protobuf.UaaClient.Privacy.PrivacyLevel; + +/** + * Provides a startup-time reminder of the 'uaa status' command if the user + * hasn't indicated a UAA Terms of Use acceptance or rejection. + *

    + * This class is separate from the other {@link ShellStatusListener} in the UAA + * module due to of lifecycle timing reasons. It needs minimal dependencies on + * other SCR components. + * + * @author Ben Alex + * @since 1.1.1 + */ +@Service +@Component +public class UaaShellStatusListener implements ShellStatusListener { + + @Reference Shell shell; + private boolean startupMessageConsidered = false; + @Reference UaaService uaaService; + + protected void activate(final ComponentContext componentContext) { + shell.addShellStatusListener(this); + final String originalThreadName = Thread.currentThread().getName(); + try { + // Preventing thread name appearing on JLine console + Thread.currentThread().setName(""); + onShellStatusChange(null, shell.getShellStatus()); + } + finally { + Thread.currentThread().setName(originalThreadName); + } + } + + protected void deactivate(final ComponentContext componentContext) { + shell.removeShellStatusListener(this); + } + + public void onShellStatusChange(final ShellStatus oldStatus, + final ShellStatus newStatus) { + if (!startupMessageConsidered + && newStatus.getStatus() == Status.USER_INPUT) { + startupMessageConsidered = true; + if (uaaService.getPrivacyLevel() == PrivacyLevel.UNDECIDED_TOU) { + // NB: The first line of the text file must contain spaces to + // overwrite the roo> prompt on the current line + MessageDisplayUtils.displayFile("startup_undecided.txt", + ShellListeningUaaRegistrationFacility.class, true); + } + } + } +} diff --git a/uaa/src/main/resources/org/springframework/roo/uaa/accepted_tou.txt b/uaa/src/main/resources/org/springframework/roo/uaa/accepted_tou.txt new file mode 100644 index 000000000..59a431f6e --- /dev/null +++ b/uaa/src/main/resources/org/springframework/roo/uaa/accepted_tou.txt @@ -0,0 +1 @@ +Thank you. All Spring Roo download features have now been enabled. diff --git a/uaa/src/main/resources/org/springframework/roo/uaa/declined_tou.txt b/uaa/src/main/resources/org/springframework/roo/uaa/declined_tou.txt new file mode 100644 index 000000000..e77f0d928 --- /dev/null +++ b/uaa/src/main/resources/org/springframework/roo/uaa/declined_tou.txt @@ -0,0 +1,6 @@ +Thank you. We have recorded that you have declined the UAA Terms of Use. This +means no further reminders will be displayed in the future. + +Please be aware features requiring downloads from VMware-operated domains are +now disabled. You can type 'download status' and press ENTER at any time in +the future if you'd like to review the status or obtain help on UAA commands. diff --git a/uaa/src/main/resources/org/springframework/roo/uaa/startup_undecided.txt b/uaa/src/main/resources/org/springframework/roo/uaa/startup_undecided.txt new file mode 100644 index 000000000..712a41c2e --- /dev/null +++ b/uaa/src/main/resources/org/springframework/roo/uaa/startup_undecided.txt @@ -0,0 +1,4 @@ + +At this time you have not authorized Spring Roo to download an index of +available add-ons. This will reduce Spring Roo features available to you. +Please type 'download status' and press ENTER for further information. diff --git a/uaa/src/main/resources/org/springframework/roo/uaa/status_accepted.txt b/uaa/src/main/resources/org/springframework/roo/uaa/status_accepted.txt new file mode 100644 index 000000000..406e7c6fd --- /dev/null +++ b/uaa/src/main/resources/org/springframework/roo/uaa/status_accepted.txt @@ -0,0 +1,21 @@ + + **** DOWNLOAD CONSENT GRANTED **** + +You have previously accepted the Spring User Agent Analysis (UAA) Terms of Use +as displayed at http://www.springsource.org/uaa/terms_of_use. There is also a +FAQ available at http://www.springsource.org/uaa/faq. + +This means that all Spring Roo download functions are enabled. All applicable +add-on discovery and installation commands are fully enabled. + +If you'd like to change your mind and revoke your acceptance of the UAA Terms +of Use (and in turn disable Spring Roo features such as add-on support), you +may do so by typing 'download reject terms of use' and pressing ENTER. + +If you'd like to keep UAA enabled (and thus all Spring Roo features enabled) +but look at the exact data Spring UAA is sending, please type 'download view' +and press ENTER. You can also fine-tune the level of data that Spring UAA is +sending using the 'download privacy level' command. + +Questions on Spring UAA? Please ask us on the Spring Roo Community Forum at +http://forum.springsource.org/forumdisplay.php?f=67. We're happy to help. diff --git a/uaa/src/main/resources/org/springframework/roo/uaa/status_declined.txt b/uaa/src/main/resources/org/springframework/roo/uaa/status_declined.txt new file mode 100644 index 000000000..87f35fc69 --- /dev/null +++ b/uaa/src/main/resources/org/springframework/roo/uaa/status_declined.txt @@ -0,0 +1,29 @@ + + **** DOWNLOAD CONSENT DECLINED **** + +Spring Roo needs to communicate with VMware-operated domains to download +up-to-date information such as the availability of add-ons that offer extra +Spring Roo features. Other VMware software also needs to communicate in order +to provide some of their features. These downloads include anonymous usage +information that help us better understand how Spring is being used. See +http://www.springsource.org/uaa/terms_of_use for the full Spring User Agent +Analysis (UAA) Terms of Use. There is also an FAQ page available at +http://www.springsource.org/uaa/faq for your convenience. + +You have previously used the 'download reject terms of use' command or +rejected the Spring User Agent Analysis (UAA) Terms of Use in another Spring +tool. As a consequence, Spring Roo is unable to download up-to-date details +such as add-on indexes. While you can still use Spring Roo, features +requiring downloads from VMware-operated domains will remain disabled. + +You may change your mind and indicate you consent to the Spring User Agent +Analysis (UAA) Terms of Use at http://www.springsource.org/uaa/terms_of_use. +To indicate your consent, please type 'download accept terms of use' and press +ENTER. If you do not consent to the Terms of Use, you do not need to do +anything as your previous rejection of the Terms of Use will remain in effect. + +Next steps: + +* To enable downloads, type 'download accept terms of use' and press ENTER + +* To continue to disable downloads and reminders, do nothing diff --git a/uaa/src/main/resources/org/springframework/roo/uaa/status_undecided.txt b/uaa/src/main/resources/org/springframework/roo/uaa/status_undecided.txt new file mode 100644 index 000000000..13046e08f --- /dev/null +++ b/uaa/src/main/resources/org/springframework/roo/uaa/status_undecided.txt @@ -0,0 +1,31 @@ + + **** DOWNLOAD CONSENT REQUIRED **** + +Spring Roo needs to download resources from VMware domains to improve your +experience. We include anonymous usage information as part of these downloads. + +The Spring team gathers anonymous usage information to improve your Spring +experience, not for marketing purposes. For example, we collect anonymous +information about the usage of public Roo add-ons. This lets us offer you +higher quality add-on search results, highlighting the add-ons which are most +popular in the Roo community. We also use this information to help guide our +roadmap, prioritizing the features most valued by the community and enabling +us to optimize the compatibility of technologies frequently used together. + +Please see the Spring User Agent Analysis (UAA) Terms of Use at +http://www.springsource.org/uaa/terms_of_use for more information on what +information is collected and how such information is used. There is also an +FAQ at http://www.springsource.org/uaa/faq for your convenience. + +To consent to the Terms of Use, please type 'download accept terms of use' at +the command prompt and press ENTER. If you do not type 'download accept terms +of use' to indicate your consent, Spring Roo add-on discovery features will +not be available and anonymous data collection will remain disabled. + +Next steps: + +* To enable downloads, type 'download accept terms of use' and press ENTER + +* To disable downloads, type 'download reject terms of use' and press ENTER + +If you don't type either command, Roo will remind you next time it loads. diff --git a/url-stream-jdk/pom.xml b/url-stream-jdk/pom.xml new file mode 100644 index 000000000..06cd3b89b --- /dev/null +++ b/url-stream-jdk/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.url.stream.jdk + bundle + Spring Roo - URL Stream - JDK + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + + org.springframework.roo + org.springframework.roo.shell + + + org.springframework.roo + org.springframework.roo.support + + + org.springframework.roo + org.springframework.roo.support.osgi + + + org.springframework.roo + org.springframework.roo.url.stream + + + org.springframework.roo + org.springframework.roo.shell.osgi + + + + org.springframework.uaa + org.springframework.uaa.client + + + \ No newline at end of file diff --git a/url-stream-jdk/src/main/java/org/springframework/roo/url/stream/jdk/JdkUrlInputStreamService.java b/url-stream-jdk/src/main/java/org/springframework/roo/url/stream/jdk/JdkUrlInputStreamService.java new file mode 100644 index 000000000..1c2c34583 --- /dev/null +++ b/url-stream-jdk/src/main/java/org/springframework/roo/url/stream/jdk/JdkUrlInputStreamService.java @@ -0,0 +1,137 @@ +package org.springframework.roo.url.stream.jdk; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Level; + +import org.apache.commons.lang3.Validate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.osgi.AbstractFlashingObject; +import org.springframework.roo.url.stream.UrlInputStreamService; +import org.springframework.roo.url.stream.UrlInputStreamUtils; +import org.springframework.uaa.client.ProxyService; +import org.springframework.uaa.client.UaaService; + +/** + * Simple implementation of {@link UrlInputStreamService} that uses the JDK. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class JdkUrlInputStreamService extends AbstractFlashingObject implements + UrlInputStreamService { + + private class ProgressIndicatingInputStream extends InputStream { + private final InputStream delegate; + private long lastNotified; + private int lastPercentageIndicated = -1; + private float readSoFar; + private String text; + private final float totalSize; + + /** + * Constructor + * + * @param connection + * @throws IOException + */ + public ProgressIndicatingInputStream(final HttpURLConnection connection) + throws IOException { + Validate.notNull(connection, "URL Connection required"); + totalSize = connection.getContentLength(); + delegate = connection.getInputStream(); + text = connection.getURL().getPath(); + if ("".equals(text)) { + // Fall back to the host name + text = connection.getURL().getHost(); + } + else { + // We only want the filename + final int lastSlash = text.lastIndexOf("/"); + if (lastSlash > -1) { + text = text.substring(lastSlash + 1); + } + } + } + + @Override + public void close() throws IOException { + flash(Level.FINE, "", MY_SLOT); + delegate.close(); + } + + @Override + public int read() throws IOException { + readSoFar++; + if (totalSize > 0) { + // Total size is known + final int percentageDownloaded = Math.round(readSoFar + / totalSize * 100); + if (System.currentTimeMillis() > lastNotified + 1000) { + if (lastPercentageIndicated != percentageDownloaded) { + flash(Level.FINE, "Downloaded " + percentageDownloaded + + "% of " + text, MY_SLOT); + lastPercentageIndicated = percentageDownloaded; + lastNotified = System.currentTimeMillis(); + } + } + } + else { + // Total size is not known, rely on time-based updates instead + if (System.currentTimeMillis() > lastNotified + 1000) { + flash(Level.FINE, + "Downloaded " + Math.round(readSoFar / 1024) + + " kB of " + text, MY_SLOT); + lastNotified = System.currentTimeMillis(); + } + } + + final int result = delegate.read(); + if (result == -1) { + if (totalSize > 0) { + flash(Level.FINE, "Downloaded 100% of " + text, MY_SLOT); + } + else { + flash(Level.FINE, + "Downloaded " + Math.round(readSoFar / 1024) + + " kB of " + text, MY_SLOT); + } + flash(Level.FINE, "", MY_SLOT); + } + + return result; + } + } + + @Reference private ProxyService proxyService; + @Reference private UaaService uaaService; + + public String getUrlCannotBeOpenedMessage(final URL httpUrl) { + if (uaaService.isCommunicationRestricted(httpUrl)) { + if (!uaaService.isUaaTermsOfUseAccepted()) { + return UrlInputStreamUtils.SETUP_UAA_REQUIRED; + } + } + // No reason it shouldn't work + return null; + } + + public InputStream openConnection(final URL httpUrl) throws IOException { + Validate.notNull(httpUrl, "HTTP URL is required"); + Validate.isTrue(httpUrl.getProtocol().equals("http"), + "Only HTTP is supported (not %s)", httpUrl); + + // Fail if we're banned from accessing this domain + Validate.isTrue(getUrlCannotBeOpenedMessage(httpUrl) == null, + UrlInputStreamUtils.SETUP_UAA_REQUIRED); + final HttpURLConnection connection = proxyService + .prepareHttpUrlConnection(httpUrl); + return new ProgressIndicatingInputStream(connection); + } +} \ No newline at end of file diff --git a/url-stream-jdk/src/main/java/org/springframework/roo/url/stream/jdk/ProxyConfigurationCommands.java b/url-stream-jdk/src/main/java/org/springframework/roo/url/stream/jdk/ProxyConfigurationCommands.java new file mode 100644 index 000000000..d442f0a32 --- /dev/null +++ b/url-stream-jdk/src/main/java/org/springframework/roo/url/stream/jdk/ProxyConfigurationCommands.java @@ -0,0 +1,51 @@ +package org.springframework.roo.url.stream.jdk; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; +import org.springframework.roo.shell.CliCommand; +import org.springframework.roo.shell.CommandMarker; +import org.springframework.uaa.client.ProxyService; + +/** + * Provides the ability to configure a proxy server for usage by + * {@link JdkUrlInputStreamService}. + * + * @author Ben Alex + * @since 1.1 + */ +@Component +@Service +public class ProxyConfigurationCommands implements CommandMarker { + + @Reference private ProxyService proxyService; + + @CliCommand(value = "proxy configuration", help = "Shows the proxy server configuration") + public String proxyConfiguration() throws MalformedURLException { + final Proxy p = proxyService.setupProxy(new URL( + "http://www.springsource.org/roo")); + final StringBuilder sb = new StringBuilder(); + if (p == null) { + sb.append( + " *** Your system has no proxy setup ***") + .append(LINE_SEPARATOR); + sb.append( + "http://download.oracle.com/javase/6/docs/technotes/guides/net/proxies.html offers useful information.") + .append(LINE_SEPARATOR); + sb.append( + "For most people, simply edit /etc/java-6-openjdk/net.properties (or equivalent) and set the") + .append(LINE_SEPARATOR); + sb.append("java.net.useSystemProxies=true property to use your operating system-defined proxy settings."); + } + else { + sb.append("Proxy to use: ").append(p); + } + return sb.toString(); + } +} diff --git a/url-stream/pom.xml b/url-stream/pom.xml new file mode 100644 index 000000000..e59f398ed --- /dev/null +++ b/url-stream/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.springframework.roo + org.springframework.roo.osgi.roo.bundle + 2.0.0.BUILD-SNAPSHOT + ../osgi-roo-bundle + + org.springframework.roo.url.stream + bundle + Spring Roo - URL Stream API Contract + + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + org.apache.felix + org.apache.felix.scr.annotations + + + \ No newline at end of file diff --git a/url-stream/src/main/java/org/springframework/roo/url/stream/UrlInputStreamService.java b/url-stream/src/main/java/org/springframework/roo/url/stream/UrlInputStreamService.java new file mode 100644 index 000000000..fc0e3b57f --- /dev/null +++ b/url-stream/src/main/java/org/springframework/roo/url/stream/UrlInputStreamService.java @@ -0,0 +1,46 @@ +package org.springframework.roo.url.stream; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * Provides an {@link InputStream} for a given HTTP {@link URL}. + *

    + * This implementation can be used to provide an alternative mechanism to + * download resources. It can, for instance, populate proxy details, delegate to + * a download agent provided by a host framework (like an IDE) and augment the + * headers of the HTTP request. + * + * @author Ben Alex + * @since 1.1 + */ +public interface UrlInputStreamService { + + /** + * Returns a reason a URL cannot be opened, or null if there is no known + * reason why the URL wouldn't be able to be opened if + * {@link #openConnection(URL)} was invoked. The returned reasons should be + * formatted in a user-friendly manner for direct display to Roo users. + *

    + * The purpose of this method is to allow restrictions to be placed on the + * availability of URLs. For example, if the user has indicated offline + * operation is needed, or if the user needs to complete an enabling step + * such as terms of use acceptance. + * + * @param httpUrl desired to open (HTTP only, never HTTPS or another + * protocol) + * @return null if URL can probably be opened, or a message why that URL is + * unavailable + */ + String getUrlCannotBeOpenedMessage(URL httpUrl); + + /** + * Opens an input stream to the specified connection. The input stream + * represents the resource (no headers). + * + * @param httpUrl to open (HTTP only, never HTTPS or another protocol) + * @return the input stream (implementation may not return null) + */ + InputStream openConnection(URL httpUrl) throws IOException; +} diff --git a/url-stream/src/main/java/org/springframework/roo/url/stream/UrlInputStreamUtils.java b/url-stream/src/main/java/org/springframework/roo/url/stream/UrlInputStreamUtils.java new file mode 100644 index 000000000..4d8dd5252 --- /dev/null +++ b/url-stream/src/main/java/org/springframework/roo/url/stream/UrlInputStreamUtils.java @@ -0,0 +1,24 @@ +package org.springframework.roo.url.stream; + +import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; + +/** + * Represents utility members for implementation of + * {@link UrlInputStreamService}s. + * + * @author Ben Alex + * @since 1.1.1 + */ +public final class UrlInputStreamUtils { + + public static final String SETUP_UAA_REQUIRED = LINE_SEPARATOR + + "At this time you have not authorized Spring Roo to download resources from" + + LINE_SEPARATOR + + "VMware domains. Some Spring Roo features are therefore unavailable. Please" + + LINE_SEPARATOR + + "type 'download status' and press ENTER for further information." + + LINE_SEPARATOR; + + private UrlInputStreamUtils() { + } +}