diff --git a/.gradle/1.0-milestone-3/taskArtifacts/.gitignore b/.gradle/1.0-milestone-3/taskArtifacts/.gitignore new file mode 100644 index 00000000..b34d8944 --- /dev/null +++ b/.gradle/1.0-milestone-3/taskArtifacts/.gitignore @@ -0,0 +1,2 @@ +/cache.bin +/cache.properties diff --git a/.springBeans b/.springBeans new file mode 100644 index 00000000..ea2a215b --- /dev/null +++ b/.springBeans @@ -0,0 +1,14 @@ + + + 1 + + + + + + + src/main/resources/META-INF/spring/test.xml + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..891c7fd9 --- /dev/null +++ b/build.gradle @@ -0,0 +1,210 @@ +// used for artifact names, building doc upload urls, etc. +description = 'Spring Shell' +abbreviation = 'SPSH' + +apply plugin: 'base' + +buildscript { + repositories { + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "GitHub" + addIvyPattern 'http://cloud.github.com/downloads/costin/gradle-stuff/[organization].[module]-[artifact]-[revision].[ext]' + addArtifactPattern 'http://cloud.github.com/downloads/costin/gradle-stuff/[organization].[module]-[revision].[ext]' + } + mavenCentral() + mavenLocal() + mavenRepo name: "springsource-org-release", urls: "http://repository.springsource.com/maven/bundles/release" + mavenRepo name: "springsource-org-external", urls: "http://repository.springsource.com/maven/bundles/external" + + } + + dependencies { + classpath 'org.springframework:gradle-stuff:0.1-20110421' + classpath 'net.sf.docbook:docbook-xsl:1.75.2:ns-resources@zip' + } +} + +allprojects { + group = 'org.springframework.shell' + version = "$springShellVersion" + + releaseBuild = version.endsWith('RELEASE') + snapshotBuild = version.endsWith('SNAPSHOT') + + + repositories { + mavenLocal() + mavenCentral() + // Public Spring artefacts + mavenRepo name: "springsource-org-release", urls: "http://repository.springsource.com/maven/bundles/release" + mavenRepo name: "spring-release", urls: "http://maven.springframework.org/release" + mavenRepo name: "spring-milestone", urls: "http://maven.springframework.org/milestone" + mavenRepo name: "spring-snapshot", urls: "http://maven.springframework.org/snapshot" + mavenRepo name: "sonatype-snapshot", urls: "http://oss.sonatype.org/content/repositories/snapshots" + mavenRepo name: "ext-snapshots", urls: "http://springframework.svn.sourceforge.net/svnroot/springframework/repos/repo-ext/" + mavenRepo name: "data-nucleus", urls: "http://www.datanucleus.org/downloads/maven2/" + mavenRepo name: "conjars.org", urls: "http://conjars.org/repo" + } +} + +apply plugin: "java" +apply plugin: "maven" +apply plugin: 'eclipse' // `gradle eclipse` to generate .classpath/.project +apply plugin: 'idea' // `gradle idea` to generate .ipr/.iml +apply plugin: 'docbook' + + +// Common dependencies +dependencies { + + // Logging + runtime "log4j:log4j:$log4jVersion" + + // Spring Framework + compile "org.springframework:spring-core:$springVersion" + compile "org.springframework:spring-context-support:$springVersion" + compile "commons-io:commons-io:$commonsioVersion" + compile "net.sourceforge.jline:jline:$jlineVersion" + compile "org.fusesource.jansi:jansi:$jansiVersion" + + // needed for use of @Configuration extension points. + compile "cglib:cglib:$cglibVersion" + + + // Testing + testCompile "junit:junit:$junitVersion" + testCompile "org.mockito:mockito-core:$mockitoVersion" + testCompile "org.springframework:spring-test:$springVersion" + testCompile("javax.annotation:jsr250-api:1.0") { optional = true } + +} + +javaprojects = rootProject + +sourceCompatibility = 1.6 +targetCompatibility = 1.6 + +javadoc { + srcDir = file("${projectDir}/docs/src/api") + destinationDir = file("${buildDir}/api") + tmpDir = file("${buildDir}/api-work") + + configure(options) { + stylesheetFile = file("${srcDir}/spring-javadoc.css") + overview = "${srcDir}/overview.html" + docFilesSubDirs = true + outputLevel = org.gradle.external.javadoc.JavadocOutputLevel.QUIET + breakIterator = true + author = true + showFromProtected() + +// groups = [ +// 'Spring Data Hadoop' : ['org.springframework.data.hadoop*'], +// ] + + links = [ + "http://static.springframework.org/spring/docs/3.0.x/javadoc-api", + "http://download.oracle.com/javase/6/docs/api", + "http://logging.apache.org/log4j/docs/api/", + ] + + exclude "org/springframework/data/hadoop/config/**" + } + + title = "${rootProject.description} ${version} API" + + // collect all the sources that will be included in the javadoc output + source javaprojects.collect {project -> + project.sourceSets.main.allJava + } + + // collect all main classpaths to be able to resolve @see refs, etc. + // this collection also determines the set of projects that this + // task dependsOn, thus the runtimeClasspath is used to ensure all + // projects are included, not just *dependencies* of all classes. + // this is awkward and took me a while to figure out. + classpath = files(javaprojects.collect {project -> + project.sourceSets.main.runtimeClasspath + }) + + // copy the images from the doc-files dir over to the target + doLast { task -> + copy { + from file("${task.srcDir}/doc-files") + into file("${task.destinationDir}/doc-files") + } + } +} + +ideaProject { + withXml { provider -> + provider.node.component.find { it.@name == 'VcsDirectoryMappings' }.mapping.@vcs = 'Git' + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '1.0-milestone-3' + description = "Generate the Gradle wrapper" + group = "Distribution" +} + +apply from: "$rootDir/maven.gradle" + +// Distribution tasks +task dist(type: Zip) { + description = "Generate the ZIP Distribution" + group = "Distribution" + dependsOn assemble, subprojects*.tasks*.matching { task -> task.name == 'assemble' } + +// evaluationDependsOn(':docs') + + def zipRootDir = "${project.name}-$version" + into(zipRootDir) { + from('/docs/src/info') { + include '*.txt' + } + from('/docs/build/') { + into 'docs' + include 'reference/**/*' + } + from('samples/') { + into 'samples' + exclude '**/build/**' + exclude '**/bin/**' + exclude '**/.settings/**' + exclude '**/.gradle/**' + exclude '**/.*' + } + from('build/') { + into 'docs' + include 'api/**/*' + } + into('dist') { + from javaprojects.collect {project -> project.libsDir } + } + } + doLast { + ant.checksum(file: archivePath, algorithm: 'SHA1', fileext: '.sha1') + } +} + +task uploadDist(type: org.springframework.gradle.tasks.S3DistroUpload, dependsOn: dist) { + description = "Upload the ZIP Distribution" + group = "Distribution" + archiveFile = dist.archivePath + projectKey = 'SHDP' + projectName = 'Spring Data Hadoop' +} + +assemble.dependsOn = ['jar', 'sourceJar', 'javadocJar'] + +task run(type: JavaExec) { + description = 'Runs the application' + main = "org.springframework.shell.Bootstrap" + classpath = sourceSets.main.runtimeClasspath +} + +//defaultTasks 'run' + + +defaultTasks 'build' \ No newline at end of file diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 00000000..19481606 --- /dev/null +++ b/docs/build.gradle @@ -0,0 +1,102 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +// ----------------------------------------------------------------------------- +// Configuration for the docs subproject +// ----------------------------------------------------------------------------- + +apply plugin: 'base' +apply plugin: 'docbook' + +assemble.dependsOn = [rootProject.javadoc, 'reference'] + +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.group = 'Documentation' +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.sourceFileName = 'index.xml' +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.sourceDirectory = new File(projectDir, 'src/reference/docbook') + +docbookHtml.stylesheet = new File(projectDir, 'src/reference/resources/xsl/html-custom.xsl') +docbookHtmlSingle.stylesheet = new File(projectDir, 'src/reference/resources/xsl/html-single-custom.xsl') +docbookFoPdf.stylesheet = new File(projectDir, 'src/reference/resources/xsl/pdf-custom.xsl') + +def imagesDir = new File(projectDir, 'src/reference/resources/images'); +[docbookHtml, docbookFoPdf, docbookHtmlSingle]*.admonGraphicsPath = "./images/admon/" +[docbookFoPdf]*.imgSrcPath = "${imagesDir}" +[docbookHtml, docbookHtmlSingle]*.imgSrcPath = "./images/" + +// defined separately to prevent the replacement from taking place (seems to affect the images) +imgSpec = copySpec { + into ('reference') { + from("$projectDir/src/reference/resources") { + include 'css/**/*' + } + from("$buildDir/docs") { + include '*.pdf' + } + } + + into ('reference/images') { + from (imagesDir) + } +} + + +refSpec = copySpec { + into ('reference') { + from("$buildDir/docs") { + exclude '*.fo' + exclude '*.pdf' + } + } + + p = new Properties() + + for (e in project.properties) { + if (e.key != null && e.value != null) + p.setProperty(e.key, e.value.toString()) + } + + filter(ReplaceTokens, tokens: p) + + with(imgSpec) +} + +task reference (type: Copy) { + dependsOn 'docbook' + description = "Builds aggregated DocBook" + group = "Documentation" + destinationDir = buildDir + with(refSpec) +} + + +apiSpec = copySpec { + into('api') { + from(rootProject.javadoc.destinationDir) + } +} + +task docSiteLogin(type: org.springframework.gradle.tasks.Login) { + if (project.hasProperty('sshHost')) { + host = project.property('sshHost') + username = project.property('sshUsername') + key = project.property('sshPrivateKey') + } +} + +infoSpec = copySpec { + from("$projectDir/src/info") { + include 'changelog.txt' + } +} + +// upload task +task uploadDocs(type: org.springframework.gradle.tasks.ScpUpload) { + dependsOn rootProject.javadoc, reference + description = "Upload API Distribution" + group = "Distribution" + remoteDir = "/opt/www/domains/springframework.org/www/htdocs/spring-hadoop/docs/${project.version}" + login = docSiteLogin + + with(apiSpec) + with(refSpec) + with(infoSpec) +} \ No newline at end of file diff --git a/docs/build/highlighting/c-hl.xml b/docs/build/highlighting/c-hl.xml new file mode 100644 index 00000000..44ab02d2 --- /dev/null +++ b/docs/build/highlighting/c-hl.xml @@ -0,0 +1,101 @@ + + + + + /** + */ + + + + /// + + + + /* + */ + + // + + + # + \ + + + + " + \ + + + ' + \ + + + 0x + ul + lu + u + l + + + + . + + e + ul + lu + u + f + l + + + + auto + _Bool + break + case + char + _Complex + const + continue + default + do + double + else + enum + extern + float + for + goto + if + _Imaginary + inline + int + long + register + restrict + return + short + signed + sizeof + static + struct + switch + typedef + union + unsigned + void + volatile + while + + \ No newline at end of file diff --git a/docs/build/highlighting/cpp-hl.xml b/docs/build/highlighting/cpp-hl.xml new file mode 100644 index 00000000..2213b7ce --- /dev/null +++ b/docs/build/highlighting/cpp-hl.xml @@ -0,0 +1,150 @@ + + + + + /** + */ + + + + /// + + + + /* + */ + + // + + + # + \ + + + + " + \ + + + ' + \ + + + 0x + ul + lu + u + l + + + + . + + e + ul + lu + u + f + l + + + + + auto + _Bool + break + case + char + _Complex + const + continue + default + do + double + else + enum + extern + float + for + goto + if + _Imaginary + inline + int + long + register + restrict + return + short + signed + sizeof + static + struct + switch + typedef + union + unsigned + void + volatile + while + + asm + dynamic_cast + namespace + reinterpret_cast + try + bool + explicit + new + static_cast + typeid + catch + false + operator + template + typename + class + friend + private + this + using + const_cast + inline + public + throw + virtual + delete + mutable + protected + true + wchar_t + + \ No newline at end of file diff --git a/docs/build/highlighting/csharp-hl.xml b/docs/build/highlighting/csharp-hl.xml new file mode 100644 index 00000000..8a8a76d1 --- /dev/null +++ b/docs/build/highlighting/csharp-hl.xml @@ -0,0 +1,187 @@ + + + + + /** + */ + + + + /// + + + + /* + */ + + // + + + [ + ] + ( + ) + + + + # + \ + + + + + @" + " + \ + + + + " + \ + + + ' + \ + + + 0x + ul + lu + u + l + + + + . + + e + ul + lu + u + f + d + m + l + + + + abstract + as + base + bool + break + byte + case + catch + char + checked + class + const + continue + decimal + default + delegate + do + double + else + enum + event + explicit + extern + false + finally + fixed + float + for + foreach + goto + if + implicit + in + int + interface + internal + is + lock + long + namespace + new + null + object + operator + out + override + params + private + protected + public + readonly + ref + return + sbyte + sealed + short + sizeof + stackalloc + static + string + struct + switch + this + throw + true + try + typeof + uint + ulong + unchecked + unsafe + ushort + using + virtual + void + volatile + while + + + + add + alias + get + global + partial + remove + set + value + where + yield + + \ No newline at end of file diff --git a/docs/build/highlighting/delphi-hl.xml b/docs/build/highlighting/delphi-hl.xml new file mode 100644 index 00000000..2a45d29f --- /dev/null +++ b/docs/build/highlighting/delphi-hl.xml @@ -0,0 +1,200 @@ + + + + + + {$ + } + + + + + (*$ + ) + + + + { + } + + + (* + *) + + // + + ' + + + + #$ + + + + + # + + + + + $ + + + + . + e + + + + + and + else + inherited + packed + then + array + end + initialization + procedure + threadvar + as + except + inline + program + to + asm + exports + interface + property + try + begin + file + is + raise + type + case + final + label + record + unit + class + finalization + library + repeat + unsafe + const + finally + mod + resourcestring + until + constructor + for + nil + sealed + uses + destructor + function + not + set + var + dispinterface + goto + object + shl + while + div + if + of + shr + with + do + implementation + or + static + xor + downto + in + out + string + + + at + on + + + absolute + dynamic + local + platform + requires + abstract + export + message + private + resident + assembler + external + name + protected + safecall + automated + far + near + public + stdcall + cdecl + forward + nodefault + published + stored + contains + implements + overload + read + varargs + default + index + override + readonly + virtual + deprecated + inline + package + register + write + dispid + library + pascal + reintroduce + writeonly + + + \ No newline at end of file diff --git a/docs/build/highlighting/ini-hl.xml b/docs/build/highlighting/ini-hl.xml new file mode 100644 index 00000000..84be9c68 --- /dev/null +++ b/docs/build/highlighting/ini-hl.xml @@ -0,0 +1,45 @@ + + + + ; + + + ^(\[.+\]\s*)$ + + MULTILINE + + + + ^(.+)(?==) + + MULTILINE + + \ No newline at end of file diff --git a/docs/build/highlighting/java-hl.xml b/docs/build/highlighting/java-hl.xml new file mode 100644 index 00000000..d499d83f --- /dev/null +++ b/docs/build/highlighting/java-hl.xml @@ -0,0 +1,117 @@ + + + + + /** + */ + + + + /* + */ + + // + + " + \ + + + ' + \ + + + @ + ( + ) + + + 0x + + + + . + e + f + d + l + + + + abstract + boolean + break + byte + case + catch + char + class + const + continue + default + do + double + else + extends + final + finally + float + for + goto + if + implements + import + instanceof + int + interface + long + native + new + package + private + protected + public + return + short + static + strictfp + super + switch + synchronized + this + throw + throws + transient + try + void + volatile + while + + \ No newline at end of file diff --git a/docs/build/highlighting/javascript-hl.xml b/docs/build/highlighting/javascript-hl.xml new file mode 100644 index 00000000..5749b887 --- /dev/null +++ b/docs/build/highlighting/javascript-hl.xml @@ -0,0 +1,147 @@ + + + + + /* + */ + + // + + " + \ + + + ' + \ + + + 0x + + + + . + e + + + + break + case + catch + continue + default + delete + do + else + finally + for + function + if + in + instanceof + new + return + switch + this + throw + try + typeof + var + void + while + with + + abstract + boolean + byte + char + class + const + debugger + double + enum + export + extends + final + float + goto + implements + import + int + interface + long + native + package + private + protected + public + short + static + super + synchronized + throws + transient + volatile + + + prototype + + Array + Boolean + Date + Error + EvalError + Function + Math + Number + Object + RangeError + ReferenceError + RegExp + String + SyntaxError + TypeError + URIError + + decodeURI + decodeURIComponent + encodeURI + encodeURIComponent + eval + isFinite + isNaN + parseFloat + parseInt + + Infinity + NaN + undefined + + \ No newline at end of file diff --git a/docs/build/highlighting/m2-hl.xml b/docs/build/highlighting/m2-hl.xml new file mode 100644 index 00000000..a3ef3142 --- /dev/null +++ b/docs/build/highlighting/m2-hl.xml @@ -0,0 +1,90 @@ + + + + + (* + *) + + + " + + + ' + + + . + e + + + + and + array + begin + by + case + const + definition + div + do + else + elsif + end + exit + export + for + from + if + implementation + import + in + loop + mod + module + not + of + or + pointer + procedure + qualified + record + repeat + return + set + then + to + type + until + var + while + with + + + \ No newline at end of file diff --git a/docs/build/highlighting/myxml-hl.xml b/docs/build/highlighting/myxml-hl.xml new file mode 100644 index 00000000..efa90d7b --- /dev/null +++ b/docs/build/highlighting/myxml-hl.xml @@ -0,0 +1,116 @@ + + + + + + + + A + ABBR + ACRONYM + ADDRESS + APPLET + AREA + B + BASE + BASEFONT + BDO + BIG + BLOCKQUOTE + BODY + BR + BUTTON + CAPTION + CENTER + CITE + CODE + COL + COLGROUP + DD + DEL + DFN + DIR + DIV + DL + DT + EM + FIELDSET + FONT + FORM + FRAME + FRAMESET + H1 + H2 + H3 + H4 + H5 + H6 + HEAD + HR + HTML + I + IFRAME + IMG + INPUT + INS + ISINDEX + KBD + LABEL + LEGEND + LI + LINK + MAP + MENU + META + NOFRAMES + NOSCRIPT + OBJECT + OL + OPTGROUP + OPTION + P + PARAM + PRE + Q + S + SAMP + SCRIPT + SELECT + SMALL + SPAN + STRIKE + STRONG + STYLE + SUB + SUP + TABLE + TBODY + TD + TEXTAREA + TFOOT + TH + THEAD + TITLE + TR + TT + U + UL + VAR + XMP + + + + + xsl: + + + + \ No newline at end of file diff --git a/docs/build/highlighting/perl-hl.xml b/docs/build/highlighting/perl-hl.xml new file mode 100644 index 00000000..23fdfd82 --- /dev/null +++ b/docs/build/highlighting/perl-hl.xml @@ -0,0 +1,120 @@ + + + + # + + << + ' + " + + + + " + \ + + + ' + \ + + + + 0x + + + + . + + + + + if + unless + while + until + foreach + else + elsif + for + when + default + given + + caller + continue + die + do + dump + eval + exit + goto + last + next + redo + return + sub + wantarray + + caller + import + local + my + package + use + + do + import + no + package + require + use + + bless + dbmclose + dbmopen + package + ref + tie + tied + untie + use + + and + or + not + eq + ne + lt + gt + le + ge + cmp + + \ No newline at end of file diff --git a/docs/build/highlighting/php-hl.xml b/docs/build/highlighting/php-hl.xml new file mode 100644 index 00000000..0daa348c --- /dev/null +++ b/docs/build/highlighting/php-hl.xml @@ -0,0 +1,149 @@ + + + + + /** + */ + + + + /// + + + + /* + */ + + // + # + + " + \ + + + + ' + \ + + + + <<< + + + 0x + + + + . + e + + + + and + or + xor + __FILE__ + exception + __LINE__ + array + as + break + case + class + const + continue + declare + default + die + do + echo + else + elseif + empty + enddeclare + endfor + endforeach + endif + endswitch + endwhile + eval + exit + extends + for + foreach + function + global + if + include + include_once + isset + list + new + print + require + require_once + return + static + switch + unset + use + var + while + __FUNCTION__ + __CLASS__ + __METHOD__ + final + php_user_filter + interface + implements + extends + public + private + protected + abstract + clone + try + catch + throw + cfunction + old_function + true + false + + + + + ?> + <?php + <?= + + + \ No newline at end of file diff --git a/docs/build/highlighting/python-hl.xml b/docs/build/highlighting/python-hl.xml new file mode 100644 index 00000000..1b1f0873 --- /dev/null +++ b/docs/build/highlighting/python-hl.xml @@ -0,0 +1,100 @@ + + + + + + @ + ( + ) + + # + + """ + + + + ''' + + + + " + \ + + + ' + \ + + + 0x + l + + + + . + + e + l + + + + and + del + from + not + while + as + elif + global + or + with + assert + else + if + pass + yield + break + except + import + print + class + exec + in + raise + continue + finally + is + return + def + for + lambda + try + + \ No newline at end of file diff --git a/docs/build/highlighting/ruby-hl.xml b/docs/build/highlighting/ruby-hl.xml new file mode 100644 index 00000000..2f743528 --- /dev/null +++ b/docs/build/highlighting/ruby-hl.xml @@ -0,0 +1,109 @@ + + + + # + + << + + + + " + \ + + + %Q{ + } + \ + + + %/ + / + \ + + + ' + \ + + + %q{ + } + \ + + + 0x + + + + . + e + + + + alias + and + BEGIN + begin + break + case + class + def + defined + do + else + elsif + END + end + ensure + false + for + if + in + module + next + nil + not + or + redo + rescue + retry + return + self + super + then + true + undef + unless + until + when + while + yield + + \ No newline at end of file diff --git a/docs/build/highlighting/tcl-hl.xml b/docs/build/highlighting/tcl-hl.xml new file mode 100644 index 00000000..7a8fa9fb --- /dev/null +++ b/docs/build/highlighting/tcl-hl.xml @@ -0,0 +1,180 @@ + + + + # + + " + \ + + + -[\p{javaJavaIdentifierStart}][\p{javaJavaIdentifierPart}]+ + + + + + . + + + + + if + then + else + elseif + for + foreach + break + continue + while + eval + case + in + switch + default + exit + error + proc + rename + exec + return + uplevel + upvar + constructor + destructor + itcl_class + loop + for_array_keys + for_recursive_glob + for_file + method + body + configbody + catch + namespace + class + array + set + unset + package + source + + + subst + list + format + lappend + option + expr + puts + winfo + lindex + string + + + + verified + seteach + fixme + debug + rtl::debug + rtl::verified + rtl::template + rtl::seteach + + + mkProc + getCreator + properties + lappendunique + rtl::lappendunique + + + place + pack + grid + + + + image + font + focus + tk + bind + after + + + toplevel + frame + entry + listbox + button + radiobutton + checkbutton + canvas + menu + menubutton + text + label + message + + + + zinc + + + tkpath::gradient + + + rtl_combobox + rtl_tree + rtl_tabset + rtl_mlistbox + rtl_gridwin + rtlysizer + rtlxsizer + + + + goolbar + gstripes + zoolbar + gistbox + gooleditor + galette + + + \ No newline at end of file diff --git a/docs/build/highlighting/xslthl-config.xml b/docs/build/highlighting/xslthl-config.xml new file mode 100644 index 00000000..910289ae --- /dev/null +++ b/docs/build/highlighting/xslthl-config.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/api/doc-files/th-background.png b/docs/src/api/doc-files/th-background.png new file mode 100644 index 00000000..72d65e77 Binary files /dev/null and b/docs/src/api/doc-files/th-background.png differ diff --git a/docs/src/api/overview.html b/docs/src/api/overview.html new file mode 100644 index 00000000..38647b7f --- /dev/null +++ b/docs/src/api/overview.html @@ -0,0 +1,24 @@ + + +This document is the API specification for the Spring Data Hadoop project. +
+ +
+ +

+ If you are interested in commercial training, consultancy and + support for the Spring Data Hadoop project, + SpringSource provides + such commercial support. +

+
+ + \ No newline at end of file diff --git a/docs/src/api/spring-javadoc.css b/docs/src/api/spring-javadoc.css new file mode 100644 index 00000000..1f009c4b --- /dev/null +++ b/docs/src/api/spring-javadoc.css @@ -0,0 +1,48 @@ +/* Spring-specific Javadoc style sheet rules */ + +#overviewBody { + +} + +.code { + border: 1px solid black; + background-color: #F4F4F4; + padding: 5px; +} + +/* Vanilla Javadoc style sheet rules */ + +body { + font-family: Helvetica, Arial, sans-serif; + background-color: white; + font-size: 10pt; +} + +td { font-size: 10pt; font-family: Helvetica, Arial, sans-serif }/* Javadoc style sheet */ + +/* Define colors, fonts and other style attributes here to override the defaults */ + +/* Page background color */ +body { background-color: #FFFFFF } + +/* Headings */ +h1 { font-size: 145% } + +/* Table colors */ +.TableHeadingColor { background: #CCCCFF } /* Dark mauve */ +.TableSubHeadingColor { background: #EEEEFF } /* Light mauve */ +.TableRowColor { background: #FFFFFF } /* White */ + +/* Font used in left-hand frame lists */ +.FrameTitleFont { font-size: 100%; font-family: Helvetica, Arial, sans-serif } +.FrameHeadingFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } +.FrameItemFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } + +/* Navigation bar fonts and colors */ +.NavBarCell1 { background-color:#EEEEFF;} /* Light mauve */ +.NavBarCell1Rev { background-color:#00008B;} /* Dark Blue */ +.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} +.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} + +.NavBarCell2 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} +.NavBarCell3 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} diff --git a/docs/src/info/changelog.txt b/docs/src/info/changelog.txt new file mode 100644 index 00000000..a9429775 --- /dev/null +++ b/docs/src/info/changelog.txt @@ -0,0 +1,28 @@ +SPRING HADOOP CHANGELOG +======================= +http://www.springsource.org/spring-data/hadoop + +Commit changelog: http://github.com/SpringSource/spring-hadoop/tree/[version] +Issues changelog: http://jira.springsource.org/secure/ReleaseNote.jspa?projectId=10801 + + +Changes in version 0.9 RELEASE (2012-02-06) +------------------------------------------- + +Spring XML namespace with support for creating and/or configuring + - Hadoop Configuration object + - MapReduce and Streaming Jobs + - HBase configuration + - Hive server and Thrift client + - Pig server instances that register and execute scripts either locally or remotely + - Hadoop DistributedCache +Spring XML namespace for executing scripts authored in JSR233 compatible scripting languages +Support for executing HDFS operations in Groovy, JRuby, Jython or Rhino based on Hadoop Configuration +Embedded shell API for HDFS +Spring Batch Integration - tasklets for + - Map Reduce and Streaming jobs + - Hive + - Pig + - Script execution +Sample applications +Reference documentation diff --git a/docs/src/info/license.txt b/docs/src/info/license.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/docs/src/info/license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/docs/src/info/notice.txt b/docs/src/info/notice.txt new file mode 100644 index 00000000..c2bb1319 --- /dev/null +++ b/docs/src/info/notice.txt @@ -0,0 +1,22 @@ + ====================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, for the Spring Framework distribution. == + ====================================================================== + + This product includes software developed by + the Apache Software Foundation (http://www.apache.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)." + + Alternately, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Spring", "Spring Framework", and "Spring GemFire" must + not be used to endorse or promote products derived from this + software without prior written permission. For written permission, + please contact enquiries@springsource.com. + diff --git a/docs/src/info/readme.txt b/docs/src/info/readme.txt new file mode 100644 index 00000000..87ccf553 --- /dev/null +++ b/docs/src/info/readme.txt @@ -0,0 +1,27 @@ +SPRING DATA HADOOP +------------------ +http://www.springsource.org/spring-data/hadoop + +1. INTRODUCTION + +Spring Hadoop is a framework extension for writing Hadoop jobs that +benefit from the features of Spring, Spring Batch and Spring Integration. + +2. RELEASE NOTES + +This release comes with complete reference documentation. For further +details, consult the provided javadoc for specific packages and classes. + +3. DISTRIBUTION JAR FILES + +The Spring Hadoop jars files can be found in the 'dist' directory. + +4. GETTING STARTED + +Please see the reference documentation. +Additionally the blog at http://blog.springsource.com as well +as sections of interest in the reference documentation. + +ADDITIONAL RESOURCES +Spring Hadoop Homepage: http://www.springsource.org/spring-data/hadoop +Hadoop Homepage: http://hadoop.apache.org \ No newline at end of file diff --git a/docs/src/reference/resources/css/highlight.css b/docs/src/reference/resources/css/highlight.css new file mode 100644 index 00000000..ffefef72 --- /dev/null +++ b/docs/src/reference/resources/css/highlight.css @@ -0,0 +1,35 @@ +/* + code highlight CSS resemblign the Eclipse IDE default color schema + @author Costin Leau +*/ + +.hl-keyword { + color: #7F0055; + font-weight: bold; +} + +.hl-comment { + color: #3F5F5F; + font-style: italic; +} + +.hl-multiline-comment { + color: #3F5FBF; + font-style: italic; +} + +.hl-tag { + color: #3F7F7F; +} + +.hl-attribute { + color: #7F007F; +} + +.hl-value { + color: #2A00FF; +} + +.hl-string { + color: #2A00FF; +} \ No newline at end of file diff --git a/docs/src/reference/resources/css/manual.css b/docs/src/reference/resources/css/manual.css new file mode 100644 index 00000000..77569070 --- /dev/null +++ b/docs/src/reference/resources/css/manual.css @@ -0,0 +1,99 @@ +@IMPORT url("highlight.css"); + +html { + padding: 0pt; + margin: 0pt; +} + +body { + margin-left: 10%; + margin-right: 10%; + font-family: Arial, Sans-serif; +} + +div { + margin: 0pt; +} + +p { + text-align: justify; +} + +hr { + border: 1px solid gray; + background: gray; +} + +h1,h2,h3,h4 { + color: #234623; + font-family: Arial, Sans-serif; +} + +pre { + line-height: 1.0; + color: black; +} + +pre.programlisting { + font-size: 10pt; + padding: 7pt 3pt; + border: 1pt solid black; + background: #eeeeee; + clear: both; +} + +div.table { + margin: 1em; + padding: 0.5em; + text-align: center; +} + +div.table table { + display: table; + width: 100%; +} + +div.table td { + padding-left: 7px; + padding-right: 7px; +} + +.sidebar { + float: right; + margin: 10px 0 10px 30px; + padding: 10px 20px 20px 20px; + width: 33%; + border: 1px solid black; + background-color: #F4F4F4; + font-size: 14px; +} + +.mediaobject { + padding-top: 30px; + padding-bottom: 30px; +} + +.legalnotice { + font-family: Verdana, Arial, helvetica, sans-serif; + font-size: 12px; + font-style: italic; +} + +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; +} + +span.productname { + font-size: 200%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; +} diff --git a/docs/src/reference/resources/images/admon/blank.png b/docs/src/reference/resources/images/admon/blank.png new file mode 100644 index 00000000..764bf4f0 Binary files /dev/null and b/docs/src/reference/resources/images/admon/blank.png differ diff --git a/docs/src/reference/resources/images/admon/caution.gif b/docs/src/reference/resources/images/admon/caution.gif new file mode 100644 index 00000000..d9f5e5b1 Binary files /dev/null and b/docs/src/reference/resources/images/admon/caution.gif differ diff --git a/docs/src/reference/resources/images/admon/caution.png b/docs/src/reference/resources/images/admon/caution.png new file mode 100644 index 00000000..5b7809ca Binary files /dev/null and b/docs/src/reference/resources/images/admon/caution.png differ diff --git a/docs/src/reference/resources/images/admon/caution.tif b/docs/src/reference/resources/images/admon/caution.tif new file mode 100644 index 00000000..4a282948 Binary files /dev/null and b/docs/src/reference/resources/images/admon/caution.tif differ diff --git a/docs/src/reference/resources/images/admon/draft.png b/docs/src/reference/resources/images/admon/draft.png new file mode 100644 index 00000000..0084708c Binary files /dev/null and b/docs/src/reference/resources/images/admon/draft.png differ diff --git a/docs/src/reference/resources/images/admon/home.gif b/docs/src/reference/resources/images/admon/home.gif new file mode 100644 index 00000000..6784f5bb Binary files /dev/null and b/docs/src/reference/resources/images/admon/home.gif differ diff --git a/docs/src/reference/resources/images/admon/home.png b/docs/src/reference/resources/images/admon/home.png new file mode 100644 index 00000000..cbb711de Binary files /dev/null and b/docs/src/reference/resources/images/admon/home.png differ diff --git a/docs/src/reference/resources/images/admon/important.gif b/docs/src/reference/resources/images/admon/important.gif new file mode 100644 index 00000000..6795d9a8 Binary files /dev/null and b/docs/src/reference/resources/images/admon/important.gif differ diff --git a/docs/src/reference/resources/images/admon/important.png b/docs/src/reference/resources/images/admon/important.png new file mode 100644 index 00000000..ad57f6f7 Binary files /dev/null and b/docs/src/reference/resources/images/admon/important.png differ diff --git a/docs/src/reference/resources/images/admon/important.tif b/docs/src/reference/resources/images/admon/important.tif new file mode 100644 index 00000000..184de637 Binary files /dev/null and b/docs/src/reference/resources/images/admon/important.tif differ diff --git a/docs/src/reference/resources/images/admon/next.gif b/docs/src/reference/resources/images/admon/next.gif new file mode 100644 index 00000000..aa1516e6 Binary files /dev/null and b/docs/src/reference/resources/images/admon/next.gif differ diff --git a/docs/src/reference/resources/images/admon/next.png b/docs/src/reference/resources/images/admon/next.png new file mode 100644 index 00000000..45835bf8 Binary files /dev/null and b/docs/src/reference/resources/images/admon/next.png differ diff --git a/docs/src/reference/resources/images/admon/note.gif b/docs/src/reference/resources/images/admon/note.gif new file mode 100644 index 00000000..f329d359 Binary files /dev/null and b/docs/src/reference/resources/images/admon/note.gif differ diff --git a/docs/src/reference/resources/images/admon/note.png b/docs/src/reference/resources/images/admon/note.png new file mode 100644 index 00000000..ad57f6f7 Binary files /dev/null and b/docs/src/reference/resources/images/admon/note.png differ diff --git a/docs/src/reference/resources/images/admon/note.tif b/docs/src/reference/resources/images/admon/note.tif new file mode 100644 index 00000000..08644d6b Binary files /dev/null and b/docs/src/reference/resources/images/admon/note.tif differ diff --git a/docs/src/reference/resources/images/admon/prev.gif b/docs/src/reference/resources/images/admon/prev.gif new file mode 100644 index 00000000..64ca8f3c Binary files /dev/null and b/docs/src/reference/resources/images/admon/prev.gif differ diff --git a/docs/src/reference/resources/images/admon/prev.png b/docs/src/reference/resources/images/admon/prev.png new file mode 100644 index 00000000..cf24654f Binary files /dev/null and b/docs/src/reference/resources/images/admon/prev.png differ diff --git a/docs/src/reference/resources/images/admon/tip.gif b/docs/src/reference/resources/images/admon/tip.gif new file mode 100644 index 00000000..823f2b41 Binary files /dev/null and b/docs/src/reference/resources/images/admon/tip.gif differ diff --git a/docs/src/reference/resources/images/admon/tip.png b/docs/src/reference/resources/images/admon/tip.png new file mode 100644 index 00000000..ad57f6f7 Binary files /dev/null and b/docs/src/reference/resources/images/admon/tip.png differ diff --git a/docs/src/reference/resources/images/admon/tip.tif b/docs/src/reference/resources/images/admon/tip.tif new file mode 100644 index 00000000..4a3d8c75 Binary files /dev/null and b/docs/src/reference/resources/images/admon/tip.tif differ diff --git a/docs/src/reference/resources/images/admon/toc-blank.png b/docs/src/reference/resources/images/admon/toc-blank.png new file mode 100644 index 00000000..6ffad17a Binary files /dev/null and b/docs/src/reference/resources/images/admon/toc-blank.png differ diff --git a/docs/src/reference/resources/images/admon/toc-minus.png b/docs/src/reference/resources/images/admon/toc-minus.png new file mode 100644 index 00000000..abbb020c Binary files /dev/null and b/docs/src/reference/resources/images/admon/toc-minus.png differ diff --git a/docs/src/reference/resources/images/admon/toc-plus.png b/docs/src/reference/resources/images/admon/toc-plus.png new file mode 100644 index 00000000..941312ce Binary files /dev/null and b/docs/src/reference/resources/images/admon/toc-plus.png differ diff --git a/docs/src/reference/resources/images/admon/up.gif b/docs/src/reference/resources/images/admon/up.gif new file mode 100644 index 00000000..aabc2d01 Binary files /dev/null and b/docs/src/reference/resources/images/admon/up.gif differ diff --git a/docs/src/reference/resources/images/admon/up.png b/docs/src/reference/resources/images/admon/up.png new file mode 100644 index 00000000..07634de2 Binary files /dev/null and b/docs/src/reference/resources/images/admon/up.png differ diff --git a/docs/src/reference/resources/images/admon/warning.gif b/docs/src/reference/resources/images/admon/warning.gif new file mode 100644 index 00000000..c6acdec6 Binary files /dev/null and b/docs/src/reference/resources/images/admon/warning.gif differ diff --git a/docs/src/reference/resources/images/admon/warning.png b/docs/src/reference/resources/images/admon/warning.png new file mode 100644 index 00000000..ef3e10f4 Binary files /dev/null and b/docs/src/reference/resources/images/admon/warning.png differ diff --git a/docs/src/reference/resources/images/admon/warning.tif b/docs/src/reference/resources/images/admon/warning.tif new file mode 100644 index 00000000..7b6611ec Binary files /dev/null and b/docs/src/reference/resources/images/admon/warning.tif differ diff --git a/docs/src/reference/resources/images/batch-wordcount-ide.jpg b/docs/src/reference/resources/images/batch-wordcount-ide.jpg new file mode 100644 index 00000000..bc35eda3 Binary files /dev/null and b/docs/src/reference/resources/images/batch-wordcount-ide.jpg differ diff --git a/docs/src/reference/resources/images/callouts/1.png b/docs/src/reference/resources/images/callouts/1.png new file mode 100644 index 00000000..7d473430 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/1.png differ diff --git a/docs/src/reference/resources/images/callouts/10.png b/docs/src/reference/resources/images/callouts/10.png new file mode 100644 index 00000000..997bbc82 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/10.png differ diff --git a/docs/src/reference/resources/images/callouts/11.png b/docs/src/reference/resources/images/callouts/11.png new file mode 100644 index 00000000..ce47dac3 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/11.png differ diff --git a/docs/src/reference/resources/images/callouts/12.png b/docs/src/reference/resources/images/callouts/12.png new file mode 100644 index 00000000..31daf4e2 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/12.png differ diff --git a/docs/src/reference/resources/images/callouts/13.png b/docs/src/reference/resources/images/callouts/13.png new file mode 100644 index 00000000..14021a89 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/13.png differ diff --git a/docs/src/reference/resources/images/callouts/14.png b/docs/src/reference/resources/images/callouts/14.png new file mode 100644 index 00000000..64014b75 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/14.png differ diff --git a/docs/src/reference/resources/images/callouts/15.png b/docs/src/reference/resources/images/callouts/15.png new file mode 100644 index 00000000..0d65765f Binary files /dev/null and b/docs/src/reference/resources/images/callouts/15.png differ diff --git a/docs/src/reference/resources/images/callouts/2.png b/docs/src/reference/resources/images/callouts/2.png new file mode 100644 index 00000000..5d09341b Binary files /dev/null and b/docs/src/reference/resources/images/callouts/2.png differ diff --git a/docs/src/reference/resources/images/callouts/3.png b/docs/src/reference/resources/images/callouts/3.png new file mode 100644 index 00000000..ef7b7004 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/3.png differ diff --git a/docs/src/reference/resources/images/callouts/4.png b/docs/src/reference/resources/images/callouts/4.png new file mode 100644 index 00000000..adb8364e Binary files /dev/null and b/docs/src/reference/resources/images/callouts/4.png differ diff --git a/docs/src/reference/resources/images/callouts/5.png b/docs/src/reference/resources/images/callouts/5.png new file mode 100644 index 00000000..4d7eb460 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/5.png differ diff --git a/docs/src/reference/resources/images/callouts/6.png b/docs/src/reference/resources/images/callouts/6.png new file mode 100644 index 00000000..0ba694af Binary files /dev/null and b/docs/src/reference/resources/images/callouts/6.png differ diff --git a/docs/src/reference/resources/images/callouts/7.png b/docs/src/reference/resources/images/callouts/7.png new file mode 100644 index 00000000..472e96f8 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/7.png differ diff --git a/docs/src/reference/resources/images/callouts/8.png b/docs/src/reference/resources/images/callouts/8.png new file mode 100644 index 00000000..5e60973c Binary files /dev/null and b/docs/src/reference/resources/images/callouts/8.png differ diff --git a/docs/src/reference/resources/images/callouts/9.png b/docs/src/reference/resources/images/callouts/9.png new file mode 100644 index 00000000..a0676d26 Binary files /dev/null and b/docs/src/reference/resources/images/callouts/9.png differ diff --git a/docs/src/reference/resources/images/logo.png b/docs/src/reference/resources/images/logo.png new file mode 100644 index 00000000..a9f6d959 Binary files /dev/null and b/docs/src/reference/resources/images/logo.png differ diff --git a/docs/src/reference/resources/images/xdev-spring_logo.jpg b/docs/src/reference/resources/images/xdev-spring_logo.jpg new file mode 100644 index 00000000..622962ee Binary files /dev/null and b/docs/src/reference/resources/images/xdev-spring_logo.jpg differ diff --git a/docs/src/reference/resources/xsl/fopdf.xsl b/docs/src/reference/resources/xsl/fopdf.xsl new file mode 100644 index 00000000..8582406d --- /dev/null +++ b/docs/src/reference/resources/xsl/fopdf.xsl @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + + + + + ( + + ) + + + + Copyright © 2010-2011 + + + + + + + + + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + + + + + + Spring Data Redis () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + 1 + 1 + 0 + + + + + + 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' + src/docbkx/resources/images/admons/ + + + + + + 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/docs/src/reference/resources/xsl/highlight-fo.xsl b/docs/src/reference/resources/xsl/highlight-fo.xsl new file mode 100644 index 00000000..f0b5dd94 --- /dev/null +++ b/docs/src/reference/resources/xsl/highlight-fo.xsl @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/highlight.xsl b/docs/src/reference/resources/xsl/highlight.xsl new file mode 100644 index 00000000..c63c4765 --- /dev/null +++ b/docs/src/reference/resources/xsl/highlight.xsl @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/html-custom.xsl b/docs/src/reference/resources/xsl/html-custom.xsl new file mode 100644 index 00000000..a6330843 --- /dev/null +++ b/docs/src/reference/resources/xsl/html-custom.xsl @@ -0,0 +1,145 @@ + + + + + + + + + + '5' + '1' + + + 1 + + + 1 + + + 0 + 0 + 1 + + + + images/admon/ + .png + + 120 + images/callouts/ + .png + + + css/manual.css + text/css + book toc,title + + text-align: left + + + + + + + + + + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Begin Google Analytics code + + + End Google Analytics code + + + + + Begin LoopFuse code + + + End LoopFuse code + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/html-single-custom.xsl b/docs/src/reference/resources/xsl/html-single-custom.xsl new file mode 100644 index 00000000..c37095bb --- /dev/null +++ b/docs/src/reference/resources/xsl/html-single-custom.xsl @@ -0,0 +1,142 @@ + + + + + + + + + + + 1 + + + 1 + + + 0 + 0 + 1 + + + + images/admon/ + .png + + 120 + images/callouts/ + .png + + + css/manual.css + text/css + book toc,title + + text-align: left + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Begin Google Analytics code + + +End Google Analytics code + + + + +Begin LoopFuse code + + +End LoopFuse code + + + \ No newline at end of file diff --git a/docs/src/reference/resources/xsl/html.xsl b/docs/src/reference/resources/xsl/html.xsl new file mode 100644 index 00000000..2b0f8d6e --- /dev/null +++ b/docs/src/reference/resources/xsl/html.xsl @@ -0,0 +1,107 @@ + + + + + + + + + + + 0 + 0 + 1 + + + + + + book toc + + + + 3 + + + + + 1 + + + + + + + 1 + + + 90 + + + + + 1 + images/admons/ + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + () + + + + +
+

Authors

+

+ +

+
+ + + + + + +
+ diff --git a/docs/src/reference/resources/xsl/html_chunk.xsl b/docs/src/reference/resources/xsl/html_chunk.xsl new file mode 100644 index 00000000..29b35d28 --- /dev/null +++ b/docs/src/reference/resources/xsl/html_chunk.xsl @@ -0,0 +1,221 @@ + + + + + + + + + + '5' + '1' + 0 + 0 + 1 + + + + book toc + qandaset toc + + + 3 + + + 1 + + + + + 1 + 90 + + + + + 1 + images/admons/ + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + () + + + + + +
+

Authors

+

+ +

+
+ + + + + + + + 1 + + + + + + + + + + + + + +
diff --git a/docs/src/reference/resources/xsl/pdf-custom.xsl b/docs/src/reference/resources/xsl/pdf-custom.xsl new file mode 100644 index 00000000..cf730cf9 --- /dev/null +++ b/docs/src/reference/resources/xsl/pdf-custom.xsl @@ -0,0 +1,522 @@ + + + + + + + + + + + '1' + images/admon/ + .png + + + + + 24pt + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + book toc,title + + + + + + + + + + + + + + + + + please define productname in your docbook file! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + 0 + 1 + + + + + 0 + 0 + 0 + + + + false + + + 11 + 8 + + + 1.4 + + + + 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 + + + 0.3em + 0.3em + 0.3em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + + + + + + + + + + + + + + pt + + + + + 1em + 1em + 1em + 0.1em + 0.1em + 0.1em + + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + + + + 0.5em + 0.5em + 0.5em + 0.1em + 0.1em + 0.1em + always + + + + + + normal + italic + + + pt + + false + 0.1em + 0.1em + 0.1em + + + + + + 0 + 1 + + + 90 + + + + + + figure after + example after + equation before + table before + procedure before + + + + 1 + + 0pt + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/reference/resources/xsl/pdf-custom.xsl.bak b/docs/src/reference/resources/xsl/pdf-custom.xsl.bak new file mode 100644 index 00000000..025522e2 --- /dev/null +++ b/docs/src/reference/resources/xsl/pdf-custom.xsl.bak @@ -0,0 +1,523 @@ + + + + + + + + + + '1' + images/admon/ + .png + + + + + 24pt + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + book toc,title + + + + + + + + + + + + + + + + + please define productname in your docbook file! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 1 + 1 + + + + + 0 + 0 + 0 + + + + false + + + 11 + 8 + + + 1.4 + + + + 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 + + + 0.3em + 0.3em + 0.3em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + + + + + + + + + + + + + + pt + + + + + 1em + 1em + 1em + 0.1em + 0.1em + 0.1em + + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + + + + 0.5em + 0.5em + 0.5em + 0.1em + 0.1em + 0.1em + always + + + + + + normal + italic + + + pt + + false + 0.1em + 0.1em + 0.1em + + + + + + 0 + 1 + + + 90 + + + + + + figure after + example after + equation before + table before + procedure before + + + + 1 + + 0pt + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..b8149d16 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +## Dependecies Version + +# Logging +log4jVersion = 1.2.14 +slf4jVersion = 1.6.4 + +# Common libraries +springVersion = 3.1.1.RELEASE +commonsioVersion = 1.4 +cglibVersion = 2.2.2 +jlineVersion=1.0.S2-B +jansiVersion=1.6 + + +# Testing +junitVersion = 4.7 +mockitoVersion = 1.8.5 + +# Manifest properties + +## OSGi ranges +spring.range = "[3.0.0, 4.0.0)" +hadoop.range = "[0.20.2, 2.0)" +hive.range = "[0.8, 1.0)" +hbase.range = "[0.90, 1.0)" +batch.range = "[2.1, 2.2)" +pig.range = "[0.9, 1.0.0)" +thrift.range = "[0.7, 0.8)" + +# -------------------- +# Project wide version +# -------------------- +springShellVersion=1.0.0.BUILD-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..45bfb5c8 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..7d06c7dc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Sep 19 20:44:55 EEST 2011 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://repo.gradle.org/gradle/distributions/gradle-1.0-milestone-3-bin.zip diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..d8809f15 --- /dev/null +++ b/gradlew @@ -0,0 +1,168 @@ +#!/bin/bash + +############################################################################## +## ## +## Gradle wrapper script for UN*X ## +## ## +############################################################################## + +# Uncomment those lines to set JVM options. GRADLE_OPTS and JAVA_OPTS can be used together. +# GRADLE_OPTS="$GRADLE_OPTS -Xmx512m" +# JAVA_OPTS="$JAVA_OPTS -Xmx512m" + +GRADLE_APP_NAME=Gradle + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set JAVA_HOME if it's not already set. +if [ -z "$JAVA_HOME" ] ; then + if $darwin ; then + [ -z "$JAVA_HOME" -a -d "/Library/Java/Home" ] && export JAVA_HOME="/Library/Java/Home" + [ -z "$JAVA_HOME" -a -d "/System/Library/Frameworks/JavaVM.framework/Home" ] && export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home" + else + javaExecutable="`which javac`" + [ -z "$javaExecutable" -o "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ] && die "JAVA_HOME not set and cannot find javac to deduce location, please set JAVA_HOME." + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + [ `expr "$readLink" : '\([^ ]*\)'` = "no" ] && die "JAVA_HOME not set and readlink not available, please set JAVA_HOME." + javaExecutable="`readlink -f \"$javaExecutable\"`" + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + export JAVA_HOME="$javaHome" + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVACMD" ] && JAVACMD=`cygpath --unix "$JAVACMD"` + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +STARTER_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain +CLASSPATH=`dirname "$0"`/gradle/wrapper/gradle-wrapper.jar +WRAPPER_PROPERTIES=`dirname "$0"`/gradle/wrapper/gradle-wrapper.properties +# Determine the Java command to use to start the JVM. +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="java" + fi +fi +if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +if [ -z "$JAVA_HOME" ] ; then + warn "JAVA_HOME environment variable is not set" +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add GRADLE_APP_NAME to the JAVA_OPTS as -Xdock:name +if $darwin; then + JAVA_OPTS="$JAVA_OPTS -Xdock:name=$GRADLE_APP_NAME" +# we may also want to set -Xdock:image +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + JAVA_HOME=`cygpath --path --mixed "$JAVA_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +GRADLE_APP_BASE_NAME=`basename "$0"` + +exec "$JAVACMD" $JAVA_OPTS $GRADLE_OPTS \ + -classpath "$CLASSPATH" \ + -Dorg.gradle.appname="$GRADLE_APP_BASE_NAME" \ + -Dorg.gradle.wrapper.properties="$WRAPPER_PROPERTIES" \ + $STARTER_MAIN_CLASS \ + "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..479fdddb --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem ## +@rem Gradle startup script for Windows ## +@rem ## +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Uncomment those lines to set JVM options. GRADLE_OPTS and JAVA_OPTS can be used together. +@rem set GRADLE_OPTS=%GRADLE_OPTS% -Xmx512m +@rem set JAVA_OPTS=%JAVA_OPTS% -Xmx512m + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=.\ + +@rem Find java.exe +set JAVA_EXE=java.exe +if not defined JAVA_HOME goto init + +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. +echo. +goto end + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set STARTER_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain +set CLASSPATH=%DIRNAME%\gradle\wrapper\gradle-wrapper.jar +set WRAPPER_PROPERTIES=%DIRNAME%\gradle\wrapper\gradle-wrapper.properties + +set GRADLE_OPTS=%JAVA_OPTS% %GRADLE_OPTS% -Dorg.gradle.wrapper.properties="%WRAPPER_PROPERTIES%" + +@rem Execute Gradle +"%JAVA_EXE%" %GRADLE_OPTS% -classpath "%CLASSPATH%" %STARTER_MAIN_CLASS% %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +if not "%OS%"=="Windows_NT" echo 1 > nul | choice /n /c:1 + +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit "%ERRORLEVEL%" +exit /b "%ERRORLEVEL%" + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff --git a/maven.gradle b/maven.gradle new file mode 100644 index 00000000..f9e8750c --- /dev/null +++ b/maven.gradle @@ -0,0 +1,176 @@ +apply plugin: 'maven' + +// Create a source jar for uploading +task sourceJar(type: Jar, dependsOn: jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +// Create a javadoc jar for uploading +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourceJar + archives javadocJar +} + +// Configuration for SpringSource s3 maven deployer +configurations { + deployerJars +} +dependencies { + deployerJars "org.springframework.build.aws:org.springframework.build.aws.maven:3.0.0.RELEASE" +} + +task generatePom { + group = 'Build' + description = 'Generates a Maven POM file suitable for use in building the project' + + generatedPomFileName = "pom.xml" + + // ensure changes in the classpath trigger regeneration of poms + inputs.files(project.sourceSets.main.compileClasspath) + + // ensure version changes in gradle.properties trigger regeneration of poms + inputs.files(new File(project.rootProject.rootDir, Project.GRADLE_PROPERTIES)) + + // enable partial cleaning with `gradle cleanGeneratePom` + outputs.files(generatedPomFileName) + + doLast() { + // customize the pom creation process + p = pom { + project { + name = project.description + properties { + setProperty('project.build.sourceEncoding', 'UTF8') + } + build { + plugins { + plugin { + groupId = 'org.apache.maven.plugins' + artifactId = 'maven-compiler-plugin' + configuration { + source = '1.6' + target = '1.6' + } + } + } + } + } + } + + // customizing the artifact id is a special case that must be configured + // after the pom is fully configured, otherwise it'll be overwritten + p.whenConfigured { pom -> pom.artifactId = project.name } + + customizePom(p) + + // write the pom.xml file out to the filesystem + p.writeTo(generatedPomFileName) + } +} + + +// Remove the archive configuration from the runtime configuration, so that anything added to archives +// (such as the source jar) is no longer included in the runtime classpath +configurations.default.extendsFrom = [configurations.runtime] as Set +// Add the main jar into the default configuration +artifacts { 'default' jar } + +gradle.taskGraph.whenReady {graph -> + if (graph.hasTask(uploadArchives)) { + // check properties defined and fail early + s3AccessKey + s3SecretAccessKey + } +} + +def deployer = null + +uploadArchives { + description = "Maven deploy of archives artifacts to SpringSource Maven repos" // url appended below + group = "Distribution" + // Maven deployment + def releaseRepositoryUrl = "file://${project.properties.mavenSyncRepoDir}" + def milestoneRepositoryUrl = 's3://maven.springframework.org/milestone' + def snapshotRepositoryUrl = 's3://maven.springframework.org/snapshot' + + // add a configuration with a classpath that includes our s3 maven deployer + configurations { deployerJars } + dependencies { + deployerJars "org.springframework.build.aws:org.springframework.build.aws.maven:3.0.0.RELEASE" + } + + deployer = repositories.mavenDeployer { + configuration = configurations.deployerJars + // releaseBuild + if (releaseBuild) { + logger.info("Deploying to local Maven repo " + releaseRepositoryUrl) + // "mavenSyncRepoDir" should be set in properties + repository(url: releaseRepositoryUrl) + } else { + s3credentials = [userName: project.properties.s3AccessKey, passphrase: project.properties.s3SecretAccessKey] + repository(url: milestoneRepositoryUrl) { + authentication(s3credentials) + } + snapshotRepository(url: snapshotRepositoryUrl) { + authentication(s3credentials) + } + } + } + + customizePom(deployer.pom) +} + +install { + customizePom(repositories.mavenInstaller.pom) +} + +def customizePom(pom) { + def optionalDeps = ['log4j','jsr250-api'] + + //pom.scopeMappings.addMapping(10, configurations.provided, 'provided') + pom.whenConfigured { p -> + // Remove test scope dependencies from published poms + //p.dependencies = p.dependencies.findAll {it.scope != 'test'} + + // Flag optional deps + def opDeps = configurations.testRuntime.allDependencies.findAll { gradleDep -> + gradleDep.asDynamicObject.hasProperty('optional') && gradleDep.optional + } + + p.dependencies.findAll { dep -> + optionalDeps.contains(dep.artifactId) || + dep.groupId.startsWith('org.slf4j') || + opDeps.any { op -> + (dep.groupId == op.group && dep.artifactId == op.name) + } + }*.optional = true + + p.groupId = "org.springframework.data" + } + + pom.project { + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + // similar to Spring's configuration + dependencies { + dependency { + artifactId = groupId = 'commons-logging' + scope = 'compile' + optional = 'true' + version = '1.1.1' + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 98629eb3..66db8cf9 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ org.springframework.shell.Bootstrap true - 3.0.6.RELEASE + 3.1.1.RELEASE junit @@ -37,24 +37,20 @@ spring-context ${spring.framework.version} + + + + cglib + cglib + 2.2.2 + log4j log4j 1.2.14 - - - org.springframework.roo - org.springframework.roo.shell - 1.2.0.RELEASE - - + commons-io commons-io @@ -71,12 +67,13 @@ org.fusesource.jansi jansi 1.6 - - - org.springframework.roo.wrapping - org.springframework.roo.wrapping.json-simple - 1.1.0.0010 + + org.mockito + mockito-all + 1.8.5 + test + @@ -128,7 +125,10 @@ - + + spring-roo-repository @@ -136,4 +136,5 @@ http://spring-roo-repository.springsource.org/release + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e3da39c3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'spring-shell' diff --git a/src/main/java/org/springframework/roo/shell/AbstractShell.java b/src/main/java/org/springframework/roo/shell/AbstractShell.java new file mode 100644 index 00000000..1172347e --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/AbstractShell.java @@ -0,0 +1,488 @@ +package org.springframework.roo.shell; + +import static org.springframework.roo.support.util.StringUtils.LINE_SEPARATOR; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.DateFormat; +import java.util.Collection; +import java.util.Date; +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.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.Assert; +import org.springframework.roo.support.util.IOUtils; +import org.springframework.roo.support.util.MathUtils; +import org.springframework.roo.support.util.StringUtils; + +/** + * Provides a base {@link Shell} implementation. + * + * @author Ben Alex + */ +public abstract class AbstractShell extends AbstractShellStatusPublisher implements Shell { + + // Constants + private static final String MY_SLOT = AbstractShell.class.getName(); + + //TODO Abstract out to make configurable. + protected static final String ROO_PROMPT = "spring> "; + + // 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; + + // Instance fields + protected final Logger logger = HandlerUtils.getLogger(getClass()); + protected boolean inBlockComment; + protected ExitShellRequest exitShellRequest; + + /** + * 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); + + protected abstract String getHomeAsString(); + + protected abstract ExecutionStrategy getExecutionStrategy(); + + protected abstract Parser getParser(); + + @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) { + + Assert.notNull(script, "Script file to parse is required"); + double startedNanoseconds = System.nanoTime(); + final InputStream inputStream = openScript(script); + + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader(inputStream)); + String line; + int i = 0; + while ((line = in.readLine()) != null) { + i++; + if (lineNumbers) { + logger.fine("Line " + i + ": " + line); + } else { + logger.fine(line); + } + if (!"".equals(line.trim())) { + 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 (IOException e) { + throw new IllegalStateException(e); + } finally { + IOUtils.closeQuietly(inputStream, in); + double executionDurationInSeconds = (System.nanoTime() - startedNanoseconds) / 1000000000D; + logger.fine("Script required " + MathUtils.round(executionDurationInSeconds, 3) + " seconds to execute"); + } + } + + /** + * 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 + Assert.notNull(urls, "Unexpected error looking for '" + script.getName() + "'"); + + // Handle the search being OK but the file simply not being present + Assert.notEmpty(urls, "Script '" + script + "' not found on disk or in classpath"); + Assert.isTrue(urls.size() == 1, "More than one '" + script + "' was found in the classpath; unable to continue"); + try { + return urls.iterator().next().openStream(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + /** + * 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); + } + + public boolean executeCommand(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 (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(); + 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; + } + + setShellStatus(Status.EXECUTING); + 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 (Object o : (Iterable) result) { + logger.info(o.toString()); + } + } else { + logger.info(result.toString()); + } + } + + logCommandIfRequired(line, true); + setShellStatus(Status.EXECUTION_SUCCESS, line, parseResult); + return true; + } catch (RuntimeException e) { + setShellStatus(Status.EXECUTION_FAILED, line, parseResult); + // We rely on execution strategy to log it + try { + logCommandIfRequired(line, false); + } catch (Exception ignored) {} + return false; + } finally { + setShellStatus(Status.USER_INPUT); + } + } + + /** + * 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) {} + + /** + * 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) { + if (path == null || "".equals(path)) { + shellPrompt = ROO_PROMPT; + } else { + shellPrompt = 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(String path, boolean overrideStyle) { + setPromptPath(path); + } + + public ExitShellRequest getExitShellRequest() { + return exitShellRequest; + } + + @CliCommand(value = { "//", ";" }, help = "Inline comment markers (start of line only)") + public void inlineComment() {} + + @CliCommand(value = { "/*" }, help = "Start of block comment") + public void blockCommentBegin() { + Assert.isTrue(!inBlockComment, "Cannot open a new block comment when one already active"); + inBlockComment = true; + } + + @CliCommand(value = { "*/" }, help = "End of block comment") + public void blockCommentFinish() { + Assert.isTrue(inBlockComment, "Cannot close a block comment when it has not been opened"); + inBlockComment = false; + } + + @CliCommand(value = { "system properties" }, help = "Shows the shell's properties") + public String props() { + final Set data = new TreeSet(); // For repeatability + for (final Entry entry : System.getProperties().entrySet()) { + data.add(entry.getKey() + " = " + entry.getValue()); + } + + return StringUtils.collectionToDelimitedString(data, LINE_SEPARATOR) + LINE_SEPARATOR; + } + + @CliCommand(value = { "date" }, help = "Displays the local date and time") + public String date() { + return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date()); + } + + @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"); + } + + @CliCommand(value = { "version" }, help = "Displays shell version") + public String version(@CliOption(key = "", help = "Special version flags") final String extra) { + StringBuilder sb = new StringBuilder(); + + if ("jaime".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(); + } + + public static String versionInfo() { + // Try to determine the bundle version + String bundleVersion = null; + String gitCommitHash = null; + JarFile jarFile = null; + try { + 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); + ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF"); + Manifest manifest = new Manifest(jarFile.getInputStream(manifestEntry)); + bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version"); + gitCommitHash = manifest.getMainAttributes().getValue("Git-Commit-Hash"); + } + } catch (IOException ignoreAndMoveOn) { + } catch (URISyntaxException ignoreAndMoveOn) { + } finally { + IOUtils.closeQuietly(jarFile); + } + + StringBuilder sb = new StringBuilder(); + + if (bundleVersion != null) { + sb.append(bundleVersion); + } + + if (gitCommitHash != null && gitCommitHash.length() > 7) { + if (sb.length() > 0) { + sb.append(" "); // to separate from version + } + sb.append("[rev "); + sb.append(gitCommitHash.substring(0,7)); + sb.append("]"); + } + + if (sb.length() == 0) { + sb.append("UNKNOWN VERSION"); + } + + return sb.toString(); + } + + public String getShellPrompt() { + return shellPrompt; + } + + /** + * 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() { + String rooHome = getHomeAsString(); + File f = new File(rooHome); + Assert.isTrue(!f.exists() || (f.exists() && f.isDirectory()), "Path '" + f.getAbsolutePath() + "' must be a directory, or it must not exist"); + if (!f.exists()) { + f.mkdirs(); + } + Assert.isTrue(f.exists() && f.isDirectory(), "Path '" + f.getAbsolutePath() + "' is not a directory; please specify roo.home system property correctly"); + return f; + } + + /** + * 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) { + Assert.notNull(level, "Level is required for a flash message"); + Assert.notNull(message, "Message is required for a flash message"); + Assert.hasText(slot, "Slot name must be specified for a flash message"); + if (!("".equals(message))) { + logger.log(level, message); + } + } +} diff --git a/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java b/src/main/java/org/springframework/roo/shell/CliAvailabilityIndicator.java new file mode 100644 index 00000000..207c2934 --- /dev/null +++ b/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/src/main/java/org/springframework/roo/shell/CliCommand.java b/src/main/java/org/springframework/roo/shell/CliCommand.java new file mode 100644 index 00000000..67c091b5 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/CliCommand.java @@ -0,0 +1,22 @@ +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 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(); + + /** + * @return a help message for this command (the default is a blank String, which means there is no help) + */ + String help() default ""; +} diff --git a/src/main/java/org/springframework/roo/shell/CliOption.java b/src/main/java/org/springframework/roo/shell/CliOption.java new file mode 100644 index 00000000..8b370ce9 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/CliOption.java @@ -0,0 +1,63 @@ +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 { + + /** + * @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 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; + + /** + * @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__"; + + /** + * @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__"; + + /** + * 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 a help message for this option (the default is a blank String, which means there is no help) + */ + String help() default ""; +} diff --git a/src/main/java/org/springframework/roo/shell/CliOptionContext.java b/src/main/java/org/springframework/roo/shell/CliOptionContext.java new file mode 100644 index 00000000..895eeb4a --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/CliOptionContext.java @@ -0,0 +1,40 @@ +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(); + } + + /** + * 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); + } + + /** + * Resets the option context for the current thread. + */ + public static void resetOptionContext() { + optionContextHolder.remove(); + } + + /** + * Constructor is private to prevent instantiation + */ + private CliOptionContext() {} +} diff --git a/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java b/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java new file mode 100644 index 00000000..c0496c47 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/CliSimpleParserContext.java @@ -0,0 +1,27 @@ +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 setSimpleParserContext(final SimpleParser simpleParserContext) { + simpleParserContextHolder.set(simpleParserContext); + } + + public static void resetSimpleParserContext() { + simpleParserContextHolder.remove(); + } + + /** + * Constructor is private to prevent instantiation + */ + private CliSimpleParserContext() {} +} diff --git a/src/main/java/org/springframework/roo/shell/CommandMarker.java b/src/main/java/org/springframework/roo/shell/CommandMarker.java new file mode 100644 index 00000000..81bb3adc --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/CommandMarker.java @@ -0,0 +1,6 @@ +package org.springframework.roo.shell; + +/** + * Marker interface indicating a provider of one or more shell commands. + */ +public interface CommandMarker {} diff --git a/src/main/java/org/springframework/roo/shell/Completion.java b/src/main/java/org/springframework/roo/shell/Completion.java new file mode 100644 index 00000000..eb1e3994 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/Completion.java @@ -0,0 +1,92 @@ +package org.springframework.roo.shell; + +import org.springframework.roo.support.util.AnsiEscapeCode; +import org.springframework.roo.support.util.StringUtils; + +public class Completion { + + // Fields + private final int order; + private final String formattedValue; + private final String heading; + 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.hasText(heading)) { + heading = AnsiEscapeCode.decorate(heading, AnsiEscapeCode.UNDERSCORE, AnsiEscapeCode.FG_GREEN); + } + this.heading = heading; + } + + public String getValue() { + return value; + } + + public String getFormattedValue() { + return formattedValue; + } + + public String getHeading() { + return heading; + } + + public int getOrder() { + return order; + } + + @Override + public String toString() { + return order + ". " + heading + " - " + value; + } + + @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; + } + + @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; + } +} + diff --git a/src/main/java/org/springframework/roo/shell/Converter.java b/src/main/java/org/springframework/roo/shell/Converter.java new file mode 100644 index 00000000..944deef9 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/Converter.java @@ -0,0 +1,58 @@ +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 { + + /** + * 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); + + /** + * 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); +} diff --git a/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java b/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java new file mode 100644 index 00000000..39957cdb --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/ExecutionStrategy.java @@ -0,0 +1,39 @@ +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/src/main/java/org/springframework/roo/shell/ExitShellRequest.java b/src/main/java/org/springframework/roo/shell/ExitShellRequest.java new file mode 100644 index 00000000..08746dc3 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/ExitShellRequest.java @@ -0,0 +1,29 @@ +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 { + + // Constants + public static final ExitShellRequest NORMAL_EXIT = new ExitShellRequest(0); + 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 + + // Fields + private final int exitCode; + + private ExitShellRequest(final int exitCode) { + this.exitCode = exitCode; + } + + public int getExitCode() { + return exitCode; + } +} diff --git a/src/main/java/org/springframework/roo/shell/MethodTarget.java b/src/main/java/org/springframework/roo/shell/MethodTarget.java new file mode 100644 index 00000000..959e317b --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/MethodTarget.java @@ -0,0 +1,110 @@ +package org.springframework.roo.shell; + +import java.lang.reflect.Method; + +import org.springframework.roo.support.style.ToStringCreator; +import org.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.ObjectUtils; +import org.springframework.roo.support.util.StringUtils; + +/** + * A method that can be executed via a shell command. + *

+ * Immutable since 1.2.0. + * + * @author Ben Alex + */ +public class MethodTarget { + + // Fields + private final Method method; + private final Object target; + private final String remainingBuffer; + private final String key; + + /** + * 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) { + Assert.notNull(method, "Method is required"); + Assert.notNull(target, "Target is required"); + this.key = StringUtils.trimToEmpty(key); + this.method = method; + this.remainingBuffer = StringUtils.trimToEmpty(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 this.method.equals(otherMethodTarget.getMethod()) && this.target.equals(otherMethodTarget.getTarget()); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(method, target); + } + + @Override + public final String toString() { + final ToStringCreator tsc = new ToStringCreator(this); + tsc.append("target", target); + tsc.append("method", method); + tsc.append("remainingBuffer", remainingBuffer); + tsc.append("key", key); + return tsc.toString(); + } + + /** + * @since 1.2.0 + */ + public String getKey() { + return this.key; + } + + /** + * @return a non-null method + * @since 1.2.0 + */ + public Method getMethod() { + return this.method; + } + + /** + * @since 1.2.0 + */ + public String getRemainingBuffer() { + return this.remainingBuffer; + } + + /** + * @return a non-null Object + * @since 1.2.0 + */ + public Object getTarget() { + return this.target; + } +} diff --git a/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java b/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java new file mode 100644 index 00000000..eb4900a8 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/NaturalOrderComparator.java @@ -0,0 +1,178 @@ +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; + } + } + + 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++) { + char ca = charAt(a, ia); + 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(); + } + + 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; + } + + String a = stringify(o1); + 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; + } + } +} diff --git a/src/main/java/org/springframework/roo/shell/ParseResult.java b/src/main/java/org/springframework/roo/shell/ParseResult.java new file mode 100644 index 00000000..7492780b --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/ParseResult.java @@ -0,0 +1,92 @@ +package org.springframework.roo.shell; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.springframework.roo.support.style.ToStringCreator; +import org.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.StringUtils; + +/** + * 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 { + + // Fields + private final Method method; + private final Object instance; + private final Object[] arguments; // May be null if no arguments needed + + public ParseResult(final Method method, final Object instance, final Object[] arguments) { + Assert.notNull(method, "Method required"); + Assert.notNull(instance, "Instance required"); + int length = arguments == null ? 0 : arguments.length; + Assert.isTrue(method.getParameterTypes().length == length, "Required " + method.getParameterTypes().length + " arguments, but received " + length); + this.method = method; + this.instance = instance; + this.arguments = arguments; + } + + public Method getMethod() { + return method; + } + + public Object getInstance() { + return instance; + } + + public Object[] getArguments() { + return arguments; + } + + @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 boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + 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; + } + + @Override + public String toString() { + ToStringCreator tsc = new ToStringCreator(this); + tsc.append("method", method); + tsc.append("instance", instance); + tsc.append("arguments", StringUtils.arrayToCommaDelimitedString(arguments)); + return tsc.toString(); + } +} diff --git a/src/main/java/org/springframework/roo/shell/Parser.java b/src/main/java/org/springframework/roo/shell/Parser.java new file mode 100644 index 00000000..d22a7e0d --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/Parser.java @@ -0,0 +1,35 @@ +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 { + + ParseResult parse(String buffer); + + /** + * 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); +} \ No newline at end of file diff --git a/src/main/java/org/springframework/roo/shell/ParserUtils.java b/src/main/java/org/springframework/roo/shell/ParserUtils.java new file mode 100644 index 00000000..fbf483a3 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/ParserUtils.java @@ -0,0 +1,181 @@ +package org.springframework.roo.shell; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.roo.support.util.Assert; + +/** + * Utilities for parsing. + * + * @author Ben Alex + * @since 1.0 + */ +public class ParserUtils { + + private ParserUtils() {} + + /** + * 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) { + Assert.notNull(remainingBuffer, "Remaining buffer cannot be null, although it can be empty"); + 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 (char c : remainingBuffer.toCharArray()) { + if ('"' == c) { + count++; + } + } + Assert.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; + } + + String[] split = remainingBuffer.split(" "); + for (int i = 0; i < split.length; i++) { + String currentToken = split[i]; + + if (currentToken.startsWith("\"") && currentToken.endsWith("\"") && currentToken.length() > 1) { + String tokenLessDelimiters = currentToken.substring(1, currentToken.length() - 1); + currentValue.append(tokenLessDelimiters); + + // 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("\"")) { + 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 + 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 + int lastIndex = currentToken.lastIndexOf("-"); + 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 static void store(final Map results, final StringBuilder currentOption, final StringBuilder currentValue) { + if (currentOption.length() > 0) { + // There is an option marker + String option = currentOption.toString(); + Assert.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 + Assert.isTrue(!results.containsKey(""), "You cannot add more than one default option ('" + currentValue.toString() + "') in a single command"); + results.put("", currentValue.toString()); + } + } +} diff --git a/src/main/java/org/springframework/roo/shell/Shell.java b/src/main/java/org/springframework/roo/shell/Shell.java new file mode 100644 index 00000000..dd3c6e04 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/Shell.java @@ -0,0 +1,98 @@ +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"; + + /** + * 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(); + + /** + * @return null if no exit was requested, otherwise the last exit code indicated to the shell to use + */ + ExitShellRequest getExitShellRequest(); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + boolean isDevelopmentMode(); + + /** + * 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); + + /** + * Returns the home directory of the current running shell instance + * + * @return the home directory of the current shell instance + */ + File getHome(); +} \ No newline at end of file diff --git a/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java b/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java new file mode 100644 index 00000000..fc7f597b --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/ShellPromptAccessor.java @@ -0,0 +1,16 @@ +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/src/main/java/org/springframework/roo/shell/SimpleParser.java b/src/main/java/org/springframework/roo/shell/SimpleParser.java new file mode 100644 index 00000000..84015c17 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/SimpleParser.java @@ -0,0 +1,1077 @@ +package org.springframework.roo.shell; + +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.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.springframework.roo.support.logging.HandlerUtils; +import org.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.CollectionUtils; +import org.springframework.roo.support.util.ExceptionUtils; +import org.springframework.roo.support.util.FileCopyUtils; +import org.springframework.roo.support.util.StringUtils; +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 Parser}. + * + * @author Ben Alex + * @since 1.0 + */ +public class SimpleParser implements Parser { + + // Constants + private static final Logger LOGGER = HandlerUtils.getLogger(SimpleParser.class); + private static final Comparator COMPARATOR = new NaturalOrderComparator(); + + // Fields + private final Object mutex = new Object(); + private final Set> converters = new HashSet>(); + private final Set commands = new HashSet(); + private final Map availabilityIndicators = new HashMap(); + + private MethodTarget getAvailabilityIndicator(final String command) { + return availabilityIndicators.get(command); + } + + public ParseResult parse(final String rawInput) { + synchronized (mutex) { + Assert.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; + } + MethodTarget methodTarget = matchingTargets.iterator().next(); + + // Argument conversion time + 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 (IllegalArgumentException e) { + LOGGER.warning(ExceptionUtils.extractRootCause(e).getMessage()); + return null; + } + + final Set cliOptions = getCliOptions(parameterAnnotations); + for (CliOption cliOption : cliOptions) { + 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 (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])) { + 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.arrayToCommaDelimitedString(cliOption.key()) + "'"); + return null; + } + arguments.add(null); + continue; + } + + // Now we're ready to perform a conversion + try { + CliOptionContext.setOptionContext(cliOption.optionContext()); + CliSimpleParserContext.setSimpleParserContext(this); + Object result; + Converter c = null; + for (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 (RuntimeException e) { + LOGGER.warning(e.getClass().getName() + ": Failed to convert '" + value + "' to type " + requiredType.getSimpleName() + " for option '" + StringUtils.arrayToCommaDelimitedString(cliOption.key()) + "'"); + if (StringUtils.hasText(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 + Set unavailableOptions = getSpecifiedUnavailableOptions(cliOptions, options); + if (!unavailableOptions.isEmpty()) { + 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(StringUtils.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()); + } + } + + /** + * 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(); + } + + private Set getSpecifiedUnavailableOptions(final Set cliOptions, final Map options) { + Set cliOptionKeySet = new LinkedHashSet(); + for (CliOption cliOption : cliOptions) { + for (String key : cliOption.key()) { + cliOptionKeySet.add(key.toLowerCase()); + } + } + Set unavailableOptions = new LinkedHashSet(); + for (String suppliedOption : options.keySet()) { + if (!cliOptionKeySet.contains(suppliedOption.toLowerCase())) { + unavailableOptions.add(suppliedOption); + } + } + return unavailableOptions; + } + + private Set getCliOptions(final Annotation[][] parameterAnnotations) { + Set cliOptions = new LinkedHashSet(); + for (Annotation[] annotations : parameterAnnotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof CliOption) { + CliOption cliOption = (CliOption) annotation; + cliOptions.add(cliOption); + } + } + } + return cliOptions; + } + + 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)"); + } + + private Collection locateTargets(final String buffer, final boolean strictMatching, final boolean checkAvailabilityIndicators) { + Assert.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()) { + 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 (String value : cmd.value()) { + MethodTarget mt = getAvailabilityIndicator(value); + if (mt != null) { + Assert.isNull(available, "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 (Exception e) { + available = false; + } + } + } + // Skip this @CliCommand if it's not available + if (available != null && !available) { + continue; + } + } + + for (String value : cmd.value()) { + String remainingBuffer = isMatch(buffer, value, strictMatching); + if (remainingBuffer != null) { + result.add(new MethodTarget(method, command, remainingBuffer, value)); + } + } + } + } + } + return result; + } + + static String isMatch(final String buffer, final String command, final boolean strictMatching) { + if ("".equals(buffer.trim())) { + return ""; + } + String[] commandWords = StringUtils.delimitedListToStringArray(command, " "); + int lastCommandWordUsed = 0; + Assert.notEmpty(commandWords, "Command required"); + + String bufferToReturn = null; + String lastWord = null; + + next_buffer_loop: for (int bufferIndex = 0; bufferIndex < buffer.length(); bufferIndex++) { + String bufferSoFarIncludingThis = buffer.substring(0, bufferIndex + 1); + String bufferRemaining = buffer.substring(bufferIndex + 1); + + 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 + } + + public int complete(String buffer, int cursor, final List candidates) { + final List completions = new ArrayList(); + 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) { + Assert.notNull(buffer, "Buffer required"); + Assert.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.replaceFirst(buffer, " ", " "); + cursor--; + } + + // Begin by only including the portion of the buffer represented to the present cursor position + String translated = buffer.substring(0, cursor); + + // Start by locating a method that matches + final Collection targets = locateTargets(translated, false, true); + 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 (MethodTarget target : targets) { + // Calculate the correct starting position + 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 + MethodTarget methodTarget = targets.iterator().next(); + + // Identify the command we're working with + CliCommand cmd = methodTarget.getMethod().getAnnotation(CliCommand.class); + Assert.notNull(cmd, "CliCommand unavailable for '" + methodTarget.getMethod().toGenericString() + "'"); + + // Make a reasonable attempt at parsing the remainingBuffer + Map options; + try { + options = ParserUtils.tokenize(methodTarget.getRemainingBuffer()); + } catch (IllegalArgumentException ex) { + // Assume any IllegalArgumentException is due to a quotation mark mismatch + candidates.add(new Completion(translated + "\"")); + return 0; + } + + // Lookup arguments for this target + 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 (String value : cmd.value()) { + if (buffer.startsWith(value) || value.startsWith(buffer)) { + results.add(new Completion(value)); // no space at the end, as there's no need to continue the command further + } + } + 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 (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 + List cliOptions = new ArrayList(); + for (Annotation[] annotations : parameterAnnotations) { + CliOption cliOption = null; + for (Annotation a : annotations) { + if (a instanceof CliOption) { + cliOption = (CliOption) a; + } + } + Assert.notNull(cliOption, "CliOption not found for parameter '" + Arrays.toString(annotations) + "'"); + cliOptions.add(cliOption); + } + + // Make a list of all CliOptions they've already included or are system-provided + List alreadySpecified = new ArrayList(); + for (CliOption option : cliOptions) { + for (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 + 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 (CliOption include : unspecified) { + if (include.mandatory()) { + showAllRemaining = false; + break; + } + } + + for (CliOption include : unspecified) { + for (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 (CliOption include : unspecified) { + for (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 (Converter candidate : converters) { + // Find the target parameter + Class paramType = null; + int index = -1; + for (Annotation[] a : methodTarget.getMethod().getParameterAnnotations()) { + index++; + for (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 (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 (CliOption option : cliOptions) { + for (String value : option.key()) { + if (value != null && lastOptionKey != null && value.regionMatches(true, 0, lastOptionKey, 0, lastOptionKey.length())) { + 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 + Class[] parameterTypes = methodTarget.getMethod().getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + CliOption option = cliOptions.get(i); + Class parameterType = parameterTypes[i]; + + for (String key : option.key()) { + if (key.equals(lastOptionKey)) { + List allValues = new ArrayList(); + String suffix = " "; + + // Let's use a Converter if one is available + for (Converter candidate : converters) { + if (candidate.supports(parameterType, option.optionContext())) { + // Found a usable converter + 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 (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 + StringBuilder help = new StringBuilder(); + help.append(StringUtils.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) { + 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; + } + } + + public void helpReferenceGuide() { + synchronized (mutex) { + File f = new File("."); + File[] existing = f.listFiles(new FileFilter() { + public boolean accept(final File pathname) { + return pathname.getName().startsWith("appendix_"); + } + }); + for (File e : existing) { + e.delete(); + } + + // Compute the sections we'll be outputting, and get them into a nice order + SortedMap sections = new TreeMap(COMPARATOR); + next_target: for (Object target : commands) { + Method[] methods = target.getClass().getMethods(); + for (Method m : methods) { + CliCommand cmd = m.getAnnotation(CliCommand.class); + if (cmd != null) { + String sectionName = target.getClass().getSimpleName(); + Pattern p = Pattern.compile("[A-Z][^A-Z]*"); + Matcher matcher = p.matcher(sectionName); + 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 + DocumentBuilder builder = XmlUtils.getDocumentBuilder(); + Document document = builder.newDocument(); + List builtSections = new ArrayList(); + + for (final Entry entry : sections.entrySet()) { + final String section = entry.getKey(); + final Object target = entry.getValue(); + SortedMap individualCommands = new TreeMap(COMPARATOR); + + Method[] methods = target.getClass().getMethods(); + for (Method m : methods) { + CliCommand cmd = m.getAnnotation(CliCommand.class); + if (cmd != null) { + StringBuilder cmdSyntax = new StringBuilder(); + cmdSyntax.append(cmd.value()[0]); + + // Build the syntax list + + // Store the order options appear + List optionKeys = new ArrayList(); + // key: option key, value: help text + Map optionDetails = new HashMap(); + for (Annotation[] ann : m.getParameterAnnotations()) { + for (Annotation a : ann) { + if (a instanceof CliOption) { + 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 (String otherKey : option.key()) { + if (!"".equals(otherKey)) { + key = otherKey; + break; + } + } + if ("".equals(key)) { + key = "[default]"; + } + } + + 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 (String optionKey : optionKeys) { + anyVars = true; + 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 + CDATASection progList = document.createCDATASection(cmdSyntax.toString()); + String safeName = cmd.value()[0].replace("\\", "BCK").replace("/", "FWD").replace("*", "ASX"); + 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); + } + } + + 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); + } + + 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 (Element section : builtSections) { + appendix.appendChild(section); + } + document.appendChild(appendix); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + 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 { + File output = new File(f, "appendix-command-index.xml"); + FileCopyUtils.copy(byteArrayOutputStream.toByteArray(), output); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + } + + public void obtainHelp(@CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for") String buffer) { + synchronized (mutex) { + if (buffer == null) { + buffer = ""; + } + + 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 + MethodTarget methodTarget = matchingTargets.iterator().next(); + + // Argument conversion time + Annotation[][] parameterAnnotations = methodTarget.getMethod().getParameterAnnotations(); + if (parameterAnnotations.length > 0) { + // Offer specified help + CliCommand cmd = methodTarget.getMethod().getAnnotation(CliCommand.class); + Assert.notNull(cmd, "CliCommand not found"); + + for (String value : cmd.value()) { + sb.append("Keyword: ").append(value).append(StringUtils.LINE_SEPARATOR); + } + + sb.append("Description: ").append(cmd.help()).append(StringUtils.LINE_SEPARATOR); + + for (Annotation[] annotations : parameterAnnotations) { + CliOption cliOption = null; + for (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(StringUtils.LINE_SEPARATOR); + } + + sb.append(" Help: ").append(cliOption.help()).append(StringUtils.LINE_SEPARATOR); + sb.append(" Mandatory: ").append(cliOption.mandatory()).append(StringUtils.LINE_SEPARATOR); + sb.append(" Default if specified: '").append(cliOption.specifiedDefaultValue()).append("'").append(StringUtils.LINE_SEPARATOR); + sb.append(" Default if unspecified: '").append(cliOption.unspecifiedDefaultValue()).append("'").append(StringUtils.LINE_SEPARATOR); + sb.append(StringUtils.LINE_SEPARATOR); + } + + } + Assert.notNull(cliOption, "CliOption not found for parameter '" + Arrays.toString(annotations) + "'"); + } + } + // Only a single argument, so default to the normal help operation + } + + SortedSet result = new TreeSet(COMPARATOR); + for (MethodTarget mt : matchingTargets) { + CliCommand cmd = mt.getMethod().getAnnotation(CliCommand.class); + if (cmd != null) { + for (String value : cmd.value()) { + if ("".equals(cmd.help())) { + result.add("* " + value); + } else { + result.add("* " + value + " - " + cmd.help()); + } + } + } + } + + for (String s : result) { + sb.append(s).append(StringUtils.LINE_SEPARATOR); + } + + LOGGER.info(sb.toString()); + LOGGER.warning("** Type 'hint' (without the quotes) and hit ENTER for step-by-step guidance **" + StringUtils.LINE_SEPARATOR); + } + } + + public Set getEveryCommand() { + synchronized (mutex) { + SortedSet result = new TreeSet(COMPARATOR); + for (Object o : commands) { + Method[] methods = o.getClass().getMethods(); + for (Method m : methods) { + CliCommand cmd = m.getAnnotation(CliCommand.class); + if (cmd != null) { + result.addAll(Arrays.asList(cmd.value())); + } + } + } + return result; + } + } + + public final void add(final CommandMarker command) { + synchronized (mutex) { + commands.add(command); + for (final Method method : command.getClass().getMethods()) { + CliAvailabilityIndicator availability = method.getAnnotation(CliAvailabilityIndicator.class); + if (availability != null) { + Assert.isTrue(method.getParameterTypes().length == 0, "CliAvailabilityIndicator is only legal for 0 parameter methods (" + method.toGenericString() + ")"); + Assert.isTrue(method.getReturnType().equals(Boolean.TYPE), "CliAvailabilityIndicator is only legal for primitive boolean return types (" + method.toGenericString() + ")"); + for (String cmd : availability.value()) { + Assert.isTrue(!availabilityIndicators.containsKey(cmd), "Cannot specify an availability indicator for '" + cmd + "' more than once"); + availabilityIndicators.put(cmd, new MethodTarget(method, command)); + } + } + } + } + } + + public final void remove(final CommandMarker command) { + synchronized (mutex) { + commands.remove(command); + for (Method m : command.getClass().getMethods()) { + CliAvailabilityIndicator availability = m.getAnnotation(CliAvailabilityIndicator.class); + if (availability != null) { + for (String cmd : availability.value()) { + availabilityIndicators.remove(cmd); + } + } + } + } + } + + public final void add(final Converter converter) { + synchronized (mutex) { + converters.add(converter); + } + } + + public final void remove(final Converter converter) { + synchronized (mutex) { + converters.remove(converter); + } + } +} diff --git a/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java b/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java new file mode 100644 index 00000000..b8dc4a9e --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/AvailableCommandsConverter.java @@ -0,0 +1,41 @@ +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 supports(final Class requiredType, final String optionContext) { + return String.class.isAssignableFrom(requiredType) && "availableCommands".equals(optionContext); + } + + public boolean getAllPossibleValues(final List completions, final Class requiredType, final String existingData, final String optionContext, final MethodTarget target) { + if (target.getTarget() instanceof SimpleParser) { + SimpleParser cmd = (SimpleParser) target.getTarget(); + + // Only include the first word of each command + for (String s : cmd.getEveryCommand()) { + if (s.contains(" ")) { + completions.add(new Completion(s.substring(0, s.indexOf(" ")))); + } else { + completions.add(new Completion(s)); + } + } + } + return true; + } +} diff --git a/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java b/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java new file mode 100644 index 00000000..be606d43 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/BigDecimalConverter.java @@ -0,0 +1,29 @@ +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/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java b/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java new file mode 100644 index 00000000..2ddb6c8f --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/BigIntegerConverter.java @@ -0,0 +1,29 @@ +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/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java b/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java new file mode 100644 index 00000000..972106a9 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/BooleanConverter.java @@ -0,0 +1,40 @@ +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/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java b/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java new file mode 100644 index 00000000..085f1059 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/CharacterConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/converters/DateConverter.java b/src/main/java/org/springframework/roo/shell/converters/DateConverter.java new file mode 100644 index 00000000..e30ce63a --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/DateConverter.java @@ -0,0 +1,47 @@ +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 { + + // Fields + private final DateFormat dateFormat; + + public DateConverter() { + this.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 (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/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java b/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java new file mode 100644 index 00000000..c3fa7869 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/DoubleConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java b/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java new file mode 100644 index 00000000..10227e9e --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/EnumConverter.java @@ -0,0 +1,38 @@ +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) { + 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) { + Class enumClass = (Class) requiredType; + for (Enum enumValue : enumClass.getEnumConstants()) { + 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/src/main/java/org/springframework/roo/shell/converters/FileConverter.java b/src/main/java/org/springframework/roo/shell/converters/FileConverter.java new file mode 100644 index 00000000..ba5c6f9c --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/FileConverter.java @@ -0,0 +1,127 @@ +package org.springframework.roo.shell.converters; + +import java.io.File; +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.support.util.Assert; +import org.springframework.roo.support.util.FileUtils; + +/** + * {@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 = "~"; + // Constants + private static final String home = System.getProperty("user.home"); + + // Fields + + /** + * @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(); + + public File convertFromText(final String value, final Class requiredType, final String optionContext) { + return new File(convertUserInputIntoAFullyQualifiedPath(value)); + } + + public boolean getAllPossibleValues(final List completions, final Class requiredType, final String originalUserInput, final String optionContext, final MethodTarget target) { + String adjustedUserInput = convertUserInputIntoAFullyQualifiedPath(originalUserInput); + + 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; + } + + protected void populate(final List completions, final String adjustedUserInput, final String originalUserInput, final String directoryData) { + File directory = new File(directoryData); + + if (!directory.isDirectory()) { + return; + } + + for (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); + } + + private String convertCompletionBackIntoUserInputStyle(final String originalUserInput, final String completion) { + if (FileUtils.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 + Assert.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()); + } + + /** + * 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 (FileUtils.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 + Assert.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 + String fullPath = getWorkingDirectoryAsString() + userInput; + return fullPath; + } + + private String getWorkingDirectoryAsString() { + try { + return getWorkingDirectory().getCanonicalPath() + File.separator; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java b/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java new file mode 100644 index 00000000..684e5d33 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/FloatConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java b/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java new file mode 100644 index 00000000..0723c597 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/IntegerConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java b/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java new file mode 100644 index 00000000..5680d366 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/LocaleConverter.java @@ -0,0 +1,39 @@ +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) { + 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/src/main/java/org/springframework/roo/shell/converters/LongConverter.java b/src/main/java/org/springframework/roo/shell/converters/LongConverter.java new file mode 100644 index 00000000..795fafff --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/LongConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java b/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java new file mode 100644 index 00000000..f3eadd05 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/ShortConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverter.java new file mode 100644 index 00000000..b99e9ced --- /dev/null +++ b/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/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java new file mode 100644 index 00000000..fe475a03 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/StaticFieldConverterImpl.java @@ -0,0 +1,90 @@ +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.springframework.roo.shell.Completion; +import org.springframework.roo.shell.Converter; +import org.springframework.roo.shell.MethodTarget; +import org.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.StringUtils; + +/** + * 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 { + + // Fields + private final Map,Map> fields = new HashMap,Map>(); + + public void add(final Class clazz) { + Assert.notNull(clazz, "A class to provide conversion services is required"); + Assert.isNull(fields.get(clazz), "Class '" + clazz + "' is already registered for completion services"); + Map ffields = new HashMap(); + for (Field field : clazz.getFields()) { + int modifier = field.getModifiers(); + if (Modifier.isStatic(modifier) && Modifier.isPublic(modifier)) { + ffields.put(field.getName(), field); + } + } + Assert.notEmpty(ffields, "Zero public static fields accessible in '" + clazz + "'"); + fields.put(clazz, ffields); + } + + public void remove(final Class clazz) { + Assert.notNull(clazz, "A class that was providing conversion services is required"); + fields.remove(clazz); + } + + public Object convertFromText(final String value, final Class requiredType, final String optionContext) { + if (StringUtils.isBlank(value)) { + return null; + } + Map ffields = fields.get(requiredType); + if (ffields == null) { + return null; + } + Field f = ffields.get(value); + if (f == null) { + // Fallback to case insensitive search + for (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 (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) { + Map ffields = fields.get(requiredType); + if (ffields == null) { + return true; + } + for (String field : ffields.keySet()) { + completions.add(new Completion(field)); + } + return true; + } + + public boolean supports(final Class requiredType, final String optionContext) { + return fields.get(requiredType) != null; + } +} diff --git a/src/main/java/org/springframework/roo/shell/converters/StringConverter.java b/src/main/java/org/springframework/roo/shell/converters/StringConverter.java new file mode 100644 index 00000000..7b1fc0b9 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/converters/StringConverter.java @@ -0,0 +1,28 @@ +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/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java b/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java new file mode 100644 index 00000000..cdc9056e --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/event/AbstractShellStatusPublisher.java @@ -0,0 +1,67 @@ +package org.springframework.roo.shell.event; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.springframework.roo.shell.ParseResult; +import org.springframework.roo.shell.event.ShellStatus.Status; +import org.springframework.roo.support.util.Assert; + +/** + * Provides a convenience superclass for those shells wishing to publish status messages. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class AbstractShellStatusPublisher implements ShellStatusProvider { + + // Fields + protected Set shellStatusListeners = new CopyOnWriteArraySet(); + protected ShellStatus shellStatus = new ShellStatus(Status.STARTING); + + public final void addShellStatusListener(final ShellStatusListener shellStatusListener) { + Assert.notNull(shellStatusListener, "Status listener required"); + synchronized (shellStatus) { + shellStatusListeners.add(shellStatusListener); + } + } + + public final void removeShellStatusListener(final ShellStatusListener shellStatusListener) { + Assert.notNull(shellStatusListener, "Status listener required"); + synchronized (shellStatus) { + shellStatusListeners.remove(shellStatusListener); + } + } + + public final ShellStatus getShellStatus() { + synchronized (shellStatus) { + return shellStatus; + } + } + + protected void setShellStatus(final Status shellStatus) { + setShellStatus(shellStatus, null, null); + } + + protected void setShellStatus(final Status shellStatus, final String msg, final ParseResult parseResult) { + Assert.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 (ShellStatusListener listener : shellStatusListeners) { + listener.onShellStatusChange(this.shellStatus, st); + } + this.shellStatus = st; + } + } +} diff --git a/src/main/java/org/springframework/roo/shell/event/ShellStatus.java b/src/main/java/org/springframework/roo/shell/event/ShellStatus.java new file mode 100644 index 00000000..0e160c7e --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/event/ShellStatus.java @@ -0,0 +1,95 @@ +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 { + + // Fields + private final Status status; + private String message = ""; + private ParseResult parseResult; + + public enum Status { + STARTING, + STARTED, + USER_INPUT, + PARSING, + EXECUTING, + EXECUTION_RESULT_PROCESSING, + EXECUTION_SUCCESS, + EXECUTION_FAILED, + SHUTTING_DOWN + } + + ShellStatus(final Status status) { + this.status = status; + } + + ShellStatus(final Status status, final String msg, final ParseResult parseResult) { + this.status = status; + this.message = msg; + this.parseResult = parseResult; + } + + public String getMessage() { + return message; + } + + public Status getStatus() { + return status; + } + + public final ParseResult getParseResult() { + return parseResult; + } + + @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; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + 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; + } +} diff --git a/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java b/src/main/java/org/springframework/roo/shell/event/ShellStatusListener.java new file mode 100644 index 00000000..8acee67b --- /dev/null +++ b/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/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java b/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java new file mode 100644 index 00000000..660d7490 --- /dev/null +++ b/src/main/java/org/springframework/roo/shell/event/ShellStatusProvider.java @@ -0,0 +1,48 @@ +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); + + /** + * 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); + + /** + * Returns the current shell status. + * + * @return the current status (never null) + */ + ShellStatus getShellStatus(); +} diff --git a/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java b/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java new file mode 100644 index 00000000..accb678a --- /dev/null +++ b/src/main/java/org/springframework/roo/support/ant/AntPatchStringMatcher.java @@ -0,0 +1,219 @@ +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 { + + // Fields + private final char[] patArr; + private final char[] strArr; + private int patIdxStart = 0; + private int patIdxEnd; + private int strIdxStart = 0; + private int strIdxEnd; + private char ch; + 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) { + String varName = new String(patArr, curlyIdxStart + 1, curlyIdxEnd - curlyIdxStart - 1); + String varValue = new String(strArr, valIdxStart, valIdxEnd - valIdxStart + 1); + uriTemplateVariables.put(varName, varValue); + } + } + + /** + * 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 + int patLength = (patIdxTmp - patIdxStart - 1); + 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 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 int findNextStarOrCurly() { + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*' || patArr[i] == '{') { + return i; + } + } + return -1; + } + + private int findClosingCurly() { + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '}') { + return i; + } + } + return -1; + } + + private boolean onlyStarsLeft() { + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + private boolean allCharsUsed() { + return strIdxStart > strIdxEnd; + } + + private boolean shortcutPossible() { + for (char ch : patArr) { + if (ch == '*' || ch == '{' || ch == '}') { + return false; + } + } + return true; + } + + 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 boolean patternContainsOnlyStar() { + return (patIdxEnd == 0 && patArr[0] == '*'); + } + + 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 matchBeforeFirstStarOrCurly() { + while ((ch = patArr[patIdxStart]) != '*' && ch != '{' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxStart]) { + return false; + } + } + patIdxStart++; + strIdxStart++; + } + return true; + } + + private boolean matchAfterLastStarOrCurly() { + while ((ch = patArr[patIdxEnd]) != '*' && ch != '}' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxEnd]) { + return false; + } + } + patIdxEnd--; + strIdxEnd--; + } + return true; + } +} diff --git a/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java b/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java new file mode 100644 index 00000000..23148a01 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/ant/AntPathMatcher.java @@ -0,0 +1,243 @@ +package org.springframework.roo.support.ant; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.StringUtils; + +/** + * 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; + + /** + * 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); + } + + 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); + } + + /** + * 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(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); + String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String patDir = pattDirs[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(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // Up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String patDir = pattDirs[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 (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[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 + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + 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 (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + /** + * 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) { + AntPatchStringMatcher matcher = new AntPatchStringMatcher(pattern, str, uriTemplateVariables); + return matcher.matchStrings(); + } + + /** + * 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) { + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator); + + 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++) { + String patternPart = patternParts[i]; + if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) { + if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.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(this.pathSeparator); + } + builder.append(pathParts[i]); + } + + return builder.toString(); + } + + public Map extractUriTemplateVariables(final String pattern, final String path) { + Map variables = new LinkedHashMap(); + boolean result = doMatch(pattern, path, true, variables); + Assert.state(result, "Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + return variables; + } +} diff --git a/src/main/java/org/springframework/roo/support/ant/PathMatcher.java b/src/main/java/org/springframework/roo/support/ant/PathMatcher.java new file mode 100644 index 00000000..22e795ac --- /dev/null +++ b/src/main/java/org/springframework/roo/support/ant/PathMatcher.java @@ -0,0 +1,84 @@ +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 { + + /** + * 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); + + /** + * 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); +} diff --git a/src/main/java/org/springframework/roo/support/api/AddOnSearch.java b/src/main/java/org/springframework/roo/support/api/AddOnSearch.java new file mode 100644 index 00000000..972c7591 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/api/AddOnSearch.java @@ -0,0 +1,38 @@ +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/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java b/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java new file mode 100644 index 00000000..0c30bb0d --- /dev/null +++ b/src/main/java/org/springframework/roo/support/logging/DeferredLogHandler.java @@ -0,0 +1,128 @@ +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.springframework.roo.support.util.Assert; + +/** + * 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 { + + // Fields + private final List logRecords = Collections.synchronizedList(new ArrayList()); + private final Handler fallbackHandler; + private final Level fallbackPushLevel; + private boolean fallbackMode = false; + 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) { + Assert.notNull(fallbackHandler, "Fallback handler required"); + Assert.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(); + } + + /** + * 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); + } + } + + /** + * @return the target {@link Handler}, or null if there is no target {@link Handler} defined so far + */ + public Handler getTargetHandler() { + return targetHandler; + } + + public void setTargetHandler(final Handler targetHandler) { + Assert.notNull(targetHandler, "Must specify a target handler"); + this.targetHandler = targetHandler; + if (!fallbackMode) { + publishLogRecordsTo(this.targetHandler); + } + } + + private void publishLogRecordsTo(final Handler destination) { + synchronized (logRecords) { + for (LogRecord record : logRecords) { + destination.publish(record); + } + logRecords.clear(); + } + } +} diff --git a/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java b/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java new file mode 100644 index 00000000..f7c6067c --- /dev/null +++ b/src/main/java/org/springframework/roo/support/logging/HandlerUtils.java @@ -0,0 +1,148 @@ +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.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.StringUtils; + +/** + * Utility methods for dealing with {@link Handler} objects. + * + * @author Ben Alex + * @since 1.0 + * + */ +public abstract class HandlerUtils { + + /** + * 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) { + Assert.notNull(clazz, "Class required"); + Logger logger = Logger.getLogger(clazz.getName()); + if (logger.getLevel() == null && clazz.getName().startsWith("org.springframework.roo")) { + logger.setLevel(Level.FINE); + } + return logger; + } + + /** + * 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) { + Assert.notNull(logger, "Logger is required"); + Assert.notNull(fallbackSeverity, "Fallback severity is required"); + + List newHandlers = new ArrayList(); + + // Create DeferredLogHandlers for each Handler in presented Logger + Handler[] handlers = logger.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (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()) { + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setFormatter(new Formatter() { + @Override + public String format(final LogRecord record) { + return record.getMessage() + StringUtils.LINE_SEPARATOR; + } + }); + newHandlers.add(new DeferredLogHandler(consoleHandler, fallbackSeverity)); + } + + // Add the new DeferredLogHandlers to the presented Logger + for (DeferredLogHandler h : newHandlers) { + logger.addHandler(h); + } + + return newHandlers.size(); + } + + /** + * 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) { + Assert.notNull(logger, "Logger is required"); + Assert.notNull(target, "Target handler is required"); + + int replaced = 0; + Handler[] handlers = logger.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (Handler h : handlers) { + if (h instanceof DeferredLogHandler) { + replaced++; + DeferredLogHandler defLogger = (DeferredLogHandler) h; + defLogger.setTargetHandler(target); + } + } + } + + return replaced; + } + + /** + * 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) { + Assert.notNull(logger, "Logger is required"); + + int flushed = 0; + Handler[] handlers = logger.getHandlers(); + if (handlers != null && handlers.length > 0) { + for (Handler h : handlers) { + flushed++; + h.flush(); + } + } + + return flushed; + } +} diff --git a/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java b/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java new file mode 100644 index 00000000..a808eb60 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/logging/LoggingOutputStream.java @@ -0,0 +1,75 @@ +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.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.IOUtils; + +/** + * 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 { + + // Constants + protected static final Logger LOGGER = HandlerUtils.getLogger(LoggingOutputStream.class); + + // Fields + private final Level level; + private String sourceClassName = LoggingOutputStream.class.getName(); + private int count; + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + /** + * Constructor + * + * @param level the level at which to log (required) + */ + public LoggingOutputStream(final Level level) { + Assert.notNull(level, "A logging level is required"); + this.level = level; + } + + @Override + public void write(final int b) throws IOException { + baos.write(b); + count++; + } + + @Override + public void flush() throws IOException { + if (count > 0) { + String msg = new String(baos.toByteArray()); + LogRecord record = new LogRecord(level, msg); + record.setSourceClassName(sourceClassName); + try { + LOGGER.log(record); + } finally { + count = 0; + IOUtils.closeQuietly(baos); + baos = new ByteArrayOutputStream(); + } + } + } + + @Override + public void close() throws IOException { + flush(); + } + + public String getSourceClassName() { + return sourceClassName; + } + + public void setSourceClassName(final String sourceClassName) { + this.sourceClassName = sourceClassName; + } +} diff --git a/src/main/java/org/springframework/roo/support/style/DefaultToStringStyler.java b/src/main/java/org/springframework/roo/support/style/DefaultToStringStyler.java new file mode 100644 index 00000000..c67e5392 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/style/DefaultToStringStyler.java @@ -0,0 +1,96 @@ +/* + * 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.style; + +import org.springframework.roo.support.util.Assert; +import org.springframework.roo.support.util.ClassUtils; +import org.springframework.roo.support.util.ObjectUtils; + +/** + * Spring's default toString() styler. + * + *

This class is used by {@link ToStringCreator} to style toString() + * output in a consistent manner according to Spring conventions. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class DefaultToStringStyler implements ToStringStyler { + + // Fields + private final ValueStyler valueStyler; + + /** + * Create a new DefaultToStringStyler. + * @param valueStyler the ValueStyler to use + */ + public DefaultToStringStyler(final ValueStyler valueStyler) { + Assert.notNull(valueStyler, "ValueStyler must not be null"); + this.valueStyler = valueStyler; + } + + /** + * Return the ValueStyler used by this ToStringStyler. + */ + protected final ValueStyler getValueStyler() { + return this.valueStyler; + } + + public void styleStart(final StringBuilder buffer, final Object obj) { + if (!obj.getClass().isArray()) { + buffer.append('[').append(ClassUtils.getShortName(obj.getClass())); + styleIdentityHashCode(buffer, obj); + } + else { + buffer.append('['); + styleIdentityHashCode(buffer, obj); + buffer.append(' '); + styleValue(buffer, obj); + } + } + + private void styleIdentityHashCode(final StringBuilder buffer, final Object obj) { + buffer.append('@'); + buffer.append(ObjectUtils.getIdentityHexString(obj)); + } + + public void styleEnd(final StringBuilder buffer, final Object o) { + buffer.append(']'); + } + + public void styleField(final StringBuilder buffer, final String fieldName, final Object value) { + styleFieldStart(buffer, fieldName); + styleValue(buffer, value); + styleFieldEnd(buffer, fieldName); + } + + protected void styleFieldStart(final StringBuilder buffer, final String fieldName) { + buffer.append(' ').append(fieldName).append(" = "); + } + + protected void styleFieldEnd(final StringBuilder buffer, final String fieldName) { + } + + public void styleValue(final StringBuilder buffer, final Object value) { + buffer.append(this.valueStyler.style(value)); + } + + public void styleFieldSeparator(final StringBuilder buffer) { + buffer.append(','); + } +} diff --git a/src/main/java/org/springframework/roo/support/style/DefaultValueStyler.java b/src/main/java/org/springframework/roo/support/style/DefaultValueStyler.java new file mode 100644 index 00000000..3e256283 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/style/DefaultValueStyler.java @@ -0,0 +1,134 @@ +/* + * 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.style; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.roo.support.util.ClassUtils; +import org.springframework.roo.support.util.ObjectUtils; + +/** + * Converts objects to String form, generally for debugging purposes, + * using Spring's toString styling conventions. + * + *

Uses the reflective visitor pattern underneath the hood to nicely + * encapsulate styling algorithms for each type of styled object. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class DefaultValueStyler implements ValueStyler { + private static final String EMPTY = "[empty]"; + private static final String NULL = "[null]"; + private static final String COLLECTION = "collection"; + private static final String SET = "set"; + private static final String LIST = "list"; + private static final String MAP = "map"; + private static final String ARRAY = "array"; + + public String style(final Object value) { + if (value == null) { + return NULL; + } else if (value instanceof String) { + return "\'" + value + "\'"; + } else if (value instanceof Class) { + return ClassUtils.getShortName((Class) value); + } else if (value instanceof Method) { + Method method = (Method) value; + return method.getName() + "@" + ClassUtils.getShortName(method.getDeclaringClass()); + } else if (value instanceof Map) { + return style((Map) value); + } else if (value instanceof Map.Entry) { + return style((Map.Entry) value); + } else if (value instanceof Collection) { + return style((Collection) value); + } else if (value.getClass().isArray()) { + return styleArray(ObjectUtils.toObjectArray(value)); + } else { + return String.valueOf(value); + } + } + + private String style(final Map value) { + StringBuilder result = new StringBuilder(value.size() * 8 + 16); + result.append(MAP + "["); + for (Iterator it = value.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + result.append(style(entry)); + if (it.hasNext()) { + result.append(',').append(' '); + } + } + if (value.isEmpty()) { + result.append(EMPTY); + } + result.append("]"); + return result.toString(); + } + + private String style(final Map.Entry value) { + return style(value.getKey()) + " -> " + style(value.getValue()); + } + + private String style(final Collection value) { + StringBuilder result = new StringBuilder(value.size() * 8 + 16); + result.append(getCollectionTypeString(value)).append('['); + for (Iterator i = value.iterator(); i.hasNext();) { + result.append(style(i.next())); + if (i.hasNext()) { + result.append(',').append(' '); + } + } + if (value.isEmpty()) { + result.append(EMPTY); + } + result.append("]"); + return result.toString(); + } + + private String getCollectionTypeString(final Collection value) { + if (value instanceof List) { + return LIST; + } else if (value instanceof Set) { + return SET; + } else { + return COLLECTION; + } + } + + private String styleArray(final Object[] array) { + StringBuilder result = new StringBuilder(array.length * 8 + 16); + result.append(ARRAY + "<").append(ClassUtils.getShortName(array.getClass().getComponentType())).append(">["); + for (int i = 0; i < array.length - 1; i++) { + result.append(style(array[i])); + result.append(',').append(' '); + } + if (array.length > 0) { + result.append(style(array[array.length - 1])); + } else { + result.append(EMPTY); + } + result.append("]"); + return result.toString(); + } +} diff --git a/src/main/java/org/springframework/roo/support/style/StylerUtils.java b/src/main/java/org/springframework/roo/support/style/StylerUtils.java new file mode 100644 index 00000000..449bc108 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/style/StylerUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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.style; + +/** + * Simple utility class to allow for convenient access to value + * styling logic, mainly to support descriptive logging messages. + * + *

For more sophisticated needs, use the {@link ValueStyler} abstraction + * directly. This class simply uses a shared {@link DefaultValueStyler} + * instance underneath. + * + * @author Keith Donald + * @since 1.2.2 + * @see ValueStyler + * @see DefaultValueStyler + */ +public abstract class StylerUtils { + + /** + * Default ValueStyler instance used by the style method. + * Also available for the {@link ToStringCreator} class in this package. + */ + static final ValueStyler DEFAULT_VALUE_STYLER = new DefaultValueStyler(); + + /** + * Style the specified value according to default conventions. + * @param value the Object value to style + * @return the styled String + * @see DefaultValueStyler + */ + public static String style(final Object value) { + return DEFAULT_VALUE_STYLER.style(value); + } +} diff --git a/src/main/java/org/springframework/roo/support/style/ToStringCreator.java b/src/main/java/org/springframework/roo/support/style/ToStringCreator.java new file mode 100644 index 00000000..0f58209e --- /dev/null +++ b/src/main/java/org/springframework/roo/support/style/ToStringCreator.java @@ -0,0 +1,194 @@ +/* + * 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.style; + +import org.springframework.roo.support.util.Assert; + +/** + * Utility class that builds pretty-printing toString() methods + * with pluggable styling conventions. By default, ToStringCreator adheres + * to Spring's toString() styling conventions. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class ToStringCreator { + + /** + * Default ToStringStyler instance used by this ToStringCreator. + */ + private static final ToStringStyler DEFAULT_TO_STRING_STYLER = new DefaultToStringStyler(StylerUtils.DEFAULT_VALUE_STYLER); + + // Fields + private final StringBuilder buffer = new StringBuilder(512); + private final ToStringStyler styler; + private final Object object; + + private boolean styledFirstField; + + /** + * Create a ToStringCreator for the given object. + * + * @param obj the object to be stringified + */ + public ToStringCreator(final Object obj) { + this(obj, (ToStringStyler) null); + } + + /** + * Create a ToStringCreator for the given object, using the provided style. + * + * @param obj the object to be stringified + * @param styler the ValueStyler encapsulating pretty-print instructions + */ + public ToStringCreator(final Object obj, final ValueStyler styler) { + this(obj, new DefaultToStringStyler(styler != null ? styler : StylerUtils.DEFAULT_VALUE_STYLER)); + } + + /** + * Create a ToStringCreator for the given object, using the provided style. + * + * @param obj the object to be stringified + * @param styler the ToStringStyler encapsulating pretty-print instructions + */ + public ToStringCreator(final Object obj, final ToStringStyler styler) { + Assert.notNull(obj, "The object to be styled must not be null"); + this.object = obj; + this.styler = (styler != null ? styler : DEFAULT_TO_STRING_STYLER); + this.styler.styleStart(this.buffer, this.object); + } + + /** + * Append a byte field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final byte value) { + return append(fieldName, Byte.valueOf(value)); + } + + /** + * Append a short field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final short value) { + return append(fieldName, Short.valueOf(value)); + } + + /** + * Append a integer field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final int value) { + return append(fieldName, Integer.valueOf(value)); + } + + /** + * Append a long field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final long value) { + return append(fieldName, Long.valueOf(value)); + } + + /** + * Append a float field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final float value) { + return append(fieldName, new Float(value)); + } + + /** + * Append a double field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final double value) { + return append(fieldName, new Double(value)); + } + + /** + * Append a boolean field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final boolean value) { + return append(fieldName, Boolean.valueOf(value)); + } + + /** + * Append a field value. + * + * @param fieldName the name of the field, usually the member variable name + * @param value the field value; can be null + * @return this, to support call-chaining + */ + public ToStringCreator append(final String fieldName, final Object value) { + printFieldSeparatorIfNecessary(); + this.styler.styleField(this.buffer, fieldName, value); + return this; + } + + private void printFieldSeparatorIfNecessary() { + if (this.styledFirstField) { + this.styler.styleFieldSeparator(this.buffer); + } + else { + this.styledFirstField = true; + } + } + + /** + * Append the provided value. + * + * @param value The value to append + * @return this, to support call-chaining. + */ + public ToStringCreator append(final Object value) { + this.styler.styleValue(this.buffer, value); + return this; + } + + /** + * Return the String representation that this ToStringCreator built. + */ + @Override + public String toString() { + this.styler.styleEnd(this.buffer, this.object); + return this.buffer.toString(); + } +} diff --git a/src/main/java/org/springframework/roo/support/style/ToStringStyler.java b/src/main/java/org/springframework/roo/support/style/ToStringStyler.java new file mode 100644 index 00000000..c1f999e6 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/style/ToStringStyler.java @@ -0,0 +1,68 @@ +/* + * 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.style; + +/** + * A strategy interface for pretty-printing toString() methods. + * Encapsulates the print algorithms; some other object such as a builder + * should provide the workflow. + * + * @author Keith Donald + * @since 1.2.2 + */ +public interface ToStringStyler { + + /** + * Style a toString()'ed object before its fields are styled. + * + * @param buffer the buffer to print to + * @param obj the object to style; can be null + */ + void styleStart(StringBuilder buffer, Object obj); + + /** + * Style a toString()'ed object after it's fields are styled. + * + * @param buffer the buffer to print to + * @param obj the object to style; can be null + */ + void styleEnd(StringBuilder buffer, Object obj); + + /** + * Style a field value as a string. + * + * @param buffer the buffer to print to + * @param fieldName the he name of the field + * @param value the field value; can be null + */ + void styleField(StringBuilder buffer, String fieldName, Object value); + + /** + * Style the given value. + * + * @param buffer the buffer to print to + * @param value the field value; can be null + */ + void styleValue(StringBuilder buffer, Object value); + + /** + * Style the field separator. + * + * @param buffer buffer to print to + */ + void styleFieldSeparator(StringBuilder buffer); +} diff --git a/src/main/java/org/springframework/roo/support/style/ValueStyler.java b/src/main/java/org/springframework/roo/support/style/ValueStyler.java new file mode 100644 index 00000000..4ec3aca3 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/style/ValueStyler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2007 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.style; + +/** + * Strategy that encapsulates value String styling algorithms + * according to Spring conventions. + * + * @author Keith Donald + * @since 1.2.2 + */ +public interface ValueStyler { + + /** + * Style the given value, returning a String representation. + * @param value the Object value to style + * @return the styled String + */ + String style(Object value); +} diff --git a/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java b/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java new file mode 100644 index 00000000..e84d68ad --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/AnsiEscapeCode.java @@ -0,0 +1,69 @@ +package org.springframework.roo.support.util; + +/** + * 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_YELLOW(33), + FG_WHITE(37), + OFF(0), + REVERSE(7), + UNDERSCORE(4); + + // Constant for the escape character + private static final boolean ANSI_SUPPORTED = Boolean.getBoolean("roo.console.ansi"); + private static final char ESC = 27; + + /** + * 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 (text == null || "".equals(text)) { + return text; + } + + final StringBuilder sb = new StringBuilder(); + if (ANSI_SUPPORTED) { + for (final AnsiEscapeCode code : codes) { + sb.append(code.code); + } + } + sb.append(text); + if (codes != null && codes.length > 0 && ANSI_SUPPORTED) { + sb.append(OFF.code); + } + return sb.toString(); + } + + // Fields + 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/src/main/java/org/springframework/roo/support/util/Assert.java b/src/main/java/org/springframework/roo/support/util/Assert.java new file mode 100644 index 00000000..695eae4c --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/Assert.java @@ -0,0 +1,397 @@ +package org.springframework.roo.support.util; + +/* + * Copyright 2002-2007 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.Collection; +import java.util.Map; + +/** + * Assertion utility class that assists in validating arguments. + * Useful for identifying programmer errors early and clearly at runtime. + * + *

For example, if the contract of a public method states it does not + * allow null arguments, Assert can be used to validate that + * contract. Doing this clearly indicates a contract violation when it + * occurs and protects the class's invariants. + * + *

Typically used to validate method arguments rather than configuration + * properties, to check for cases that are usually programmer errors rather than + * configuration errors. In contrast to config initialization code, there is + * usally no point in falling back to defaults in such methods. + * + *

This class is similar to JUnit's assertion library. If an argument value is + * deemed invalid, an {@link IllegalArgumentException} is thrown (typically). + * For example: + * + *

+ * Assert.notNull(clazz, "The class must not be null");
+ * Assert.isTrue(i > 0, "The value must be greater than zero");
+ * + * Mainly for internal use within the framework; consider Jakarta's Commons Lang + * >= 2.0 for a more comprehensive suite of assertion utilities. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @author Rob Harrop + * @since 1.1.2 + */ +public abstract class Assert { + + /** + * Assert a boolean expression, throwing IllegalArgumentException + * if the test result is false. + *
Assert.isTrue(i > 0, "The value must be greater than zero");
+ * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(final boolean expression, final String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert a boolean expression, throwing IllegalArgumentException + * if the test result is false. + *
Assert.isTrue(i > 0);
+ * @param expression a boolean expression + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(final boolean expression) { + isTrue(expression, "[Assertion failed] - this expression must be true"); + } + + /** + * Assert that an object is null . + *
Assert.isNull(value, "The value must be null");
+ * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is not null + */ + public static void isNull(final Object object, final String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is null . + *
Assert.isNull(value);
+ * @param object the object to check + * @throws IllegalArgumentException if the object is not null + */ + public static void isNull(final Object object) { + isNull(object, "[Assertion failed] - the object argument must be null"); + } + + /** + * Assert that an object is not null . + *
Assert.notNull(clazz, "The class must not be null");
+ * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(final Object object, final String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not null . + *
Assert.notNull(clazz);
+ * @param object the object to check + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(final Object object) { + notNull(object, "[Assertion failed] - this argument is required; it must not be null"); + } + + /** + * Assert that the given String is not empty; that is, + * it must not be null and not the empty String. + *
Assert.hasLength(name, "Name must not be empty");
+ * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see StringUtils#hasLength + */ + public static void hasLength(final String text, final String message) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be null and not the empty String. + *
Assert.hasLength(name);
+ * @param text the String to check + * @see StringUtils#hasLength + */ + public static void hasLength(final String text) { + hasLength(text, + "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be null and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see StringUtils#hasText + */ + public static void hasText(final String text, final String message) { + if (StringUtils.isBlank(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be null and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check + * @see StringUtils#hasText + */ + public static void hasText(final String text) { + hasText(text, + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails + */ + public static void doesNotContain(final String textToSearch, final String substring, final String message) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.indexOf(substring) != -1) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod");
+ * @param textToSearch the text to search + * @param substring the substring to find within the text + */ + public static void doesNotContain(final String textToSearch, final String substring) { + doesNotContain(textToSearch, substring, + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + } + + /** + * Assert that an array has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(array, "The array must have elements");
+ * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array is null or has no elements + */ + public static void notEmpty(final Object[] array, final String message) { + if (ObjectUtils.isEmpty(array)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(array);
+ * @param array the array to check + * @throws IllegalArgumentException if the object array is null or has no elements + */ + public static void notEmpty(final Object[] array) { + notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + *
Assert.noNullElements(array, "The array must have non-null elements");
+ * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array contains a null element + */ + public static void noNullElements(final Object[] array, final String message) { + if (array != null) { + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + *
Assert.noNullElements(array);
+ * @param array the array to check + * @throws IllegalArgumentException if the object array contains a null element + */ + public static void noNullElements(final Object[] array) { + noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); + } + + /** + * Assert that a collection has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection is null or has no elements + */ + public static void notEmpty(final Collection collection, final String message) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a collection has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check + * @throws IllegalArgumentException if the collection is null or has no elements + */ + public static void notEmpty(final Collection collection) { + notEmpty(collection, + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that a Map has entries; that is, it must not be null + * and must have at least one entry. + *
Assert.notEmpty(map, "Map must have entries");
+ * @param map the map to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the map is null or has no entries + */ + public static void notEmpty(final Map map, final String message) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map has entries; that is, it must not be null + * and must have at least one entry. + *
Assert.notEmpty(map);
+ * @param map the map to check + * @throws IllegalArgumentException if the map is null or has no entries + */ + public static void notEmpty(final Map map) { + notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); + } + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * @param clazz the required class + * @param obj the object to check + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(final Class clazz, final Object obj) { + isInstanceOf(clazz, obj, ""); + } + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * @param type the type to check against + * @param obj the object to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(final Class type, final Object obj, final String message) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + throw new IllegalArgumentException(message + + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); + } + } + + /** + * Assert that superType.isAssignableFrom(subType) is true. + *
Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check + * @param subType the sub type to check + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(final Class superType, final Class subType) { + isAssignable(superType, subType, ""); + } + + /** + * Assert that superType.isAssignableFrom(subType) is true. + *
Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check against + * @param subType the sub type to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(final Class superType, final Class subType, final String message) { + notNull(superType, "Type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + throw new IllegalArgumentException(message + subType + " is not assignable to " + superType); + } + } + + /** + * Assert a boolean expression, throwing IllegalStateException + * if the test result is false. Call isTrue if you wish to + * throw IllegalArgumentException on an assertion failure. + *
Assert.state(id == null, "The id property must not already be initialized");
+ * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalStateException if expression is false + */ + public static void state(final boolean expression, final String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing {@link IllegalStateException} + * if the test result is false. + *

Call {@link #isTrue(boolean)} if you wish to + * throw {@link IllegalArgumentException} on an assertion failure. + *

Assert.state(id == null);
+ * @param expression a boolean expression + * @throws IllegalStateException if the supplied expression is false + */ + public static void state(final boolean expression) { + state(expression, "[Assertion failed] - this state invariant must be true"); + } +} diff --git a/src/main/java/org/springframework/roo/support/util/Base64.java b/src/main/java/org/springframework/roo/support/util/Base64.java new file mode 100644 index 00000000..83f27fae --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/Base64.java @@ -0,0 +1,1882 @@ +package org.springframework.roo.support.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + *

Encodes and decodes to and from Base64 notation.

+ *

Homepage: http://iharder.net/base64.

+ * + *

Example:

+ * + * String encoded = Base64.encode(myByteArray); + *
+ * byte[] myByteArray = Base64.decode(encoded); + * + *

The options parameter, which appears in a few places, is used to pass + * several pieces of information to the encoder. In the "higher level" methods such as + * encodeBytes(bytes, options) the options parameter can be used to indicate such + * things as first gzipping the bytes before encoding them, not inserting linefeeds, + * and encoding using the URL-safe and Ordered dialects.

+ * + *

Note, according to RFC3548, + * Section 2.1, implementations should not add line feeds unless explicitly told + * to do so. I've got Base64 set to this behavior now, although earlier versions + * broke lines by default.

+ * + *

The constants defined in Base64 can be OR-ed together to combine options, so you + * might make a call like this:

+ * + * String encoded = Base64.encodeBytes(mybytes, Base64.GZIP | Base64.DO_BREAK_LINES); + *

to compress the data before encoding it and then making the output have newline characters.

+ *

Also...

+ * String encoded = Base64.encodeBytes(crazyString.getBytes()); + * + * + * + *

+ * Change Log: + *

+ *
    + *
  • v2.3.7 - Fixed subtle bug when base 64 input stream contained the + * value 01111111, which is an invalid base 64 character but should not + * throw an ArrayIndexOutOfBoundsException either. Led to discovery of + * mishandling (or potential for better handling) of other bad input + * characters. You should now get an IOException if you try decoding + * something that has bad characters in it.
  • + *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded + * string ended in the last column; the buffer was not properly shrunk and + * contained an extra (null) byte that made it into the string.
  • + *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size + * was wrong for files of size 31, 34, and 37 bytes.
  • + *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing + * the Base64.OutputStream closed the Base64 encoding (by padding with equals + * signs) too soon. Also added an option to suppress the automatic decoding + * of gzipped streams. Also added experimental support for specifying a + * class loader when using the + * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)} + * method.
  • + *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java + * footprint with its CharEncoders and so forth. Fixed some javadocs that were + * inconsistent. Removed imports and specified things like IOException + * explicitly inline.
  • + *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the + * final encoded data will be so that the code doesn't have to create two output + * arrays: an oversized initial one and then a final, exact-sized one. Big win + * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not + * using the gzip options which uses a different mechanism with streams and stuff).
  • + *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some + * similar helper methods to be more efficient with memory by not returning a + * String but just a byte array.
  • + *
  • v2.3 - This is not a drop-in replacement! This is two years of comments + * and bug fixes queued up and finally executed. Thanks to everyone who sent + * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. + * Much bad coding was cleaned up including throwing exceptions where necessary + * instead of returning null values or something similar. Here are some changes + * that may affect you: + *
      + *
    • Does not break lines, by default. This is to keep in compliance with + * RFC3548.
    • + *
    • Throws exceptions instead of returning null values. Because some operations + * (especially those that may permit the GZIP option) use IO streams, there + * is a possiblity of an IOException being thrown. After some discussion and + * thought, I've changed the behavior of the methods to throw IOExceptions + * rather than return null if ever there's an error. I think this is more + * appropriate, though it will require some changes to your code. Sorry, + * it should have been done this way to begin with.
    • + *
    • Removed all references to System.out, System.err, and the like. + * Shame on me. All I can say is sorry they were ever there.
    • + *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed + * such as when passed arrays are null or offsets are invalid.
    • + *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. + * This was especially annoying before for people who were thorough in their + * own projects and then had gobs of javadoc warnings on this file.
    • + *
    + *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug + * when using very small files (~< 40 bytes).
  • + *
  • v2.2 - Added some helper methods for encoding/decoding directly from + * one file to the next. Also added a main() method to support command line + * encoding/decoding from one file to the next. Also added these Base64 dialects: + *
      + *
    1. The default is RFC3548 format.
    2. + *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates + * URL and file name friendly format as described in Section 4 of RFC3548. + * http://www.faqs.org/rfcs/rfc3548.html
    4. + *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates + * URL and file name friendly format that preserves lexical ordering as described + * in http://www.faqs.org/qa/rfcc-1940.html
    6. + *
    + * Special thanks to Jim Kellerman at http://www.powerset.com/ + * for contributing the new Base64 dialects. + *
  • + * + *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added + * some convenience methods for reading and writing to and from files.
  • + *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems + * with other encodings (like EBCDIC).
  • + *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.
  • + *
  • v2.0 - I got rid of methods that used booleans to set options. + * Now everything is more consolidated and cleaner. The code now detects + * when data that's being decoded is gzip-compressed and will decompress it + * automatically. Generally things are cleaner. You'll probably have to + * change some method calls that you were making to support the new + * options format (ints that you "OR" together).
  • + *
  • v1.5.1 - Fixed bug when decompressing and decoding to a + * byte[] using decode(String s, boolean gzipCompressed). + * Added the ability to "suspend" encoding in the Output Stream so + * you can turn on and off the encoding if you need to embed base64 + * data in an otherwise "normal" stream (like an XML file).
  • + *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. + * This helps when using GZIP streams. + * Added the ability to GZip-compress objects before encoding them.
  • + *
  • v1.4 - Added helper methods to read/write files.
  • + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ * + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.7 + */ +public class Base64 { + +/* ******** P U B L I C F I E L D S ******** */ + + /** No options specified. Value is zero. */ + public final static int NO_OPTIONS = 0; + + /** Specify encoding in first bit. Value is one. */ + public final static int ENCODE = 1; + + /** Specify decoding in first bit. Value is zero. */ + public final static int DECODE = 0; + + /** Specify that data should be gzip-compressed in second bit. Value is two. */ + public final static int GZIP = 2; + + /** Specify that gzipped data should not be automatically gunzipped. */ + public final static int DONT_GUNZIP = 4; + + /** Do break lines when encoding. Value is 8. */ + public final static int DO_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described + * in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * It is important to note that data encoded this way is not officially valid Base64, + * or at the very least should not be called Base64 without also specifying that is + * was encoded using the URL- and Filename-safe dialect. + */ + public final static int URL_SAFE = 16; + + /** + * Encode using the special "ordered" dialect of Base64 described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + public final static int ORDERED = 32; + +/* ******** P R I V A T E F I E L D S ******** */ + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + /** Preferred encoding. */ + private final static String PREFERRED_ENCODING = "US-ASCII"; + + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + +/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private final static byte[] _STANDARD_ALPHABET = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' + }; + + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + */ + private final static byte[] _STANDARD_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9,-9,-9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + }; + + +/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private final static byte[] _URL_SAFE_ALPHABET = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' + }; + + /** + * Used in decoding URL- and Filename-safe dialects of Base64. + */ + private final static byte[] _URL_SAFE_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + }; + +/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, + * and it is described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + private final static byte[] _ORDERED_ALPHABET = { + (byte)'-', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', + (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'_', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' + }; + + /** + * Used in decoding the "ordered" dialect of Base64. + */ + private final static byte[] _ORDERED_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' + 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' + -9,-9,-9,-9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' + 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' + -9,-9,-9,-9,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + }; + + + /* ******** D E T E R M I N E W H I C H A L P H A B E T ******** */ + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URLSAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getAlphabet(final int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_ALPHABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_ALPHABET; + } else { + return _STANDARD_ALPHABET; + } + } + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URL_SAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getDecodabet(final int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_DECODABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_DECODABET; + } else { + return _STANDARD_DECODABET; + } + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to the first three bytes of array threeBytes + * and returns a four-byte array in Base64 notation. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * The array threeBytes needs only be as big as + * numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(final byte[] b4, final byte[] threeBytes, final int numSigBytes, final int options) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); + return b4; + } + + /** + *

Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes.

+ *

This is the lowest level of the encoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(final byte[] source, final int srcOffset, final int numSigBytes, final byte[] destination, final int destOffset, final int options) { + + byte[] ALPHABET = getAlphabet(options); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset ] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch(numSigBytes) { + case 3: + destination[destOffset ] = ALPHABET[(inBuff >>> 18) ]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff ) & 0x3f]; + return destination; + + case 2: + destination[destOffset ] = ALPHABET[(inBuff >>> 18) ]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset ] = ALPHABET[(inBuff >>> 18) ]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded ByteBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode(final java.nio.ByteBuffer raw, final java.nio.ByteBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while(raw.hasRemaining()) { + int rem = Math.min(3,raw.remaining()); + raw.get(raw3,0,rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + encoded.put(enc4); + } // end input remaining + } + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded CharBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode(final java.nio.ByteBuffer raw, final java.nio.CharBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while(raw.hasRemaining()) { + int rem = Math.min(3,raw.remaining()); + raw.get(raw3,0,rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + for (int i = 0; i < 4; i++) { + encoded.put((char)(enc4[i] & 0xFF)); + } + } // end input remaining + } + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + * + *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @throws IOException if there is an error + * @throws NullPointerException if serializedObject is null + * @since 1.4 + */ + public static String encodeObject(final java.io.Serializable serializableObject) throws IOException { + return encodeObject(serializableObject, NO_OPTIONS); + } + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + * + *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * The object is not GZip-compressed before being encoded. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     * 
+ *

+ * Example: encodeObject(myObj, Base64.GZIP) or + *

+ * Example: encodeObject(myObj, Base64.GZIP | Base64.DO_BREAK_LINES) + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws IOException if there is an error + * @since 2.0 + */ + public static String encodeObject(final java.io.Serializable serializableObject, final int options) throws IOException { + if (serializableObject == null) { + throw new NullPointerException("Cannot serialize a null object."); + } + + // Streams + ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + GZIPOutputStream gzos = null; + ObjectOutputStream oos = null; + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + if ((options & GZIP) != 0) { + // Gzip + gzos = new GZIPOutputStream(b64os); + oos = new ObjectOutputStream(gzos); + } else { + // Not gzipped + oos = new ObjectOutputStream(b64os); + } + oos.writeObject(serializableObject); + } catch (IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } finally { + IOUtils.closeQuietly(oos, gzos, b64os, baos); + } + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } catch (UnsupportedEncodingException uue) { + // Fall back to some Java default + return new String(baos.toByteArray()); + } // end catch + } // end encode + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @return The data in Base64-encoded form + * @throws NullPointerException if source array is null + * @since 1.4 + */ + public static String encodeBytes(final byte[] source) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes(myData, Base64.GZIP) or + *

+ * Example: encodeBytes(myData, Base64.GZIP | Base64.DO_BREAK_LINES) + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws IOException if there is an error + * @throws NullPointerException if source array is null + * @since 2.0 + */ + public static String encodeBytes(final byte[] source, final int options) throws IOException { + return encodeBytes(source, 0, source.length, options); + } + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + *

As of v 2.3, if there is an error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @return The Base64-encoded data as a String + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 1.4 + */ + public static String encodeBytes(final byte[] source, final int off, final int len) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, off, len, NO_OPTIONS); + } catch (IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes(myData, Base64.GZIP) or + *

+ * Example: encodeBytes(myData, Base64.GZIP | Base64.DO_BREAK_LINES) + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.0 + */ + public static String encodeBytes(final byte[] source, final int off, final int len, final int options) throws IOException { + byte[] encoded = encodeBytesToBytes(source, off, len, options); + + // Return value according to relevant encoding. + try { + return new String(encoded, PREFERRED_ENCODING); + } catch (UnsupportedEncodingException uue) { + return new String(encoded); + } + } + + /** + * Similar to {@link #encodeBytes(byte[])} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * @param source The data to convert + * @return The Base64-encoded data as a byte[] (of ASCII characters) + * @throws NullPointerException if source array is null + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(final byte[] source) { + byte[] encoded = null; + try { + encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS); + } catch (IOException ex) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return encoded; + } + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(final byte[] source, final int off, final int len, final int options) throws IOException { + if (source == null) { + throw new NullPointerException("Cannot serialize a null array."); + } + + if (off < 0) { + throw new IllegalArgumentException("Cannot have negative offset: " + off); + } + + if (len < 0) { + throw new IllegalArgumentException("Cannot have length offset: " + len); + } + + if (off + len > source.length ) { + throw new IllegalArgumentException(String.format("Cannot have offset of %d and length of %d with array of length %d", off,len,source.length)); + } + + if ((options & GZIP) != 0) { + // Compress + ByteArrayOutputStream baos = null; + GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + gzos = new GZIPOutputStream(b64os); + + gzos.write(source, off, len); + } catch (IOException e) { + throw e; + } finally { + IOUtils.closeQuietly(gzos, b64os, baos); + } + + return baos.toByteArray(); + } else { + // Don't compress. Better not to use streams at all then. + boolean breakLines = (options & DO_BREAK_LINES) != 0; + + //int len43 = len * 4 / 3; + //byte[] outBuff = new byte[ (len43) // Main 4:3 + // + ((len % 3) > 0 ? 4 : 0) // Account for padding + // + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New lines + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding + if (breakLines) { + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + } + byte[] outBuff = new byte[encLen]; + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d+=3, e+=4) { + encode3to4(source, d+off, 3, outBuff, e, options); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) + { + outBuff[e+4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d+off, len - d, outBuff, e, options); + e += 4; + } // end if: some padding needed + + // Only resize array if we didn't guess it right. + if (e <= outBuff.length - 1) { + // If breaking lines and the last byte falls right at + // the line length (76 bytes per line), there will be + // one extra byte, and the array will need to be resized. + // Not too bad of an estimate on array size, I'd say. + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff,0, finalOut,0,e); + // System.err.println("Having to resize array from " + outBuff.length + " to " + e); + return finalOut; + } else { + // System.err.println("No need to resize array."); + return outBuff; + } + } // end else: don't compress + } // end encodeBytesToBytes + +/* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + *

This is the lowest level of the decoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @throws NullPointerException if source or destination arrays are null + * @throws IllegalArgumentException if srcOffset or destOffset are invalid + * or there is not enough room in the array. + * @since 1.3 + */ + private static int decode4to3(final byte[] source, final int srcOffset, final byte[] destination, final int destOffset, final int options) { + if (source == null) { + throw new NullPointerException("Source array was null."); + } + if (destination == null) { + throw new NullPointerException("Destination array was null."); + } + if (srcOffset < 0 || srcOffset + 3 >= source.length) { + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset)); + } + if (destOffset < 0 || destOffset +2 >= destination.length) { + throw new IllegalArgumentException(String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset)); + } + + byte[] DECODABET = getDecodabet(options); + + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ((DECODABET[source[srcOffset ]] << 24) >>> 6) + // | ((DECODABET[source[srcOffset + 1]] << 24) >>> 12); + int outBuff = ((DECODABET[source[srcOffset ]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte)(outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ((DECODABET[source[srcOffset ]] << 24) >>> 6) + // | ((DECODABET[source[srcOffset + 1]] << 24) >>> 12) + // | ((DECODABET[source[srcOffset + 2]] << 24) >>> 18); + int outBuff = ((DECODABET[source[srcOffset ]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset ] = (byte)(outBuff >>> 16); + destination[destOffset + 1] = (byte)(outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ((DECODABET[source[srcOffset ]] << 24) >>> 6) + // | ((DECODABET[source[srcOffset + 1]] << 24) >>> 12) + // | ((DECODABET[source[srcOffset + 2]] << 24) >>> 18) + // | ((DECODABET[source[srcOffset + 3]] << 24) >>> 24); + int outBuff = ((DECODABET[source[srcOffset ]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF) ); + + + destination[destOffset ] = (byte)(outBuff >> 16); + destination[destOffset + 1] = (byte)(outBuff >> 8); + destination[destOffset + 2] = (byte)(outBuff ); + + return 3; + } + } // end decodeToBytes + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 2.3.1 + */ + public static byte[] decode(final byte[] source) throws IOException { + return decode(source, 0, source.length, Base64.NO_OPTIONS); + } + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param options Can specify options such as alphabet type to use + * @return decoded data + * @throws IOException If bogus characters exist in source data + * @since 1.3 + */ + public static byte[] decode(final byte[] source, final int off, final int len, final int options) throws IOException { + // Lots of error checking and exception throwing + if (source == null) { + throw new NullPointerException("Cannot decode null source array."); + } + if (off < 0 || off + len > source.length) { + throw new IllegalArgumentException(String.format("Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len)); + } + + if (len == 0) { + return new byte[0]; + } else if (len < 4) { + throw new IllegalArgumentException( + "Base64-encoded string must have at least four characters, but length specified was " + len); + } // end if + + byte[] DECODABET = getDecodabet(options); + + int len34 = len * 3 / 4; // Estimate on array size + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; // Keep track of where we're writing + + byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space + int b4Posn = 0; // Keep track of four byte input buffer + int i = 0; // Source array counter + byte sbiDecode = 0; // Special value from DECODABET + + for (i = off; i < off+len; i++) { // Loop through source + + sbiDecode = DECODABET[source[i]&0xFF]; + + // White space, Equals sign, or legit Base64 character + // Note the values such as -5 and -9 in the + // DECODABETs at the top of the file. + if (sbiDecode >= WHITE_SPACE_ENC) { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = source[i]; // Save non-whitespace + if (b4Posn > 3) { // Time to decode? + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (source[i] == EQUALS_SIGN) { + break; + } // end if: equals sign + } // end if: quartet built + } // end if: equals sign or better + } // end if: white space, equals sign or better + else { + // There's a bad input character in the Base64 stream. + throw new IOException(String.format( + "Bad Base64 input character decimal %d in array position %d", source[i] & 0xFF, i)); + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @throws IOException If there is a problem + * @since 1.4 + */ + public static byte[] decode(final String s) throws IOException { + return decode(s, NO_OPTIONS); + } + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * @return the decoded data + * @throws IOException if there is an error + * @throws NullPointerException if s is null + * @since 1.4 + */ + public static byte[] decode(final String s, final int options) throws IOException { + if (s == null) { + throw new NullPointerException("Input string was null."); + } // end if + + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length, options); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + boolean dontGunzip = (options & DONT_GUNZIP) != 0; + if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) { + + int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (GZIPInputStream.GZIP_MAGIC == head) { + ByteArrayInputStream bais = null; + GZIPInputStream gzis = null; + ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new ByteArrayOutputStream(); + bais = new ByteArrayInputStream(bytes); + gzis = new GZIPInputStream(bais); + + while((length = gzis.read(buffer)) >= 0) { + baos.write(buffer,0,length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } catch (IOException e) { + e.printStackTrace(); + // Just return originally-decoded bytes + } finally { + IOUtils.closeQuietly(baos, gzis, bais); + } + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 1.5 + */ + public static Object decodeToObject(final String encodedObject) + throws IOException, java.lang.ClassNotFoundException { + return decodeToObject(encodedObject,NO_OPTIONS,null); + } + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * If loader is not null, it will be the class loader + * used when deserializing. + * + * @param encodedObject The Base64 data to decode + * @param options Various parameters related to decoding + * @param loader Optional class loader to use in deserializing classes. + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 2.3.4 + */ + public static Object decodeToObject(final String encodedObject, final int options, final ClassLoader loader) throws IOException, ClassNotFoundException { + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject, options); + + ByteArrayInputStream bais = null; + ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new ByteArrayInputStream(objBytes); + + // If no custom class loader is provided, use Java's builtin OIS. + if (loader == null) { + ois = new ObjectInputStream(bais); + } else { + // Make a customized object input stream that uses the provided class loader + ois = new ObjectInputStream(bais) { + @Override + public Class resolveClass(final java.io.ObjectStreamClass streamClass) + throws IOException, ClassNotFoundException { + Class c = Class.forName(streamClass.getName(), false, loader); + if (c == null) { + return super.resolveClass(streamClass); + } else { + return c; // Class loader knows of this class. + } // end else: not null + } // end resolveClass + }; // end ois + } // end else: no custom class loader + + obj = ois.readObject(); + } // end try + catch (IOException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + catch (java.lang.ClassNotFoundException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + finally { + IOUtils.closeQuietly(bais, ois); + } + + return obj; + } // end decodeObject + + + + /** + * Convenience method for encoding data to a file. + * + *

As of v 2.3, if there is a error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @throws IOException if there is an error + * @throws NullPointerException if dataToEncode is null + * @since 2.1 + */ + public static void encodeToFile(final byte[] dataToEncode, final String filename) + throws IOException { + + if (dataToEncode == null) { + throw new NullPointerException("Data to encode was null."); + } // end iff + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + } // end try + catch (IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: IOException + finally { + try { bos.close(); } catch (Exception e) {} + } // end finally + + } // end encodeToFile + + + /** + * Convenience method for decoding data to a file. + * + *

As of v 2.3, if there is a error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @throws IOException if there is an error + * @since 2.1 + */ + public static void decodeToFile(final String dataToDecode, final String filename) + throws IOException { + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + } // end try + catch (IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: IOException + finally { + try { bos.close(); } catch (Exception e) {} + } // end finally + + } // end decodeToFile + + + + + /** + * Convenience method for reading a base64-encoded + * file and decoding it. + * + *

As of v 2.3, if there is a error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading encoded data + * @return decoded byte array + * @throws IOException if there is an error + * @since 2.1 + */ + public static byte[] decodeFromFile(final String filename) + throws IOException { + + Base64.InputStream bis = null; + try + { + // Set up some useful variables + File file = new File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) + { + throw new IOException("File is too big for this convenience method (" + file.length() + " bytes)."); + } // end if: file too big for int index + buffer = new byte[(int)file.length()]; + + // Open a stream + bis = new Base64.InputStream( + new BufferedInputStream( + new FileInputStream(file)), Base64.DECODE); + + // Read until done + while((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } // end while + + // Save in a variable to return + final byte[] decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + return decodedData; + } catch (IOException e) { + throw e; // Catch and release to execute finally{} + } finally { + IOUtils.closeQuietly(bis); + } + } + + /** + * Convenience method for reading a binary file + * and base64-encoding it. + * + *

As of v 2.3, if there is a error, + * the method will throw an IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading binary data + * @return base64-encoded string + * @throws IOException if there is an error + * @since 2.1 + */ + public static String encodeFromFile(final String filename) throws IOException { + Base64.InputStream bis = null; + try { + // Set up some useful variables + File file = new File(filename); + byte[] buffer = new byte[Math.max((int)(file.length() * 1.4+1),40)]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream(new BufferedInputStream(new FileInputStream(file)), Base64.ENCODE); + + // Read until done + while((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } // end while + + return new String(buffer, 0, length, Base64.PREFERRED_ENCODING); + + } catch (IOException e) { + throw e; // Catch and release to execute finally{} + } finally { + IOUtils.closeQuietly(bis); + } + } + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws IOException if there is an error + * @since 2.2 + */ + public static void encodeFileToFile(final String infile, final String outfile) throws IOException { + String encoded = Base64.encodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(outfile)); + out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output. + } catch (IOException e) { + throw e; + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws IOException if there is an error + * @since 2.2 + */ + public static void decodeFileToFile(final String infile, final String outfile) throws IOException { + byte[] decoded = Base64.decodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(outfile)); + out.write(decoded); + } catch (IOException e) { + throw e; // Catch and release to execute finally{} + } finally { + IOUtils.closeQuietly(out); + } + } + + /** Defeats instantiation. */ + private Base64() {} + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + /** + * A {@link Base64.InputStream} will read data from another + * java.io.InputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + + // Fields + private final boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private final byte[] buffer; // Small buffer holding converted data + private final int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private final boolean breakLines; // Break lines at less than 80 characters + private final int options; // Record options used to create the stream. + private final byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the java.io.InputStream from which to read data. + * @since 1.3 + */ + public InputStream(final java.io.InputStream in) { + this(in, DECODE); + } + + /** + * Constructs a {@link Base64.InputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.InputStream(in, Base64.DECODE) + * + * @param in the java.io.InputStream from which to read data. + * @param options Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public InputStream(final java.io.InputStream in, final int options) { + super(in); + this.options = options; // Record for later + this.breakLines = (options & DO_BREAK_LINES) > 0; + this.encode = (options & ENCODE) > 0; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + this.decodabet = getDecodabet(options); + } + + /** + * Reads enough of the input stream to convert + * to/from Base64 and returns the next byte. + * + * @return next byte + * @since 1.3 + */ + @Override + public int read() throws IOException { + + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte)b; + numBinaryBytes++; + } else { + break; // out of for loop + } // end else: end of stream + + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; // Must be end of stream + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { b = in.read(); } + while(b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) { + break; // Reads a -1 if end of stream + } // end if: end of stream + + b4[i] = (byte)b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0, options); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new IOException("Improperly padded Base64 input."); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if (position >= numSigBytes) { + return -1; + } // end if: got data + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) { + position = -1; + } // end if: end + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + throw new IOException("Error in Base64 code reading stream."); + } // end else + } // end read + + /** + * Calls {@link #read()} repeatedly until the end of stream + * is reached or len bytes are read. + * Returns number of bytes read into array or -1 if + * end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + @Override + public int read(final byte[] dest, final int off, final int len) throws IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + if (b >= 0) { + dest[off + i] = (byte) b; + } + else if (i == 0) { + return -1; + } + else { + break; // Out of 'for' loop + } // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + /** + * A {@link Base64.OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private final boolean encode; + private int position; + private byte[] buffer; + private final int bufferLength; + private int lineLength; + private final boolean breakLines; + private final byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private final int options; // Record for later + private final byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 + */ + public OutputStream(final java.io.OutputStream out) { + this(out, ENCODE); + } + + /** + * Constructs a {@link Base64.OutputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.OutputStream(out, Base64.ENCODE) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream(final java.io.OutputStream out, final int options) { + super(out); + this.breakLines = (options & DO_BREAK_LINES) != 0; + this.encode = (options & ENCODE) != 0; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.decodabet = getDecodabet(options); + } + + /** + * Writes the byte to the output stream after converting to/from Base64 + * notation. When encoding, bytes are buffered three at a time before + * the output stream actually gets a write() call. When decoding, bytes + * are buffered four at a time. + * + * @param theByte the byte to write + * @since 1.3 + */ + @Override + public void write(final int theByte) throws IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theByte); + return; + } + + // Encode? + if (encode) { + buffer[position++] = (byte)theByte; + if (position >= bufferLength) { // Enough to encode. + + this.out.write(encode3to4(b4, buffer, bufferLength, options)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + this.out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte)theByte; + if (position >= bufferLength) { + // Enough to output + int len = Base64.decode4to3(buffer, 0, b4, 0, options); + out.write(b4, 0, len); + position = 0; + } + } else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { + // Not white space either + throw new IOException("Invalid character in Base64 data."); + } + } + } + + /** + * Calls {@link #write(int)} repeatedly until len + * bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + @Override + public void write(final byte[] theBytes, final int off, final int len) throws IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + } + + /** + * Pads the buffer without closing the stream. + * + * @throws IOException if there's an error. + */ + public void flushBase64() throws IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position, options)); + position = 0; + } // end if: encoding + else { + throw new IOException("Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + } + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } + + /** + * Suspends encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @throws IOException if there's an error flushing + * @since 1.5.1 + */ + public void suspendEncoding() throws IOException { + flushBase64(); + this.suspendEncoding = true; + } + + /** + * Resumes encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } + } // end inner class OutputStream +} // end class Base64 diff --git a/src/main/java/org/springframework/roo/support/util/ClassUtils.java b/src/main/java/org/springframework/roo/support/util/ClassUtils.java new file mode 100644 index 00000000..c4d4a21c --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/ClassUtils.java @@ -0,0 +1,1027 @@ +package org.springframework.roo.support.util; + +/* + * Copyright 2002-2010 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.beans.Introspector; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Miscellaneous class utility methods. Mainly for internal use within the + * framework; consider + * Apache Commons Lang + * for a more comprehensive suite of class utilities. + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Sam Brannen + * @since 1.1 + * @see org.springframework.util.TypeUtils + * @see ReflectionUtils + */ +@SuppressWarnings("all") +public abstract class ClassUtils { + + /** Suffix for array class names: "[]" */ + public static final String ARRAY_SUFFIX = "[]"; + + /** Prefix for internal array class names: "[" */ + private static final String INTERNAL_ARRAY_PREFIX = "["; + + /** Prefix for internal non-primitive array class names: "[L" */ + private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; + + /** The package separator character '.' */ + private static final char PACKAGE_SEPARATOR = '.'; + + /** The inner class separator character '$' */ + private static final char INNER_CLASS_SEPARATOR = '$'; + + /** The CGLIB class separator character "$$" */ + public static final String CGLIB_CLASS_SEPARATOR = "$$"; + + /** The ".class" file suffix */ + public static final String CLASS_FILE_SUFFIX = ".class"; + + /** + * Map with primitive wrapper type as key and corresponding primitive + * type as value, for example: Integer.class -> int.class. + */ + private static final Map, Class> primitiveWrapperTypeMap = new HashMap, Class>(8); + + /** + * Map with primitive type as key and corresponding wrapper + * type as value, for example: int.class -> Integer.class. + */ + private static final Map, Class> primitiveTypeToWrapperMap = new HashMap, Class>(8); + + /** + * Map with primitive type name as key and corresponding primitive + * type as value, for example: "int" -> "int.class". + */ + private static final Map> primitiveTypeNameMap = new HashMap>(32); + + /** + * Map with common "java.lang" class name as key and corresponding Class as value. + * Primarily for efficient deserialization of remote invocations. + */ + private static final Map> commonClassCache = new HashMap>(32); + + static { + primitiveWrapperTypeMap.put(Boolean.class, boolean.class); + primitiveWrapperTypeMap.put(Byte.class, byte.class); + primitiveWrapperTypeMap.put(Character.class, char.class); + primitiveWrapperTypeMap.put(Double.class, double.class); + primitiveWrapperTypeMap.put(Float.class, float.class); + primitiveWrapperTypeMap.put(Integer.class, int.class); + primitiveWrapperTypeMap.put(Long.class, long.class); + primitiveWrapperTypeMap.put(Short.class, short.class); + + for (Map.Entry, Class> entry : primitiveWrapperTypeMap.entrySet()) { + primitiveTypeToWrapperMap.put(entry.getValue(), entry.getKey()); + registerCommonClasses(entry.getKey()); + } + + Set> primitiveTypes = new HashSet>(32); + primitiveTypes.addAll(primitiveWrapperTypeMap.values()); + primitiveTypes.addAll(Arrays.asList(boolean[].class, byte[].class, char[].class, double[].class, float[].class, int[].class, long[].class, short[].class)); + primitiveTypes.add(void.class); + for (Class primitiveType : primitiveTypes) { + primitiveTypeNameMap.put(primitiveType.getName(), primitiveType); + } + + registerCommonClasses(Boolean[].class, Byte[].class, Character[].class, Double[].class, Float[].class, Integer[].class, Long[].class, Short[].class); + registerCommonClasses(Number.class, Number[].class, String.class, String[].class, Object.class, Object[].class, Class.class, Class[].class); + registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, Error.class, StackTraceElement.class, StackTraceElement[].class); + } + + /** + * Register the given common classes with the ClassUtils cache. + */ + private static void registerCommonClasses(final Class... commonClasses) { + for (Class clazz : commonClasses) { + commonClassCache.put(clazz.getName(), clazz); + } + } + + /** + * Return the default ClassLoader to use: typically the thread context + * ClassLoader, if available; the ClassLoader that loaded the ClassUtils + * class will be used as fallback. + *

Call this method if you intend to use the thread context ClassLoader + * in a scenario where you absolutely need a non-null ClassLoader reference: + * for example, for class path resource loading (but not necessarily for + * Class.forName, which accepts a null ClassLoader + * reference as well). + * @return the default ClassLoader (never null) + * @see java.lang.Thread#getContextClassLoader() + */ + public static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back to system class loader... + } + if (cl == null) { + // No thread context class loader -> use class loader of this class. + cl = ClassUtils.class.getClassLoader(); + } + return cl; + } + + /** + * Override the thread context ClassLoader with the environment's bean ClassLoader + * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread + * context ClassLoader already. + * @param classLoaderToUse the actual ClassLoader to use for the thread context + * @return the original thread context ClassLoader, or null if not overridden + */ + public static ClassLoader overrideThreadContextClassLoader(final ClassLoader classLoaderToUse) { + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); + if (classLoaderToUse != null && !classLoaderToUse.equals(threadContextClassLoader)) { + currentThread.setContextClassLoader(classLoaderToUse); + return threadContextClassLoader; + } + return null; + } + + /** + * Replacement for Class.forName() that also returns Class instances + * for primitives (like "int") and array class names (like "String[]"). + *

Always uses the default class loader: that is, preferably the thread context + * class loader, or the ClassLoader that loaded the ClassUtils class as fallback. + * @param name the name of the Class + * @return Class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + * @see #getDefaultClassLoader() + * @deprecated as of Spring 3.0, in favor of specifying a ClassLoader explicitly: + * see {@link #forName(String, ClassLoader)} + */ + @Deprecated + public static Class forName(final String name) throws ClassNotFoundException, LinkageError { + return forName(name, getDefaultClassLoader()); + } + + /** + * Replacement for Class.forName() that also returns Class instances + * for primitives (e.g."int") and array class names (e.g. "String[]"). + * Furthermore, it is also capable of resolving inner class names in Java source + * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). + * @param name the name of the Class + * @param classLoader the class loader to use + * (may be null, which indicates the default class loader) + * @return Class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + */ + public static Class forName(final String name, final ClassLoader classLoader) throws ClassNotFoundException, LinkageError { + Assert.notNull(name, "Name must not be null"); + + Class clazz = resolvePrimitiveClassName(name); + if (clazz == null) { + clazz = commonClassCache.get(name); + } + if (clazz != null) { + return clazz; + } + + // "java.lang.String[]" style arrays + if (name.endsWith(ARRAY_SUFFIX)) { + String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); + Class elementClass = forName(elementClassName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[Ljava.lang.String;" style arrays + if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { + String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); + Class elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[[I" or "[[Ljava.lang.String;" style arrays + if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { + String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); + Class elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = getDefaultClassLoader(); + } + try { + return classLoaderToUse.loadClass(name); + } catch (ClassNotFoundException ex) { + int lastDotIndex = name.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1); + try { + return classLoaderToUse.loadClass(innerClassName); + } catch (ClassNotFoundException ex2) { + // swallow - let original exception get through + } + } + throw ex; + } + } + + /** + * Resolve the given class name into a Class instance. Supports + * primitives (like "int") and array class names (like "String[]"). + *

This is effectively equivalent to the forName + * method with the same arguments, with the only difference being + * the exceptions thrown in case of class loading failure. + * @param className the name of the Class + * @param classLoader the class loader to use + * (may be null, which indicates the default class loader) + * @return Class instance for the supplied name + * @throws IllegalArgumentException if the class name was not resolvable + * (that is, the class could not be found or the class file could not be loaded) + * @see #forName(String, ClassLoader) + */ + public static Class resolveClassName(final String className, final ClassLoader classLoader) throws IllegalArgumentException { + try { + return forName(className, classLoader); + } catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Cannot find class [" + className + "]", ex); + } catch (LinkageError ex) { + throw new IllegalArgumentException("Error loading class [" + className + "]: problem with class file or dependent class.", ex); + } + } + + /** + * Resolve the given class name as primitive class, if appropriate, + * according to the JVM's naming rules for primitive classes. + *

Also supports the JVM's internal class names for primitive arrays. + * Does not support the "[]" suffix notation for primitive arrays; + * this is only supported by {@link #forName(String, ClassLoader)}. + * @param name the name of the potentially primitive class + * @return the primitive class, or null if the name does not denote + * a primitive class or primitive array class + */ + public static Class resolvePrimitiveClassName(final String name) { + Class result = null; + // Most class names will be quite long, considering that they + // SHOULD sit in a package, so a length check is worthwhile. + if (name != null && name.length() <= 8) { + // Could be a primitive - likely. + result = primitiveTypeNameMap.get(name); + } + return result; + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return false if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @return whether the specified class is present + * @deprecated as of Spring 2.5, in favor of {@link #isPresent(String, ClassLoader)} + */ + @Deprecated + public static boolean isPresent(final String className) { + return isPresent(className, getDefaultClassLoader()); + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return false if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @param classLoader the class loader to use + * (may be null, which indicates the default class loader) + * @return whether the specified class is present + */ + public static boolean isPresent(final String className, final ClassLoader classLoader) { + try { + forName(className, classLoader); + return true; + } catch (Throwable ex) { + // Class or one of its dependencies is not present... + return false; + } + } + + /** + * Return the user-defined class for the given instance: usually simply + * the class of the given instance, but the original class in case of a + * CGLIB-generated subclass. + * @param instance the instance to check + * @return the user-defined class + */ + public static Class getUserClass(final Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getUserClass(instance.getClass()); + } + + /** + * Return the user-defined class for the given class: usually simply the given + * class, but the original class in case of a CGLIB-generated subclass. + * @param clazz the class to check + * @return the user-defined class + */ + public static Class getUserClass(final Class clazz) { + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) { + return superClass; + } + } + return clazz; + } + + /** + * Check whether the given class is cache-safe in the given context, + * i.e. whether it is loaded by the given ClassLoader or a parent of it. + * @param clazz the class to analyze + * @param classLoader the ClassLoader to potentially cache metadata in + */ + public static boolean isCacheSafe(final Class clazz, final ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + ClassLoader target = clazz.getClassLoader(); + if (target == null) { + return false; + } + ClassLoader cur = classLoader; + if (cur == target) { + return true; + } + while (cur != null) { + cur = cur.getParent(); + if (cur == target) { + return true; + } + } + return false; + } + + /** + * Get the class name without the qualified package name. + * @param className the className to get the short name for + * @return the class name of the class without the package name + * @throws IllegalArgumentException if the className is empty + */ + public static String getShortName(final String className) { + Assert.hasLength(className, "Class name must not be empty"); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR); + if (nameEndIndex == -1) { + nameEndIndex = className.length(); + } + String shortName = className.substring(lastDotIndex + 1, nameEndIndex); + shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR); + return shortName; + } + + /** + * Get the class name without the qualified package name. + * @param clazz the class to get the short name for + * @return the class name of the class without the package name + */ + public static String getShortName(final Class clazz) { + return getShortName(getQualifiedName(clazz)); + } + + /** + * Return the short string name of a Java class in uncapitalized JavaBeans + * property format. Strips the outer class name in case of an inner class. + * @param clazz the class + * @return the short name rendered in a standard JavaBeans property format + * @see java.beans.Introspector#decapitalize(String) + */ + public static String getShortNameAsProperty(final Class clazz) { + String shortName = ClassUtils.getShortName(clazz); + int dotIndex = shortName.lastIndexOf('.'); + shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName); + return Introspector.decapitalize(shortName); + } + + /** + * Determine the name of the class file, relative to the containing + * package: e.g. "String.class" + * @param clazz the class + * @return the file name of the ".class" file + */ + public static String getClassFileName(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX; + } + + /** + * Determine the name of the package of the given class: + * e.g. "java.lang" for the java.lang.String class. + * @param clazz the class + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return (lastDotIndex != -1 ? className.substring(0, lastDotIndex) : ""); + } + + /** + * Return the qualified name of the given class: usually simply + * the class name, but component type class name + "[]" for arrays. + * @param clazz the class + * @return the qualified name of the class + */ + public static String getQualifiedName(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isArray()) { + return getQualifiedNameForArray(clazz); + } else { + return clazz.getName(); + } + } + + /** + * Build a nice qualified name for an array: + * component type class name + "[]". + * @param clazz the array class + * @return a qualified name for the array class + */ + private static String getQualifiedNameForArray(Class clazz) { + StringBuilder result = new StringBuilder(); + while (clazz.isArray()) { + clazz = clazz.getComponentType(); + result.append(ClassUtils.ARRAY_SUFFIX); + } + result.insert(0, clazz.getName()); + return result.toString(); + } + + /** + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @return the qualified name of the method + */ + public static String getQualifiedMethodName(final Method method) { + Assert.notNull(method, "Method must not be null"); + return method.getDeclaringClass().getName() + "." + method.getName(); + } + + /** + * Return a descriptive name for the given object's type: usually simply + * the class name, but component type class name + "[]" for arrays, + * and an appended list of implemented interfaces for JDK proxies. + * @param value the value to introspect + * @return the qualified name of the class + */ + public static String getDescriptiveType(final Object value) { + if (value == null) { + return null; + } + Class clazz = value.getClass(); + if (Proxy.isProxyClass(clazz)) { + StringBuilder result = new StringBuilder(clazz.getName()); + result.append(" implementing "); + Class[] ifcs = clazz.getInterfaces(); + for (int i = 0; i < ifcs.length; i++) { + result.append(ifcs[i].getName()); + if (i < ifcs.length - 1) { + result.append(','); + } + } + return result.toString(); + } else if (clazz.isArray()) { + return getQualifiedNameForArray(clazz); + } else { + return clazz.getName(); + } + } + + /** + * Check whether the given class matches the user-specified type name. + * @param clazz the class to check + * @param typeName the type name to match + */ + public static boolean matchesTypeName(final Class clazz, final String typeName) { + return (typeName != null && + (typeName.equals(clazz.getName()) || typeName.equals(clazz.getSimpleName()) || + (clazz.isArray() && typeName.equals(getQualifiedNameForArray(clazz))))); + } + + /** + * Determine whether the given class has a public constructor with the given signature. + *

Essentially translates NoSuchMethodException to "false". + * @param clazz the clazz to analyze + * @param parameterTypes the parameter types of the method + * @return whether the class has a corresponding constructor + * @see java.lang.Class#getMethod + */ + public static boolean hasConstructor(final Class clazz, final Class... parameterTypes) { + return (getConstructorIfAvailable(clazz, parameterTypes) != null); + } + + /** + * Determine whether the given class has a public constructor with the given signature, + * and return it if available (else return null). + *

Essentially translates NoSuchMethodException to null. + * @param clazz the clazz to analyze + * @param parameterTypes the parameter types of the method + * @return the constructor, or null if not found + * @see java.lang.Class#getConstructor + */ + public static Constructor getConstructorIfAvailable(final Class clazz, final Class... parameterTypes) { + Assert.notNull(clazz, "Class must not be null"); + try { + return clazz.getConstructor(parameterTypes); + } catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Determine whether the given class has a method with the given signature. + *

Essentially translates NoSuchMethodException to "false". + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param parameterTypes the parameter types of the method + * @return whether the class has a corresponding method + * @see java.lang.Class#getMethod + */ + public static boolean hasMethod(final Class clazz, final String methodName, final Class... parameterTypes) { + return (getMethodIfAvailable(clazz, methodName, parameterTypes) != null); + } + + /** + * Determine whether the given class has a method with the given signature, + * and return it if available (else return null). + *

Essentially translates NoSuchMethodException to null. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param parameterTypes the parameter types of the method + * @return the method, or null if not found + * @see java.lang.Class#getMethod + */ + public static Method getMethodIfAvailable(final Class clazz, final String methodName, final Class... parameterTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + return clazz.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Return the number of methods with a given name (with any argument types), + * for the given class and/or its superclasses. Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return the number of methods with the given name + */ + public static int getMethodCountForName(final Class clazz, final String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + int count = 0; + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (methodName.equals(method.getName())) { + count++; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + count += getMethodCountForName(ifc, methodName); + } + if (clazz.getSuperclass() != null) { + count += getMethodCountForName(clazz.getSuperclass(), methodName); + } + return count; + } + + /** + * Does the given class or one of its superclasses at least have one or more + * methods with the supplied name (with any argument types)? + * Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return whether there is at least one method with the given name + */ + public static boolean hasAtLeastOneMethodWithName(final Class clazz, final String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (method.getName().equals(methodName)) { + return true; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + if (hasAtLeastOneMethodWithName(ifc, methodName)) { + return true; + } + } + return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName)); + } + + /** + * Given a method, which may come from an interface, and a target class used + * in the current reflective invocation, find the corresponding target method + * if there is one. E.g. the method may be IFoo.bar() and the + * target class may be DefaultFoo. In this case, the method may be + * DefaultFoo.bar(). This enables attributes on that method to be found. + *

NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, + * this method does not resolve Java 5 bridge methods automatically. + * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} + * if bridge method resolution is desirable (e.g. for obtaining metadata from + * the original method definition). + * @param method the method to be invoked, which may come from an interface + * @param targetClass the target class for the current invocation. + * May be null or may not even implement the method. + * @return the specific target method, or the original method if the + * targetClass doesn't implement it or is null + */ + public static Method getMostSpecificMethod(final Method method, final Class targetClass) { + Method specificMethod = null; + if (method != null && isOverridable(method, targetClass) && targetClass != null && !targetClass.equals(method.getDeclaringClass())) { + specificMethod = ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes()); + } + return (specificMethod != null ? specificMethod : method); + } + + /** + * Determine whether the given method is overridable in the given target class. + * @param method the method to check + * @param targetClass the target class to check against + */ + private static boolean isOverridable(final Method method, final Class targetClass) { + if (Modifier.isPrivate(method.getModifiers())) { + return false; + } + if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + return true; + } + return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass)); + } + + /** + * Return a public static method of a class. + * @param methodName the static method name + * @param clazz the class which defines the method + * @param args the parameter types to the method + * @return the static method, or null if no static method was found + * @throws IllegalArgumentException if the method name is blank or the clazz is null + */ + public static Method getStaticMethod(final Class clazz, final String methodName, final Class... args) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + Method method = clazz.getMethod(methodName, args); + return Modifier.isStatic(method.getModifiers()) ? method : null; + } catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Check if the given class represents a primitive wrapper, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper class + */ + public static boolean isPrimitiveWrapper(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return primitiveWrapperTypeMap.containsKey(clazz); + } + + /** + * Check if the given class represents a primitive (i.e. boolean, byte, + * char, short, int, long, float, or double) or a primitive wrapper + * (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double). + * @param clazz the class to check + * @return whether the given class is a primitive or primitive wrapper class + */ + public static boolean isPrimitiveOrWrapper(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); + } + + /** + * Check if the given class represents an array of primitives, + * i.e. boolean, byte, char, short, int, long, float, or double. + * @param clazz the class to check + * @return whether the given class is a primitive array class + */ + public static boolean isPrimitiveArray(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && clazz.getComponentType().isPrimitive()); + } + + /** + * Check if the given class represents an array of primitive wrappers, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper array class + */ + public static boolean isPrimitiveWrapperArray(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); + } + + /** + * Resolve the given class if it is a primitive class, + * returning the corresponding primitive wrapper type instead. + * @param clazz the class to check + * @return the original class, or a primitive wrapper for the original primitive type + */ + public static Class resolvePrimitiveIfNecessary(final Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() && clazz != void.class? primitiveTypeToWrapperMap.get(clazz) : clazz); + } + + /** + * Check if the right-hand side type may be assigned to the left-hand side + * type, assuming setting by reflection. Considers primitive wrapper + * classes as assignable to the corresponding primitive types. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return if the target type is assignable from the value type + * @see org.springframework.util.TypeUtils#isAssignable + */ + public static boolean isAssignable(final Class lhsType, final Class rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + return (lhsType.isAssignableFrom(rhsType) || lhsType.equals(primitiveWrapperTypeMap.get(rhsType))); + } + + /** + * Determine if the given type is assignable from the given value, + * assuming setting by reflection. Considers primitive wrapper classes + * as assignable to the corresponding primitive types. + * @param type the target type + * @param value the value that should be assigned to the type + * @return if the type is assignable from the value + */ + public static boolean isAssignableValue(final Class type, final Object value) { + Assert.notNull(type, "Type must not be null"); + return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive()); + } + + /** + * Convert a "/"-based resource path to a "."-based fully qualified class name. + * @param resourcePath the resource path pointing to a class + * @return the corresponding fully qualified class name + */ + public static String convertResourcePathToClassName(final String resourcePath) { + Assert.notNull(resourcePath, "Resource path must not be null"); + return resourcePath.replace('/', '.'); + } + + /** + * Convert a "."-based fully qualified class name to a "/"-based resource path. + * @param className the fully qualified class name + * @return the corresponding resource path, pointing to the class + */ + public static String convertClassNameToResourcePath(final String className) { + Assert.notNull(className, "Class name must not be null"); + return className.replace('.', '/'); + } + + /** + * Return a path suitable for use with ClassLoader.getResource + * (also suitable for use with Class.getResource by prepending a + * slash ('/') to the return value). Built by taking the package of the specified + * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash + * if necessary, and concatenating the specified resource name to this. + *
As such, this function may be used to build a path suitable for + * loading a resource file that is in the same package as a class file, + * although {@link org.springframework.core.io.ClassPathResource} is usually + * even more convenient. + * @param clazz the Class whose package will be used as the base + * @param resourceName the resource name to append. A leading slash is optional. + * @return the built-up resource path + * @see java.lang.ClassLoader#getResource + * @see java.lang.Class#getResource + */ + public static String addResourcePathToPackagePath(final Class clazz, final String resourceName) { + Assert.notNull(resourceName, "Resource name must not be null"); + if (!resourceName.startsWith("/")) { + return classPackageAsResourcePath(clazz) + "/" + resourceName; + } + return classPackageAsResourcePath(clazz) + resourceName; + } + + /** + * Given an input class object, return a string which consists of the + * class's package name as a pathname, i.e., all dots ('.') are replaced by + * slashes ('/'). Neither a leading nor trailing slash is added. The result + * could be concatenated with a slash and the name of a resource and fed + * directly to ClassLoader.getResource(). For it to be fed to + * Class.getResource instead, a leading slash would also have + * to be prepended to the returned value. + * @param clazz the input class. A null value or the default + * (empty) package will result in an empty string ("") being returned. + * @return a path which represents the package name + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String classPackageAsResourcePath(final Class clazz) { + if (clazz == null) { + return ""; + } + String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf('.'); + if (packageEndIndex == -1) { + return ""; + } + String packageName = className.substring(0, packageEndIndex); + return packageName.replace('.', '/'); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given array. + *

Basically like AbstractCollection.toString(), but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be null) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(final Class... classes) { + return classNamesToString(Arrays.asList(classes)); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given collection. + *

Basically like AbstractCollection.toString(), but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be null) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(final Collection> classes) { + if (CollectionUtils.isEmpty(classes)) { + return "[]"; + } + StringBuilder sb = new StringBuilder("["); + for (Iterator> it = classes.iterator(); it.hasNext();) { + Class clazz = it.next(); + sb.append(clazz.getName()); + if (it.hasNext()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + + /** + * Return all interfaces that the given instance implements as array, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as array + */ + public static Class[] getAllInterfaces(final Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClass(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as array, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as array + */ + public static Class[] getAllInterfacesForClass(final Class clazz) { + return getAllInterfacesForClass(clazz, null); + } + + /** + * Return all interfaces that the given class implements as array, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be null when accepting all declared interfaces) + * @return all interfaces that the given object implements as array + */ + public static Class[] getAllInterfacesForClass(final Class clazz, final ClassLoader classLoader) { + Set ifcs = getAllInterfacesForClassAsSet(clazz, classLoader); + return ifcs.toArray(new Class[ifcs.size()]); + } + + /** + * Return all interfaces that the given instance implements as Set, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as Set + */ + public static Set getAllInterfacesAsSet(final Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClassAsSet(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as Set, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as Set + */ + public static Set getAllInterfacesForClassAsSet(final Class clazz) { + return getAllInterfacesForClassAsSet(clazz, null); + } + + /** + * Return all interfaces that the given class implements as Set, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be null when accepting all declared interfaces) + * @return all interfaces that the given object implements as Set + */ + public static Set getAllInterfacesForClassAsSet(Class clazz, final ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface() && isVisible(clazz, classLoader)) { + return Collections.singleton(clazz); + } + Set interfaces = new LinkedHashSet(); + while (clazz != null) { + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + interfaces.addAll(getAllInterfacesForClassAsSet(ifc, classLoader)); + } + clazz = clazz.getSuperclass(); + } + return interfaces; + } + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

This implementation builds a JDK proxy class for the given interfaces. + * @param interfaces the interfaces to merge + * @param classLoader the ClassLoader to create the composite Class in + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + public static Class createCompositeInterface(final Class[] interfaces, final ClassLoader classLoader) { + Assert.notEmpty(interfaces, "Interfaces must not be empty"); + Assert.notNull(classLoader, "ClassLoader must not be null"); + return Proxy.getProxyClass(classLoader, interfaces); + } + + /** + * Check whether the given class is visible in the given ClassLoader. + * @param clazz the class to check (typically an interface) + * @param classLoader the ClassLoader to check against (may be null, + * in which case this method will always return true) + */ + public static boolean isVisible(final Class clazz, final ClassLoader classLoader) { + if (classLoader == null) { + return true; + } + try { + Class actualClass = classLoader.loadClass(clazz.getName()); + return (clazz == actualClass); + // Else: different interface class found... + } catch (ClassNotFoundException ex) { + // No interface class found... + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/roo/support/util/CollectionUtils.java b/src/main/java/org/springframework/roo/support/util/CollectionUtils.java new file mode 100644 index 00000000..af2a4588 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/CollectionUtils.java @@ -0,0 +1,367 @@ +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.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Miscellaneous collection utility methods. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Andrew Swan + * @since 1.1.3 + */ +public final class CollectionUtils { + + /** + * 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()); + } + + /** + * 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)); + } + + /** + * Merge the given array into the given Collection. + * + * @param array the array to merge (may be null) + * @param collection the target Collection to merge the array into + */ + public static void mergeArrayIntoCollection(final Object array, final Collection collection) { + if (collection == null) { + throw new IllegalArgumentException("Collection must not be null"); + } + final Object[] arr = ObjectUtils.toObjectArray(array); + for (final Object elem : arr) { + collection.add(elem); + } + } + + /** + * Merge the given Properties instance into the given Map, + * copying all properties (key-value pairs) over. + *

Uses Properties.propertyNames() to even catch + * default properties linked into the original Properties instance. + * + * @param props the Properties instance to merge (may be null) + * @param map the target Map to merge the properties into + */ + public static void mergePropertiesIntoMap(final Properties props, final Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (props != null) { + for (final Enumeration en = props.propertyNames(); en.hasMoreElements();) { + final String key = (String) en.nextElement(); + map.put(key, props.getProperty(key)); + } + } + } + + /** + * Check whether the given Iterator contains the given element. + * + * @param iterator the Iterator to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(final Iterator iterator, final Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + final Object candidate = iterator.next(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(final Enumeration enumeration, final Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + final Object candidate = enumeration.nextElement(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance. + *

Enforces the given instance to be present, rather than returning + * true for an equal element as well. + * + * @param collection the Collection to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean containsInstance(final Collection collection, final Object element) { + if (collection != null) { + for (final Object candidate : collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return true if any element in 'candidates' is + * contained in 'source'; otherwise returns false. + * + * @param source the source Collection + * @param candidates the candidates to search for + * @return whether any of the candidates has been found + */ + public static boolean containsAny(final Collection source, final Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (final Object candidate : candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; + } + + /** + * Return the first element in 'candidates' that is contained in + * 'source'. If no element in 'candidates' is present in + * 'source' returns null. Iteration order is + * {@link Collection} implementation specific. + * + * @param source the source Collection + * @param candidates the candidates to search for + * @return the first present object, or null if not found + */ + public static Object findFirstMatch(final Collection source, final Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (final Object candidate : candidates) { + if (source.contains(candidate)) { + return candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * + * @param collection the Collection to search + * @param type the type to look for + * @return a value of the given type found if there is a clear match, + * or null if none or more than one such value found + */ + @SuppressWarnings("unchecked") + public static T findValueOfType(final Collection collection, final Class type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (final Object element : collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: + * searching the Collection for a value of the first type, then + * searching for a value of the second type, etc. + * + * @param collection the collection to search + * @param types the types to look for, in prioritized order + * @return a value of one of the given types found if there is a clear match, + * or null if none or more than one such value found + */ + public static Object findValueOfType(final Collection collection, final Class... types) { + if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { + return null; + } + for (final Class type : types) { + final Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * + * @param collection the Collection to check + * @return true if the collection contains a single reference or + * multiple references to the same instance, false else + */ + public static boolean hasUniqueObject(final Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (final Iterator it = collection.iterator(); it.hasNext();) { + final Object elem = it.next(); + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } + else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * 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; + } + + /** + * Adds the given items to the given collection + * + * @param the type of item in the collection being updated + * @param newItems the items being added; can be null for none + * @param existingItems the items being added to; must be modifiable + * @return true if the existing collection was modified + * @throws UnsupportedOperationException if there are items to add and the + * existing collection is not modifiable + * @since 1.2.0 + */ + public static boolean addAll(final Collection newItems, final Collection existingItems) { + if (existingItems != null && newItems != null) { + return existingItems.addAll(newItems); + } + return false; + } + + /** + * 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; + } + + /** + * 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(); + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private CollectionUtils() {} +} \ No newline at end of file diff --git a/src/main/java/org/springframework/roo/support/util/DomUtils.java b/src/main/java/org/springframework/roo/support/util/DomUtils.java new file mode 100644 index 00000000..af8b4649 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/DomUtils.java @@ -0,0 +1,295 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +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 { + + /** + * 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) { + Assert.notNull(element, "Element must not be null"); + Assert.notNull(childElementNames, "Element names collection must not be null"); + List childEleNameList = Arrays.asList(childElementNames); + NodeList nl = element.getChildNodes(); + List childEles = new ArrayList(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childEleNameList)) { + childEles.add((Element) node); + } + } + return childEles; + } + + /** + * 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}); + } + + /** + * 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) { + Assert.notNull(element, "Element must not be null"); + Assert.notNull(childElementName, "Element name must not be null"); + NodeList nl = element.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childElementName)) { + return (Element) node; + } + } + return null; + } + + /** + * 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) { + Element child = getChildElementByTagName(element, childElementName); + return (child != null ? getTextValue(child) : null); + } + + /** + * 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) { + Assert.notNull(valueElement, "Element must not be null"); + StringBuilder sb = new StringBuilder(); + NodeList nl = valueElement.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + 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) { + Assert.notNull(node, "Node must not be null"); + Assert.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 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())); + } + + /** + * 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())); + } + + /** + * 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; + } + } + } + + /** + * 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(); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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) { + Assert.hasText(name, "Element name required"); + Assert.notNull(root, "Root element required"); + return (Element) root.getElementsByTagName(name).item(0); + } + + /** + * 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); + } + } + + /** + * Constructor is private to prevent instantiation + */ + private DomUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/ExceptionUtils.java b/src/main/java/org/springframework/roo/support/util/ExceptionUtils.java new file mode 100644 index 00000000..49470753 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/ExceptionUtils.java @@ -0,0 +1,25 @@ +package org.springframework.roo.support.util; + +/** + * Methods for working with exceptions. + * + * @author Ben Alex + * @since 1.0 + */ +public abstract class ExceptionUtils { + + /** + * Obtains the root cause of an exception, if available. + * + * @param ex to extract the root cause from (required) + * @return the root cause, or original exception is unavailable (guaranteed to never be null) + */ + public final static Throwable extractRootCause(final Throwable ex) { + Assert.notNull(ex, "An exception is required"); + Throwable root = ex; + if (ex.getCause() != null) { + root = ex.getCause(); + } + return root; + } +} diff --git a/src/main/java/org/springframework/roo/support/util/FileCopyUtils.java b/src/main/java/org/springframework/roo/support/util/FileCopyUtils.java new file mode 100644 index 00000000..d4c1bc0a --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/FileCopyUtils.java @@ -0,0 +1,231 @@ +package org.springframework.roo.support.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; + +/** + * Simple utility methods for file and stream copying. All copy methods use a block size of 4096 bytes, and close all affected streams when done. + * + *

+ * Mainly for use within the framework, but also useful for application code. + * + * @author Juergen Hoeller + * @since 1.0 + */ +public final class FileCopyUtils { + + public static final int BUFFER_SIZE = 4096; + + // --------------------------------------------------------------------- + // Copy methods for java.io.File + // --------------------------------------------------------------------- + + /** + * Copy the contents of the given input File to the given output File. + * + * @param in the file to copy from + * @param out the file to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(final File in, final File out) throws IOException { + Assert.notNull(in, "No input File specified"); + Assert.notNull(out, "No output File specified"); + return copy(new BufferedInputStream(new FileInputStream(in)), new BufferedOutputStream(new FileOutputStream(out))); + } + + /** + * Copy the contents of the given byte array to the given output File. + * + * @param bytes the byte array to copy from + * @param file the file to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(final byte[] bytes, final File file) throws IOException { + Assert.notNull(bytes, "No input byte array specified"); + Assert.notNull(file, "No output File specified"); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); + copy(in, out); + } + + /** + * Copy the contents of the given input File into a new byte array. + * + * @param in the file to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(final File in) throws IOException { + Assert.notNull(in, "No input File specified"); + return copyToByteArray(new BufferedInputStream(new FileInputStream(in))); + } + + // --------------------------------------------------------------------- + // Copy methods for java.io.InputStream / java.io.OutputStream + // --------------------------------------------------------------------- + + /** + * Copy the contents of the given InputStream to the given OutputStream. Closes both streams when done. + * + * @param in the stream to copy from + * @param out the stream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + if (!(in instanceof BufferedInputStream)) { + in = new BufferedInputStream(in); + } + if (!(out instanceof BufferedOutputStream)) { + out = new BufferedOutputStream(out); + } + try { + int byteCount = 0; + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } finally { + IOUtils.closeQuietly(in, out); + } + } + + /** + * Copy the contents of the given byte array to the given OutputStream. Closes the stream when done. + * + * @param bytes the byte array to copy from + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(final byte[] bytes, final OutputStream out) throws IOException { + Assert.notNull(bytes, "No input byte array specified"); + Assert.notNull(out, "No OutputStream specified"); + try { + out.write(bytes); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Copy the contents of the given InputStream into a new byte array. Closes the stream when done. + * + * @param in the stream to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(final InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } + + // --------------------------------------------------------------------- + // Copy methods for java.io.Reader / java.io.Writer + // --------------------------------------------------------------------- + + /** + * Copy the contents of the given Reader to the given Writer. Closes both when done. + * + * @param in the Reader to copy from + * @param out the Writer to copy to + * @return the number of characters copied + * @throws IOException in case of I/O errors + */ + public static int copy(Reader in, Writer out) throws IOException { + Assert.notNull(in, "No Reader specified"); + Assert.notNull(out, "No Writer specified"); + if (!(in instanceof BufferedReader)) { + in = new BufferedReader(in); + } + if (!(out instanceof BufferedWriter)) { + out = new BufferedWriter(out); + } + try { + int byteCount = 0; + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } finally { + IOUtils.closeQuietly(in, out); + } + } + + /** + * Copy the contents of the given String to the given output Writer. Closes the writer when done. + * + * @param in the String to copy from + * @param out the Writer to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(final String in, Writer out) throws IOException { + Assert.notNull(in, "No input String specified"); + Assert.notNull(out, "No Writer specified"); + if (!(out instanceof BufferedWriter)) { + out = new BufferedWriter(out); + } + try { + out.write(in); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Copy the contents of the given Reader into a String. Closes the reader when done. + * + * @param in the reader to copy from + * @return the String that has been copied to + * @throws IOException in case of I/O errors + */ + public static String copyToString(final Reader in) throws IOException { + StringWriter out = new StringWriter(); + copy(in, out); + return out.toString(); + } + + /** + * Returns the contents of the given File as a String. + *

+ * Consider using {@link FileUtils#read(File)} instead if any + * {@link IOException}s would be unrecoverable. + * + * @param file the file to read from + * @return the contents + * @throws IOException in case of I/O errors + */ + public static String copyToString(final File file) throws IOException { + return copyToString(new BufferedReader(new FileReader(file))); + } + + /** + * Constructor is private to prevent instantiation + */ + private FileCopyUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/FileUtils.java b/src/main/java/org/springframework/roo/support/util/FileUtils.java new file mode 100644 index 00000000..e7966223 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/FileUtils.java @@ -0,0 +1,357 @@ +package org.springframework.roo.support.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Pattern; + +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 { + + // Constants + private static final String BACKSLASH = "\\"; + private static final String ESCAPED_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 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 static final PathMatcher PATH_MATCHER; + + static { + PATH_MATCHER = new AntPathMatcher(); + ((AntPathMatcher) PATH_MATCHER).setPathSeparator(File.separator); + } + + /** + * Deletes the specified {@link File}. + * + *

+ * If the {@link File} refers to a directory, any contents of that directory (including other directories) + * are also deleted. + * + *

+ * If the {@link File} does not already exist, this method immediately returns true. + * + * @param file to delete (required; the file may or may not exist) + * @return true if the file is fully deleted, or false if there was a failure when deleting + */ + public static boolean deleteRecursively(final File file) { + Assert.notNull(file, "File to delete required"); + if (!file.exists()) { + return true; + } + if (file.isDirectory()) { + for (File f : file.listFiles()) { + if (!deleteRecursively(f)) { + return false; + } + } + } + file.delete(); + return true; + } + + /** + * 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) { + Assert.notNull(source, "Source directory required"); + Assert.notNull(destination, "Destination directory required"); + Assert.isTrue(source.exists(), "Source directory '" + source + "' must exist"); + Assert.isTrue(source.isDirectory(), "Source directory '" + source + "' must be a directory"); + if (destination.exists()) { + Assert.isTrue(destination.isDirectory(), "Destination directory '" + destination + "' must be a directory"); + } else { + destination.mkdirs(); + if (deleteDestinationOnExit) { + destination.deleteOnExit(); + } + } + for (File s : source.listFiles()) { + File d = new File(destination, s.getName()); + if (deleteDestinationOnExit) { + d.deleteOnExit(); + } + if (s.isFile()) { + try { + FileCopyUtils.copy(s, d); + } catch (IOException ioe) { + return false; + } + } else { + // It's a sub-directory, so copy it + d.mkdir(); + if (!copyRecursively(s, d, deleteDestinationOnExit)) { + return false; + } + } + } + return true; + } + + /** + * 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 + */ + public static boolean denotesAbsolutePath(final String fileName) { + if (OsUtils.isWindows()) { + // first check for drive letter + if (WINDOWS_DRIVE_PATH.matcher(fileName).matches()) { + return true; + } + } + return fileName.startsWith(File.separator); + } + + /** + * 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 = removeTrailingSeparator(fileIdentifier); + if (new File(fileIdentifier).isDirectory()) { + return fileIdentifier; + } + return backOneDirectory(fileIdentifier); + } + + /** + * Returns the given file system path minus its last element + * + * @param fileIdentifier + * @return + * @since 1.2.0 + */ + public static String backOneDirectory(String fileIdentifier) { + fileIdentifier = removeTrailingSeparator(fileIdentifier); + fileIdentifier = fileIdentifier.substring(0, fileIdentifier.lastIndexOf(File.separator)); + return removeTrailingSeparator(fileIdentifier); + } + + /** + * Removes any trailing {@link File#separator}s from the given path + * + * @param path the path to modify (can be null) + * @return the modified path + * @since 1.2.0 + */ + public static String removeTrailingSeparator(String path) { + while (path != null && path.endsWith(File.separator)) { + path = StringUtils.removeSuffix(path, File.separator); + } + return path; + } + + /** + * 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) { + Assert.hasText(antPattern, "Ant pattern required"); + Assert.hasText(canonicalPath, "Canonical path required"); + return PATH_MATCHER.match(antPattern, canonicalPath); + } + + /** + * Removes any leading or trailing {@link File#separator}s from the given path. + * + * @param path the path to modify (can be null) + * @return the path, modified as above, or null if null was given + * @since 1.2.0 + */ + public static String removeLeadingAndTrailingSeparators(String path) { + if (StringUtils.isBlank(path)) { + return path; + } + while (path.endsWith(File.separator)) { + path = StringUtils.removeSuffix(path, File.separator); + } + while (path.startsWith(File.separator)) { + path = StringUtils.removePrefix(path, File.separator); + } + return path; + } + + /** + * 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) { + Assert.notNull(path); + return removeTrailingSeparator(path) + File.separatorChar; + } + + /** + * 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) { + return getSystemDependentPath(Arrays.asList(pathElements)); + } + + /** + * 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) { + Assert.notEmpty(pathElements); + return StringUtils.collectionToDelimitedString(pathElements, File.separator); + } + + /** + * 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; + } + + /** + * 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) { + Assert.notNull(loadingClass, "Loading class required"); + Assert.hasText(relativeFilename, "Filename required"); + Assert.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; + } + + /** + * 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 IllegalArgumentException if the given file cannot be found + */ + public static File getFile(final Class loadingClass, final String filename) { + final URL url = loadingClass.getResource(filename); + Assert.notNull(url, "Could not locate '" + filename + "' in classpath of " + loadingClass.getName()); + try { + return new File(url.toURI()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 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 IllegalArgumentException if the given file cannot be found + */ + public static InputStream getInputStream(final Class loadingClass, final String filename) { + final InputStream inputStream = loadingClass.getResourceAsStream(filename); + Assert.notNull(inputStream, "Could not locate '" + filename + "' in classpath of " + loadingClass.getName()); + return inputStream; + } + + /** + * Returns the contents of the given File as a String. + * + * @param file the file to read from (must be an existing file) + * @return the contents + * @throws IllegalStateException in case of I/O errors + * @since 1.2.0 + */ + public static String read(final File file) { + try { + return FileCopyUtils.copyToString(file); + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * Constructor is private to prevent instantiation + * + * @since 1.2.0 + */ + private FileUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/Filter.java b/src/main/java/org/springframework/roo/support/util/Filter.java new file mode 100644 index 00000000..05de67c1 --- /dev/null +++ b/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/src/main/java/org/springframework/roo/support/util/HexUtils.java b/src/main/java/org/springframework/roo/support/util/HexUtils.java new file mode 100644 index 00000000..a8def105 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/HexUtils.java @@ -0,0 +1,41 @@ +package org.springframework.roo.support.util; + +/** + * Encodes a given byte array as hex. + * + *

+ * Most methods in this class were obtained from the Spring Security class, + * org.springframework.security.core.codec.Hex. Spring Security is licensed + * under the Apache Software License version 2.0 and the following code is used + * pursuant to that license. + * + * @author Luke Taylor + * @author Ben Alex + * @since 1.1.1 + */ +public abstract class HexUtils { + + public static String toHex(final byte[] bytes) { + return new String(encode(bytes)); + } + + private static final char[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + public static char[] encode(final byte[] bytes) { + final int nBytes = bytes.length; + char[] result = new char[2 * nBytes]; + int j = 0; + + for (int i = 0; i < nBytes; i++) { + // Char for top 4 bits + result[j++] = HEX[(0xF0 & bytes[i]) >>> 4]; + + // Bottom 4 + result[j++] = HEX[(0x0F & bytes[i])]; + } + + return result; + } +} diff --git a/src/main/java/org/springframework/roo/support/util/IOUtils.java b/src/main/java/org/springframework/roo/support/util/IOUtils.java new file mode 100644 index 00000000..1548f2a9 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/IOUtils.java @@ -0,0 +1,58 @@ +package org.springframework.roo.support.util; + +import java.io.Closeable; +import java.io.IOException; +import java.util.zip.ZipFile; + +/** + * Static helper methods relating to I/O. Inspired by the eponymous class in + * Apache Commons I/O. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class IOUtils { + + /** + * Quietly closes each of the given {@link Closeable}s, i.e. eats any + * {@link IOException}s arising. + * + * @param closeables the closeables to close (any of which can be + * null or already closed) + */ + public static void closeQuietly(final Closeable... closeables) { + for (final Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + /** + * Quietly closes each of the given {@link ZipFile}s, i.e. eats any + * {@link IOException}s arising. + * + * @param zipFiles the zipFiles to close (any of which can be + * null or already closed) + */ + public static void closeQuietly(final ZipFile... zipFiles) { + for (final ZipFile zipFile : zipFiles) { + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + /** + * Constructor is private to prevent instantiation + */ + private IOUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/MathUtils.java b/src/main/java/org/springframework/roo/support/util/MathUtils.java new file mode 100644 index 00000000..af2ab44d --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/MathUtils.java @@ -0,0 +1,16 @@ +package org.springframework.roo.support.util; + +/** + * A class which contains a number of number manipulation operations + * + * @author James Tyrrell + * @since 1.2.0 + */ +public class MathUtils { + + public static double round(final double valueToRound, final int numberOfDecimalPlaces) { + double multiplicationFactor = Math.pow(10, numberOfDecimalPlaces); + double interestedInZeroDPs = valueToRound * multiplicationFactor; + return Math.round(interestedInZeroDPs) / multiplicationFactor; + } +} diff --git a/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java b/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java new file mode 100644 index 00000000..0f8d9f37 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/MessageDisplayUtils.java @@ -0,0 +1,67 @@ +package org.springframework.roo.support.util; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.logging.Level; +import java.util.logging.Logger; + +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 { + + // Constants + private static Logger LOGGER = HandlerUtils.getLogger(MessageDisplayUtils.class); + + /** + * 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) { + Level level = important ? Level.SEVERE : Level.FINE; + String owningPackage = owner.getPackage().getName().replace('.', '/'); + String fullResourceName = "/" + owningPackage + "/" + fileName; + InputStream inputStream = owner.getClassLoader().getResourceAsStream(fullResourceName); + if (inputStream == null) { + throw new IllegalStateException("Could not locate '" + fileName + "'"); + } + try { + String message = FileCopyUtils.copyToString(new InputStreamReader(new BufferedInputStream(inputStream))); + LOGGER.log(level, message); + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + /** + * 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); + } +} diff --git a/src/main/java/org/springframework/roo/support/util/NumberUtils.java b/src/main/java/org/springframework/roo/support/util/NumberUtils.java new file mode 100644 index 00000000..fa7d9363 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/NumberUtils.java @@ -0,0 +1,81 @@ +package org.springframework.roo.support.util; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Provides extra functionality for Java Number classes. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public final class NumberUtils { + + /** + * Returns the minimum value in the array. + * + * @param array an array of Numbers (can be null) + * @return the minimum value in the array, or null if all the elements are null + */ + public static BigDecimal min(final Number... array) { + return minOrMax(true, array); + } + + /** + * Returns the maximum value in the array. + * + * @param array an array of Numbers (can be null) + * @return the maximum value in the array, or null if all the elements are null + */ + public static BigDecimal max(final Number... array) { + return minOrMax(false, array); + } + + /** + * Finds the minimum or maxiumum value contained in the given array, + * ignoring any null elements + * + * @param findMinimum false to get the maximum + * @param numbers can be null, empty, or contain null + * elements + * @return null if the array is null, empty, or + * all its elements are null + */ + private static BigDecimal minOrMax(final boolean findMinimum, final Number... numbers) { + if (numbers == null || numbers.length == 0) { + return null; + } + BigDecimal extreme = null; + for (final Number number : numbers) { + if (number != null) { + final BigDecimal candidate = getBigDecimal(number); + if (extreme == null || (findMinimum ? candidate.compareTo(extreme) < 0 : candidate.compareTo(extreme) > 0)) { + // The non-null candidate is the new extreme + extreme = candidate; + } + } + } + return extreme; + } + + /** + * Converts the given number to a {@link BigDecimal} + * + * @param number the number to convert (can be null) + * @return null if the given number was null + */ + private static BigDecimal getBigDecimal(final Number number) { + if (number == null || number instanceof BigDecimal) { + return (BigDecimal) number; + } + if (number instanceof BigInteger) { + return new BigDecimal((BigInteger) number); + } + return new BigDecimal(number.toString()); + } + + /** + * Constructor is private to prevent instantiation + */ + private NumberUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/ObjectUtils.java b/src/main/java/org/springframework/roo/support/util/ObjectUtils.java new file mode 100644 index 00000000..ba39e969 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/ObjectUtils.java @@ -0,0 +1,925 @@ +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.lang.ObjectUtils + */ +public final class ObjectUtils { + + // Constants + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + /** + * Return whether the given throwable is a checked exception: + * that is, neither a RuntimeException nor an Error. + * + * @param ex the throwable to check + * @return whether the throwable is a checked exception + * @see java.lang.Exception + * @see java.lang.RuntimeException + * @see java.lang.Error + */ + public static boolean isCheckedException(final Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the exceptions + * declared in a throws clause. + * + * @param ex the exception to checked + * @param declaredExceptions the exceptions declared in the throws clause + * @return whether the given exception is compatible + */ + public static boolean isCompatibleWithThrowsClause(final Throwable ex, final Class... declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + int i = 0; + while (i < declaredExceptions.length) { + if (declaredExceptions[i].isAssignableFrom(ex.getClass())) { + return true; + } + i++; + } + } + return false; + } + + /** + * Return whether the given array is empty: that is, null + * or of zero length. + * + * @param array the array to check + * @return whether the given array is empty + */ + public static boolean isEmpty(final Object[] array) { + return array == null || array.length == 0; + } + + /** + * Check whether the given array contains the given element. + * + * @param array the array to check (may be null, + * in which case the return value will always be false) + * @param element the element to check for + * @return whether the element has been found in the given array + */ + public static boolean containsElement(final Object[] array, final Object element) { + if (array == null) { + return false; + } + for (Object arrayEle : array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Append the given Object to the given array, returning a new array + * consisting of the input array contents plus the given Object. + * + * @param array the array to append to (can be null) + * @param obj the Object to append + * @return the new array (of the same component type; never null) + */ + public static Object[] addObjectToArray(final Object[] array, final Object obj) { + Class compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } + else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + Object[] newArr = (Object[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * 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); + } + int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * 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; + } + + /** + * Return as hash code for the given object; typically the value of + * {@link Object#hashCode()}. If the object is an array, + * this method will delegate to any of the nullSafeHashCode + * methods for arrays in this class. If the object is null, + * this method returns 0. + * + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(final Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array the array from whose elements to calculate the hash code (can be null) + * @return 0 if the array is null + */ + public static int nullSafeHashCode(final Object... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (final Object element : array) { + hash = MULTIPLIER * hash + nullSafeHashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final boolean... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final byte... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final char... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final double... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final float... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final int... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final long... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * + * @param array can be null + * @return 0 if array is null + */ + public static int nullSafeHashCode(final short... array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Returns the hash code of the given boolean value. + * + * @param bool the boolean for which to return the hash code + * @return see {@link Boolean#hashCode()} + */ + public static int hashCode(final boolean bool) { + return Boolean.valueOf(bool).hashCode(); + } + + /** + * Return the same value as {@link Double#hashCode()}. + * + * @see Double#hashCode() + */ + public static int hashCode(final double dbl) { + long bits = Double.doubleToLongBits(dbl); + return hashCode(bits); + } + + /** + * Return the same value as {@link Float#hashCode()}. + * + * @see Float#hashCode() + */ + public static int hashCode(final float flt) { + return Float.floatToIntBits(flt); + } + + /** + * Return the same value as {@link Long#hashCode()}. + * + * @see Long#hashCode() + */ + public static int hashCode(final long lng) { + return (int) (lng ^ (lng >>> 32)); + } + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * + * @param obj the object (may be null) + * @return the object's identity as String representation, + * or an empty String if the object was null + */ + public static String identityToString(final Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * + * @param obj the object + * @return the object's identity code in hex notation + */ + public static String getIdentityHexString(final Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if obj is + * not null; otherwise returns an empty String. + *

Differs from {@link #nullSafeToString(Object)} in that it returns + * an empty String rather than "null" for a null value. + * + * @param obj the object to build a display String for + * @return a display String representation of obj + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(final Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object. + *

Returns "null" if obj is null. + * + * @param obj the object to introspect (may be null) + * @return the corresponding class name + */ + public static String nullSafeClassName(final Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + *

Builds a String representation of the contents in case of an array. + * Returns "null" if obj is null. + * + * @param obj the object to build a String representation for + * @return a String representation of obj + */ + public static String nullSafeToString(final Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final Object... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(String.valueOf(array[i])); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final boolean... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final byte... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final char... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append("'").append(array[i]).append("'"); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final double... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final float... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final int... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final long... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(final short... array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Compares the two given objects, with null equivalent to + * null and null "less than" any + * non-null instance. Two non-null instances are + * compared using the first one's {@link Comparable#compareTo(Object)} + * method. + * + * @param the type of objects being compared + * @param one the first object being compared (can be null) + * @param other the second object being compared (can be null) + * @return see {@link Comparable#compareTo(Object)} + * @since 1.2.0 + */ + public static int nullSafeComparison(final Comparable one, final T other) { + if (one == null) { + if (other == null) { + return 0; + } + // First is null, second is not + return -1; + } + // If we get here, the first object is non-null + if (other == null) { + return 1; + } + return one.compareTo(other); + } + + /** + * Returns the String representation of the given object, or if it's + * null, the given default string + * + * @param object the object to represent as a String (can be null) + * @param defaultValue the value to return if the given object is null (can itself be blank) + * @return see above + * @since 1.2.0 + */ + public static String toString(final Object object, final String defaultValue) { + if (object == null) { + return defaultValue; + } + return object.toString(); + } + + /** + * Returns the given object if not null, otherwise the given + * default value + * + * @param the type of object being defaulted + * @param object the object to check (can be null) + * @param defaultValue the default value (can be null) + * @return null iff both values are null + * @since 1.2.0 + */ + public static T defaultIfNull(final T object, final T defaultValue) { + if (object == null) { + return defaultValue; + } + return object; + } + + /** + * Constructor is private to prevent instantiation + */ + private ObjectUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/OsUtils.java b/src/main/java/org/springframework/roo/support/util/OsUtils.java new file mode 100644 index 00000000..3da4cd38 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/OsUtils.java @@ -0,0 +1,15 @@ +package org.springframework.roo.support.util; + +/** + * Utilities for handling OS-specific behavior. + * + * @author Joris Kuipers + * @since 1.1.1 + */ +public class OsUtils { + private static final boolean WINDOWS_OS = System.getProperty("os.name").toLowerCase().contains("windows"); + + public static boolean isWindows() { + return WINDOWS_OS; + } +} diff --git a/src/main/java/org/springframework/roo/support/util/Pair.java b/src/main/java/org/springframework/roo/support/util/Pair.java new file mode 100644 index 00000000..029cc524 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/Pair.java @@ -0,0 +1,70 @@ +package org.springframework.roo.support.util; + +/** + * A pair with a key of type "K" and a value of type "V". Instances are immutable. + * + * @author Andrew Swan + * @since 1.2.0 + * @param the key type + * @param the value type + */ +public class Pair { + + // Fields + private final K key; + private final V value; + + /** + * Constructor + * + * @param key can be null + * @param value can be null + */ + public Pair(final K key, final V value) { + this.key = key; + this.value = value; + } + + /** + * Returns the key + * + * @return null if it is + */ + public K getKey() { + return key; + } + + /** + * Returns the value + * + * @return null if it is + */ + public V getValue() { + return value; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Pair)) { + return false; + } + final Pair otherPair = (Pair) obj; + return ObjectUtils.nullSafeEquals(key, otherPair.getKey()) && ObjectUtils.nullSafeEquals(value, otherPair.getValue()); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(new Object[] {getKey(), getValue()}); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("key: ").append(key); + sb.append(", value: ").append(value); + return sb.toString(); + } +} diff --git a/src/main/java/org/springframework/roo/support/util/PairList.java b/src/main/java/org/springframework/roo/support/util/PairList.java new file mode 100644 index 00000000..730f88da --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/PairList.java @@ -0,0 +1,103 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A {@link List} of {@link Pair}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; + + /** + * 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 Pair... pairs) { + addAll(Arrays.asList(pairs)); + } + + /** + * 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) { + Assert.isTrue(!(keys == null ^ values == null), "Parameter types and names must either both be null or both be not null"); + if (keys == null) { + Assert.isTrue(values == null, "Parameter names must be null if types are null"); + } + else { + Assert.isTrue(values != null, "Parameter names are required if types are provided"); + Assert.isTrue(keys.size() == values.size(), "Expected " + keys.size() + " values but found " + values.size()); + for (int i = 0; i < keys.size(); i++) { + add(keys.get(i), values.get(i)); + } + } + } + + /** + * Constructor for an empty list of pairs + */ + public PairList() { + // Empty + } + + /** + * Returns the keys of each {@link Pair} in this list + * + * @return a non-null list + */ + public List getKeys() { + final List keys = new ArrayList(); + for (final Pair pair : this) { + keys.add(pair.getKey()); + } + return keys; + } + + /** + * Returns the values of each {@link Pair} in this list + * + * @return a non-null modifiable copy of this list + */ + public List getValues() { + final List values = new ArrayList(); + for (final Pair pair : this) { + values.add(pair.getValue()); + } + return values; + } + + /** + * 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 Pair(key, value)); + } + + @SuppressWarnings("unchecked") + @Override + public Pair[] toArray() { + return super.toArray(new Pair[size()]); + } +} diff --git a/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java b/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java new file mode 100644 index 00000000..89caaa9d --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/ReflectionUtils.java @@ -0,0 +1,604 @@ +/* + * 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; + +/** + * 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 + */ +public abstract class ReflectionUtils { + + /** + * 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) { + Assert.notNull(clazz, "Class must not be null"); + Assert.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) { + Field[] fields = searchType.getDeclaredFields(); + for (Field field : fields) { + if ((name == null || name.equals(field.getName())) && + (type == null || type.equals(field.getType()))) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * 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 (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException( + "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * 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 (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException( + "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * 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) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(name, "Method name must not be null"); + Class searchType = clazz; + while (!Object.class.equals(searchType) && searchType != null) { + Method[] methods = (searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods()); + for (Method method : methods) { + if (name.equals(method.getName()) && + (parameterTypes == null || Arrays.equals(parameterTypes, method.getParameterTypes()))) { + return method; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * 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 (Exception ex) { + handleReflectionException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * 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 (IllegalAccessException ex) { + handleReflectionException(ex); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + handleInvocationTargetException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * 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); + } + + /** + * 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()); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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) { + Assert.notNull(method, "Method must not be null"); + Class[] declaredExceptions = method.getExceptionTypes(); + for (Class declaredException : declaredExceptions) { + if (declaredException.isAssignableFrom(exceptionType)) { + return true; + } + } + return false; + } + + /** + * Determine whether the given field is a "public static final" constant. + * @param field the field to check + */ + public static boolean isPublicStaticFinal(final Field field) { + int modifiers = field.getModifiers(); + return (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)); + } + + /** + * 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; + } + 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 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 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); + } + } + + /** + * 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); + } + } + + /** + * 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 { + Method[] methods = targetClass.getDeclaredMethods(); + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException( + "Shouldn't be illegal to access method '" + method.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } + while (targetClass != 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()]); + } + + /** + * 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. + Field[] fields = targetClass.getDeclaredFields(); + for (Field field : fields) { + // Skip static and final fields. + if (ff != null && !ff.matches(field)) { + continue; + } + try { + fc.doWith(field); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException( + "Shouldn't be illegal to access field '" + field.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + } + + /** + * 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); + Object srcValue = field.get(src); + field.set(dest, srcValue); + } + }, COPYABLE_FIELDS); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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())); + } + }; +} diff --git a/src/main/java/org/springframework/roo/support/util/StringUtils.java b/src/main/java/org/springframework/roo/support/util/StringUtils.java new file mode 100644 index 00000000..ed0891cd --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/StringUtils.java @@ -0,0 +1,1433 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +/** + * Miscellaneous {@link String} utility methods. + * + *

Mainly for internal use within the framework; consider + * Jakarta's Commons Lang + * for a more comprehensive suite of String utilities. + * + *

This class delivers some simple functionality that should really + * be provided by the core Java String and {@link StringBuilder} + * classes, such as the ability to {@link #replace} all occurrences of a given + * substring in a target string. It also provides easy-to-use methods to convert + * between delimited strings, such as CSV strings, and collections and arrays. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Rick Evans + * @author Arjen Poutsma + * @since 16 April 2001 + * @see org.apache.commons.lang.StringUtils + */ +public final class StringUtils { + + // Constants + private static final String FOLDER_SEPARATOR = "/"; + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + private static final String TOP_PATH = ".."; + private static final String CURRENT_PATH = "."; + private static final char EXTENSION_SEPARATOR = '.'; + + /** + * The platform-specific line separator. + * + * @since 1.2.0 + */ + public static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + /** + * Check that the given CharSequence is neither null nor of length 0. + * Note: Will return true for a CharSequence that purely consists of whitespace. + *

+	 * StringUtils.hasLength(null) = false
+	 * StringUtils.hasLength("") = false
+	 * StringUtils.hasLength(" ") = true
+	 * StringUtils.hasLength("Hello") = true
+	 * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null and has length + * @see #hasText(String) + */ + public static boolean hasLength(final CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given String is neither null nor of length 0. + * Note: Will return true for a String that purely consists of whitespace. + * @param str the String to check (may be null) + * @return true if the String is not null and has length + * @see #hasLength(CharSequence) + */ + public static boolean hasLength(final String str) { + return hasLength((CharSequence) str); + } + + /** + * Check whether the given CharSequence has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + *

+	 * StringUtils.hasText(null) = false
+	 * StringUtils.hasText("") = false
+	 * StringUtils.hasText(" ") = false
+	 * StringUtils.hasText("12345") = true
+	 * StringUtils.hasText(" 12345 ") = true
+	 * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null, + * its length is greater than 0, and it does not contain whitespace only + * @see java.lang.Character#isWhitespace + */ + public static boolean hasText(final CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + * @param str the String to check (may be null) + * @return true if the String is not null, its length is + * greater than 0, and it does not contain whitespace only + * @see #hasText(CharSequence) + */ + public static boolean hasText(final String str) { + return hasText((CharSequence) str); + } + + /** + * Indicates whether the given substring occurs within the given string. + * Inspired by the eponymous method in commons-lang. + *
+		StringUtils.contains(null, *)     = false
+		StringUtils.contains(*, null)     = false
+		StringUtils.contains("", "")      = true
+		StringUtils.contains("abc", "")   = true
+		StringUtils.contains("abc", "a")  = true
+		StringUtils.contains("abc", "z")  = false
+ * + * @param str the string to look within (can be null) + * @param substr the string to look for (can be null) + * @return see above + * @since 1.2.0 + */ + public static boolean contains(final String str, final String substr) { + if (str == null || substr == null) { + return false; + } + return str.contains(substr); + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see java.lang.Character#isWhitespace + */ + public static boolean containsWhitespace(final CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0, n = strLen; i < n; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String contains any whitespace characters. + * @param str the String to check (may be null) + * @return true if the String is not empty and + * contains at least 1 whitespace character + * @see #containsWhitespace(CharSequence) + */ + public static boolean containsWhitespace(final String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimWhitespace(final String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all whitespace from the given String: + * leading, trailing, and inbetween characters. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimAllWhitespace(final String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + int index = 0; + while (sb.length() > index) { + if (Character.isWhitespace(sb.charAt(index))) { + sb.deleteCharAt(index); + } + else { + index++; + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingWhitespace(final String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimTrailingWhitespace(final String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied leading character from the given String. + * @param str the String to check + * @param leadingCharacter the leading character to be trimmed + * @return the trimmed String + */ + public static String trimLeadingCharacter(final String str, final char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied trailing character from the given String. + * @param str the String to check + * @param trailingCharacter the trailing character to be trimmed + * @return the trimmed String + */ + public static String trimTrailingCharacter(final String str, final char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Test if the given String starts with the specified prefix, + * ignoring upper/lower case. + * @param str the String to check + * @param prefix the prefix to look for + * @see java.lang.String#startsWith + */ + public static boolean startsWithIgnoreCase(final String str, final String prefix) { + if (str == null || prefix == null) { + return false; + } + if (str.startsWith(prefix)) { + return true; + } + if (str.length() < prefix.length()) { + return false; + } + String lcStr = str.substring(0, prefix.length()).toLowerCase(); + String lcPrefix = prefix.toLowerCase(); + return lcStr.equals(lcPrefix); + } + + /** + * Test if the given String ends with the specified suffix, + * ignoring upper/lower case. + * @param str the String to check + * @param suffix the suffix to look for + * @see java.lang.String#endsWith + */ + public static boolean endsWithIgnoreCase(final String str, final String suffix) { + if (str == null || suffix == null) { + return false; + } + if (str.endsWith(suffix)) { + return true; + } + if (str.length() < suffix.length()) { + return false; + } + + String lcStr = str.substring(str.length() - suffix.length()).toLowerCase(); + String lcSuffix = suffix.toLowerCase(); + return lcStr.equals(lcSuffix); + } + + /** + * Test whether the given string matches the given substring + * at the given index. + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against + * @param substring the substring to match at the given index + */ + public static boolean substringMatch(final CharSequence str, final int index, final CharSequence substring) { + for (int j = 0, n = substring.length(); j < n; j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring in string s. + * @param str string to search in. Return 0 if this is null. + * @param sub string to search for. Return 0 if this is null. + */ + public static int countOccurrencesOf(final String str, final String sub) { + if (!hasLength(str) || !hasLength(sub)) { + return 0; + } + int count = 0, pos = 0, idx = 0; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Returns the given string repeated the given number of times + * + * @param str the string to repeat (can be null or empty) + * @param times the number of times to repeat it + * @return null if null is given + */ + public static String repeat(final String str, final int times) { + if (!hasLength(str)) { + return str; + } + final StringBuilder sb = new StringBuilder(str.length() * times); + for (int i = 0; i < times; i++) { + sb.append(str); + } + return sb.toString(); + } + + /** + * Replaces all occurrences of one string within another. + * + * @param original the string to modify (can be zero length to do nothing) + * @param toReplace the string to replace (can be blank to do nothing) + * @param replacement the string to replace it with (can be null to do nothing) + * @return the original string, modified as necessary + */ + public static String replace(final String original, final String toReplace, final String replacement) { + String result = original; + String previousResult; + do { + previousResult = result; + result = replaceFirst(previousResult, toReplace, replacement); + } while (!equals(previousResult, result)); + return result; + } + + /** + * Delete all occurrences of the given substring. + * @param inString the original String + * @param pattern the pattern to delete all occurrences of + * @return the resulting String + */ + public static String delete(final String inString, final String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given String. + * @param inString the original String + * @param charsToDelete a set of characters to delete. + * E.g. "az\n" will delete 'a's, 'z's and new lines. + * @return the resulting String + */ + public static String deleteAny(final String inString, final String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = inString.length(); i < n; i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + return sb.toString(); + } + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * Quote the given String with single quotes. + * @param str the input String (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or null if the input was null + */ + public static String quote(final String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a String with single quotes + * if it is a String; keeping the Object as-is else. + * @param obj the input Object (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or the input object as-is if not a String + */ + public static Object quoteIfString(final Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, + * "this.name.is.qualified", returns "qualified". + * @param qualifiedName the qualified name + */ + public static String unqualify(final String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, + * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * @param qualifiedName the qualified name + * @param separator the separator + */ + public static String unqualify(final String qualifiedName, final char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a String, changing the first letter to + * upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * @param str the String to capitalize, may be null + * @return the capitalized String, null if null + */ + public static String capitalize(final String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a String, changing the first letter to + * lower case as per {@link Character#toLowerCase(char)}. + * No other letters are changed. + * @param str the String to uncapitalize, may be null + * @return the uncapitalized String, null if null + */ + public static String uncapitalize(final String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(final String str, final boolean capitalize) { + if (!hasText(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str.length()); + if (capitalize) { + sb.append(Character.toUpperCase(str.charAt(0))); + } else { + sb.append(Character.toLowerCase(str.charAt(0))); + } + sb.append(str.substring(1)); + return sb.toString(); + } + + /** + * Extract the filename from the given path, + * e.g. "mypath/myfile.txt" -> "myfile.txt". + * @param path the file path (may be null) + * @return the extracted filename, or null if none + */ + public static String getFilename(final String path) { + if (path == null) { + return null; + } + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "txt". + * @param path the file path (may be null) + * @return the extracted filename extension, or null if none + */ + public static String getFilenameExtension(final String path) { + if (path == null) { + return null; + } + int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + return (sepIndex != -1 ? path.substring(sepIndex + 1) : null); + } + + /** + * Strip the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * @param path the file path (may be null) + * @return the path with stripped filename extension, + * or null if none + */ + public static String stripFilenameExtension(final String path) { + if (path == null) { + return null; + } + int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + return (sepIndex != -1 ? path.substring(0, sepIndex) : path); + } + + /** + * Apply the given relative path to the given path, + * assuming standard Java folder separation (i.e. "/" separators); + * @param path the path to start from (usually a full file path) + * @param relativePath the relative path to apply + * (relative to the full file path above) + * @return the full file path that results from applying the relative path + */ + public static String applyRelativePath(final String path, final String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + newPath += FOLDER_SEPARATOR; + } + return newPath + relativePath; + } + return relativePath; + } + + /** + * Normalize the path by suppressing sequences like "path/.." and + * inner simple dots. + *

The result is convenient for path comparison. For other uses, + * notice that Windows separators ("\") are replaced by simple slashes. + * @param path the original path + * @return the normalized path + */ + public static String cleanPath(final String path) { + if (path == null) { + return null; + } + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(":"); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + pathToUse = pathToUse.substring(prefixIndex + 1); + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); + List pathElements = new ArrayList(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (CURRENT_PATH.equals(element)) { + // Points to current directory - drop it. + } else if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } + else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.add(0, TOP_PATH); + } + + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * @param path1 first path for comparison + * @param path2 second path for comparison + * @return whether the two paths are equivalent after normalization + */ + public static boolean pathEquals(final String path1, final String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Parse the given localeString into a {@link Locale}. + *

This is the inverse operation of {@link Locale#toString Locale's toString}. + * @param localeString the locale string, following Locale's + * toString() format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores + * @return a corresponding Locale instance + */ + public static Locale parseLocaleString(final String localeString) { + String[] parts = tokenizeToStringArray(localeString, "_ ", false, false); + String language = (parts.length > 0 ? parts[0] : ""); + String country = (parts.length > 1 ? parts[1] : ""); + String variant = ""; + if (parts.length >= 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + /** + * Determine the RFC 3066 compliant language tag, + * as used for the HTTP "Accept-Language" header. + * @param locale the Locale to transform to a language tag + * @return the RFC 3066 compliant language tag as String + */ + public static String toLanguageTag(final Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + //--------------------------------------------------------------------- + // Convenience methods for working with String arrays + //--------------------------------------------------------------------- + + /** + * Append the given String to the given String array, returning a new array + * consisting of the input array contents plus the given String. + * @param arr the array to append to (can be null) + * @param str the String to append + * @return the new array (never null) + */ + public static String[] addStringToArray(final String[] arr, final String str) { + if (ObjectUtils.isEmpty(arr)) { + return new String[] {str}; + } + String[] newArr = new String[arr.length + 1]; + System.arraycopy(arr, 0, newArr, 0, arr.length); + newArr[arr.length] = str; + return newArr; + } + + /** + * Concatenate the given String arrays into one, + * with overlapping array elements included twice. + *

The order of elements in the original arrays is preserved. + * @param arr1 the first array (can be null) + * @param arr2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] concatenateStringArrays(final String[] arr1, final String[] arr2) { + if (ObjectUtils.isEmpty(arr1)) { + return arr2; + } + if (ObjectUtils.isEmpty(arr2)) { + return arr1; + } + String[] newArr = new String[arr1.length + arr2.length]; + System.arraycopy(arr1, 0, newArr, 0, arr1.length); + System.arraycopy(arr2, 0, newArr, arr1.length, arr2.length); + return newArr; + } + + /** + * Merge the given String arrays into one, with overlapping + * array elements only included once. + *

The order of elements in the original arrays is preserved + * (with the exception of overlapping elements, which are only + * included on their first occurrence). + * @param arr1 the first array (can be null) + * @param arr2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] mergeStringArrays(final String[] arr1, final String[] arr2) { + if (ObjectUtils.isEmpty(arr1)) { + return arr2; + } + if (ObjectUtils.isEmpty(arr2)) { + return arr1; + } + List result = new ArrayList(); + result.addAll(Arrays.asList(arr1)); + for (int i = 0, n = arr2.length; i < n; i++) { + String str = arr2[i]; + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Turn given source String array into sorted array. + * @param arr the source array + * @return the sorted array (never null) + */ + public static String[] sortStringArray(final String[] arr) { + if (ObjectUtils.isEmpty(arr)) { + return new String[0]; + } + Arrays.sort(arr); + return arr; + } + + /** + * Copy the given Collection into a String array. + * The Collection must contain String elements only. + * @param coll the Collection to copy + * @return the String array (null if the passed-in + * Collection was null) + */ + public static String[] toStringArray(final Collection coll) { + if (coll == null) { + return null; + } + return coll.toArray(new String[coll.size()]); + } + + /** + * Copy the given Enumeration into a String array. + * The Enumeration must contain String elements only. + * @param enumeration the Enumeration to copy + * @return the String array (null if the passed-in + * Enumeration was null) + */ + public static String[] toStringArray(final Enumeration enumeration) { + if (enumeration == null) { + return null; + } + List list = Collections.list(enumeration); + return list.toArray(new String[list.size()]); + } + + /** + * Trim the elements of the given String array, + * calling String.trim() on each of them. + * @param arr the original String array + * @return the resulting array (of the same size) with trimmed elements + */ + public static String[] trimArrayElements(final String[] arr) { + if (ObjectUtils.isEmpty(arr)) { + return new String[0]; + } + String[] result = new String[arr.length]; + for (int i = 0, n = arr.length; i < n; i++) { + String element = arr[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate Strings from the given array. + * Also sorts the array, as it uses a TreeSet. + * @param arr the String array + * @return an array without duplicates, in natural sort order + */ + public static String[] removeDuplicateStrings(final String[] arr) { + if (ObjectUtils.isEmpty(arr)) { + return arr; + } + Set set = new TreeSet(); + for (int i = 0, n = arr.length; i < n; i++) { + set.add(arr[i]); + } + return toStringArray(set); + } + + /** + * Split a String at the first occurrence of the delimiter. + * Does not include the delimiter in the result. + * @param toSplit the string to split + * @param delim to split the string up with + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the delimiter); + * or null if the delimiter wasn't found in the given input String + */ + public static String[] split(final String toSplit, final String delim) { + if (!hasLength(toSplit) || !hasLength(delim)) { + return null; + } + int offset = toSplit.indexOf(delim); + if (offset < 0) { + return null; + } + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delim.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param arr the array to process + * @param delim to split each element using (typically the equals symbol) + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties(final String[] arr, final String delim) { + return splitArrayElementsIntoProperties(arr, delim, null); + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * + * @param arr the array to process + * @param delim to split each element using (typically the equals symbol) + * @param charsToDelete one or more characters to remove from each element + * prior to attempting the split operation (typically the quotation mark + * symbol), or null if no removal should occur + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties(final String[] arr, final String delim, final String charsToDelete) { + if (ObjectUtils.isEmpty(arr)) { + return null; + } + + Properties result = new Properties(); + for (int i = 0, n = arr.length; i < n; i++) { + String element = arr[i]; + if (charsToDelete != null) { + element = deleteAny(arr[i], charsToDelete); + } + String[] splittedElement = split(element, delim); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + * Trims tokens and omits empty tokens. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter). + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(final String str, final String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's trim + * @param ignoreEmptyTokens omit empty tokens from the result array + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens (null if the input String + * was null) + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(final String str, final String delimiters, final boolean trimTokens, final boolean ignoreEmptyTokens) { + if (str == null) { + return null; + } + + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delim the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(final String str, final String delim) { + return delimitedListToStringArray(str, delim, null); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @param charsToDelete a set of characters to delete. Useful for deleting unwanted + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(final String str, final String delimiter, final String charsToDelete) { + if (str == null) { + return new String[0]; + } + if (delimiter == null) { + return new String[] {str}; + } + List result = new ArrayList(); + if ("".equals(delimiter)) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos = 0; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a CSV list into an array of Strings. + * @param str the input String + * @return an array of Strings, or the empty array in case of empty input + */ + public static String[] commaDelimitedListToStringArray(final String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convenience method to convert a CSV string list to a set. + * Note that this will suppress duplicates. + * @param str the input String + * @return a Set of String entries in the list + */ + public static Set commaDelimitedListToSet(final String str) { + Set set = new TreeSet(); + String[] tokens = commaDelimitedListToStringArray(str); + for (int i = 0; i < tokens.length; i++) { + set.add(tokens[i]); + } + return set; + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @param prefix the String to start each element with + * @param suffix the String to end each element with + * @return the delimited String (never null) + */ + public static String collectionToDelimitedString(final Collection coll, final String delim, final String prefix, final String suffix) { + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + 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(); + } + + /** + * Concatenates the given collection using the given delimiter between each item. + * + * @param coll the collection to display (can be null or empty) + * @param delim the delimiter to use (can be null or empty for none) + * @return the delimited String (never null) + */ + public static String collectionToDelimitedString(final Collection coll, final String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convenience method to return a Collection as a CSV String. + * E.g. useful for toString() implementations. + * @param coll the Collection to display + * @return the delimited String + */ + public static String collectionToCommaDelimitedString(final Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convenience method to return a String array as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * + * @param arr the array to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + * @since 1.2.0 + */ + public static String arrayToDelimitedString(final String delim, final Object... arr) { + return arrayToDelimitedString(arr, delim); + } + + /** + * Convenience method to return a String array as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * + * @param arr the array to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String arrayToDelimitedString(final Object[] arr, final String delim) { + if (ObjectUtils.isEmpty(arr)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + sb.append(delim); + } + sb.append(arr[i]); + } + return sb.toString(); + } + + /** + * Convenience method to return a String array as a CSV String. + * E.g. useful for toString() implementations. + * @param arr the array to display + * @return the delimited String + */ + public static String arrayToCommaDelimitedString(final Object[] arr) { + return arrayToDelimitedString(arr, ","); + } + + /** + * Converts the given String to uppercase. + * + * @param str the input String (may be null) + * @return the String in uppercase, otherwise null + */ + public static String toUpperCase(final String str) { + return str == null ? null : str.toUpperCase(); + } + + /** + * Converts the given String to lowercase. + * + * @param str the input String (may be null) + * @return the String in lowercase, otherwise null + */ + public static String toLowerCase(final String str) { + return str == null ? null : str.toLowerCase(); + } + + /** + *

+ * Removes leading and trailing whitespace from both ends of this String returning null if the String is empty ("") after the trim or if it is null. + * + *

+	 * StringUtils.trimToNull(null) = null
+	 * StringUtils.trimToNull("") = null
+	 * StringUtils.trimToNull(" ") = null
+	 * StringUtils.trimToNull("abc") = "abc"
+	 * StringUtils.trimToNull(" abc ") = "abc"
+	 * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, null if only chars <= 32, empty or null String input + * @since 1.1 + */ + public static String trimToNull(final String str) { + String ts = trimWhitespace(str); + return !hasText(ts) ? null : ts; + } + + /** + *

+ * Removes leading and trailing whitespace from both ends of this String returning an empty String ("") if the String is empty after the trim or if it is null. + * + *

+	 * StringUtils.trimToNull(null) = ""
+	 * StringUtils.trimToNull("") = ""
+	 * StringUtils.trimToNull(" ") = ""
+	 * StringUtils.trimToNull("abc") = "abc"
+	 * StringUtils.trimToNull(" abc ") = "abc"
+	 * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, an empty String("") if only chars <= 32, empty or null String input + * @since 1.1 + */ + public static String trimToEmpty(final String str) { + String ts = trimWhitespace(str); + return !hasText(ts) ? "" : ts; + } + + /** + * Returns either the passed in String, or if it's blank, the first of the + * given default values that is not blank. If all the given Strings are + * blank, returns the last of them. + * + *
    + *
  • StringUtils.defaultIfEmpty(null, "NULL") = "NULL"
  • + *
  • StringUtils.defaultIfEmpty("", "NULL") = "NULL"
  • + *
  • StringUtils.defaultIfEmpty("bat", "NULL") = "bat"
  • + *
  • StringUtils.defaultIfEmpty(null, "", "bat") = "bat"
  • + *
  • StringUtils.defaultIfEmpty(null, null, "") = ""
  • + *
+ * + * @param str the String to check, may be null + * @param defaultStr the default String to return if the input is empty ("") + * or null, may be null; note that if this is an expression, it will be + * evaluated before this method is called regardless of whether the first + * string is empty, so if this evaluation is expensive and performance is + * critical, check the first string for emptiness yourself rather than using + * this method + * @return the passed in String, or the default + */ + public static String defaultIfEmpty(final String str, final String... defaultValues) { + if (hasText(str) || ObjectUtils.isEmpty(defaultValues)) { + return str; + } + for (final String defaultValue : defaultValues) { + if (hasText(defaultValue)) { + return defaultValue; + } + } + return defaultValues[defaultValues.length - 1]; + } + + /** + * Right pads the presented string with the delim character. + * + * @param str the string to pad + * @param size the size to pad to + * @param padChar the padding character + * @return the right padded string + */ + public static String padRight(final String str, final int size, final char padChar) { + if (str == null) { + return null; + } + int pads = size - str.length(); + if (pads <= 0) { + return str; + } + return str.concat(padding(pads, padChar)); + } + + /** + * Left pads the presented string with the delim character. + * + * @param str the string to pad + * @param size the size to pad to + * @param padChar the padding character + * @return the left padded string + */ + public static String padLeft(final String str, final int size, final char padChar) { + if (str == null) { + return null; + } + int pads = size - str.length(); + if (pads <= 0) { + return str; + } + return padding(pads, padChar).concat(str); + } + + private static String padding(final int repeat, final char padChar) throws IndexOutOfBoundsException { + if (repeat < 0) { + throw new IndexOutOfBoundsException("Cannot pad a negative amount: " + repeat); + } + final char[] buf = new char[repeat]; + for (int i = 0; i < buf.length; i++) { + buf[i] = padChar; + } + return new String(buf); + } + + /** + * Prefixes the given string with the given prefix, if it's not already. + * + * @param str the string to prefix (can be blank) + * @param prefix the prefix to apply (can be blank to do nothing) + * @return null if a null string was given, + * otherwise the prefixed string + * @since 1.2.0 + */ + public static String prefix(final String str, final String prefix) { + if (str == null || prefix == null || str.startsWith(prefix)) { + return str; + } + return prefix + str; + } + + /** + * Removes the given prefix from the given string, if it exists + * + * @param str the string to modify (can be blank to do nothing) + * @param prefix the prefix to remove (can be blank to do nothing) + * @return null if a null string was given + * @since 1.2.0 + */ + public static String removePrefix(final String str, final String prefix) { + if (!hasText(str) || !hasText(prefix) || !str.startsWith(prefix)) { + return str; + } + return str.substring(prefix.length()); + } + + /** + * Removes the given suffix from the given string, if it exists + * + * @param str the string to modify (can be blank to do nothing) + * @param suffix the suffix to remove (can be blank to do nothing) + * @return null if a null string was given + * @since 1.2.0 + */ + public static String removeSuffix(final String str, final String suffix) { + if (!hasText(str) || !hasText(suffix) || !str.endsWith(suffix)) { + return str; + } + return str.substring(0, str.length() - suffix.length()); + } + + /** + * Appends the given suffix to the given string, if not already present + * + * @param str the string to modify (can be blank to do nothing) + * @param suffix the suffix to append (can be blank to do nothing) + * @return null if a null string was given + * @since 1.2.0 + */ + public static String suffix(final String str, final String suffix) { + if (str == null || suffix == null || str.endsWith(suffix)) { + return str; + } + return str + suffix; + } + + /** + * Indicates whether the two given strings are equal, including case, where + * null is (only) equal to null. + * + * @param str1 the first string to compare (can be null) + * @param str2 the second string to compare (can be null) + * @return see above + * @since 1.2.0 + */ + public static boolean equals(final String str1, final String str2) { + if (str1 == null) { + return str2 == null; + } + return str1.equals(str2); + } + + /** + * Indicates whether the given text is blank. More fluent than calling + * StringUtils.isBlank(blah). + * + * @param str the text to check (can be blank) + * @return the opposite of {@link #hasText(String)} + * @since 1.2.0 + */ + public static boolean isBlank(final String str) { + return !hasText(str); + } + + /** + * Replaces the first occurrence of the given substring in the given string. + *

+ * Use in preference to {@link String#replaceFirst(String, String)} when + * toReplace is not a regular expression (e.g. some part of a + * file path, which on Windows will contain backslashes, which have special + * meaning to regexs). + * + * @param original the string to modify (can be zero length to do nothing) + * @param toReplace the string to replace (can be blank to do nothing) + * @param replacement the string to replace it with (can be null to do nothing) + * @return the original string, modified as necessary + * @since 1.2.0 + */ + public static String replaceFirst(final String original, final String toReplace, final String replacement) { + if (!hasLength(original) || !hasLength(toReplace) || replacement == null || !original.contains(toReplace)) { + return original; + } + final int startOfOld = original.indexOf(toReplace); + final int endOfOld = startOfOld + toReplace.length(); + return arrayToDelimitedString("", original.substring(0, startOfOld), replacement, original.substring(endOfOld)); + } + + /** + * Returns the substring after the last occurrence of a separator. The separator is not returned. + *

A null string input will return null. An empty ("") string input will return the empty string. + * An empty or null separator will return the empty string if the input string is not null. + *

If nothing is found, the empty string is returned. + *

+	     StringUtils.substringAfterLast(null, *)      = null
+	     StringUtils.substringAfterLast("", *)        = ""
+	     StringUtils.substringAfterLast(*, "")        = ""
+	     StringUtils.substringAfterLast(*, null)      = ""
+	     StringUtils.substringAfterLast("abc", "a")   = "bc"
+	     StringUtils.substringAfterLast("abcba", "b") = "a"
+	     StringUtils.substringAfterLast("abc", "c")   = ""
+	     StringUtils.substringAfterLast("a", "a")     = ""
+	     StringUtils.substringAfterLast("a", "z")     = ""
+ * + * @param original the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the last occurrence of the separator, + * null if null String input + * @since 1.2.0 + */ + public static String substringAfterLast(final String original, final String separator) { + if (!hasLength(original)) { + return original; + } + if (!hasLength(separator)) { + return ""; + } + final int separatorStart = original.lastIndexOf(separator); + if (separatorStart == -1) { + return ""; + } + return original.substring(separatorStart + separator.length()); + } + + /** + * Constructor is private to prevent instantiation + */ + private StringUtils() {} +} \ No newline at end of file diff --git a/src/main/java/org/springframework/roo/support/util/TemplateUtils.java b/src/main/java/org/springframework/roo/support/util/TemplateUtils.java new file mode 100644 index 00000000..5ddf9365 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/TemplateUtils.java @@ -0,0 +1,43 @@ +package org.springframework.roo.support.util; + +import java.io.InputStream; + +/** + * Utilities for dealing with "templates", which are commonly used by ROO add-ons. + * + * @author Ben Alex + * @since 1.0 + */ +public final class TemplateUtils { + + /** + * Determines the path to the requested template. + * + * @param clazz which owns the template (required) + * @param templateFilename the filename of the template (required) + * @return the full classloader-specific path to the template (never null) + * @deprecated use {@link FileUtils#getPath(Class, String)} instead + */ + @Deprecated + public static String getTemplatePath(final Class loadingClass, final String relativeFilename) { + return FileUtils.getPath(loadingClass, relativeFilename); + } + + /** + * Acquires an {@link InputStream} to the requested classloader-derived template. + * + * @param clazz which owns the template (required) + * @param templateFilename the filename of the template (required) + * @return the input stream (never null; an exception is thrown if cannot be found) + * @deprecated use {@link FileUtils#getInputStream(Class, String)} instead + */ + @Deprecated + public static InputStream getTemplate(final Class clazz, final String templateFilename) { + return clazz.getResourceAsStream(templateFilename); + } + + /** + * Constructor is private to prevent instantiation + */ + private TemplateUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/UrlUtils.java b/src/main/java/org/springframework/roo/support/util/UrlUtils.java new file mode 100644 index 00000000..c5ce0a5f --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/UrlUtils.java @@ -0,0 +1,35 @@ +package org.springframework.roo.support.util; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +/** + * Utility methods relating to networking types such as URLs. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public final class UrlUtils { + + /** + * Converts the given URI to a URL; equivalent to {@link URI#toURL()} + * except that it throws a runtime exception. + * + * @param uri + * @return a non-null URL + * @throws IllegalArgumentException if the conversion is not possible + */ + public static URL toURL(final URI uri) { + try { + return uri.toURL(); + } catch (final MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Constructor is private to prevent instantiation + */ + private UrlUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/WebXmlUtils.java b/src/main/java/org/springframework/roo/support/util/WebXmlUtils.java new file mode 100644 index 00000000..f17842d9 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/WebXmlUtils.java @@ -0,0 +1,587 @@ +package org.springframework.roo.support.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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 { + + // Constants + private static final String WEB_APP_XPATH = "/web-app/"; + private static final String WHITESPACE = "[ \t\r\n]"; + + /** + * 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) { + Assert.hasText(displayName, "display name required"); + Assert.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.hasText(comment)) { + addCommentBefore(displayNameElement, comment, document); + } + } + displayNameElement.setTextContent(displayName); + } + + /** + * 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) { + Assert.notNull(document, "Web XML document required"); + Assert.hasText(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.hasText(comment)) { + addCommentBefore(descriptionElement, comment, document); + } + } + descriptionElement.setTextContent(description); + } + + /** + * 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) { + Assert.notNull(document, "Web XML document required"); + Assert.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.hasText(comment)) { + addCommentBefore(contextParamElement, comment, document); + } + } + appendChildIfNotPresent(contextParamElement, new XmlElementBuilder("param-value", document).setText(contextParam.getValue()).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) + */ + 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()); + } + + /** + * 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) { + Assert.notNull(document, "Web XML document required"); + Assert.hasText(filterName, "Filter name required"); + Assert.hasText(filterClass, "Filter class required"); + Assert.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)) { + Assert.hasText(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)) { + Assert.hasText(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)) { + Assert.hasText(beforeFilterName, "The 'before' filter name is required when using FilterPosition.BETWEEN"); + Assert.hasText(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.hasText(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 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) { + Assert.notNull(document, "Web XML document required"); + Assert.hasText(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.hasText(comment)) { + addCommentBefore(listenerElement, comment, document); + } + } + } + + /** + * 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) { + Assert.notNull(document, "Web XML document required"); + Assert.hasText(servletName, "Servlet name required"); + Assert.hasText(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.hasText(urlPattern)) { + appendChildIfNotPresent(servletMappingElement, new XmlElementBuilder("url-pattern", document).setText(urlPattern).build()); + } else { + appendChildIfNotPresent(servletMappingElement, new XmlElementBuilder("servlet-name", document).setText(servletName).build()); + } + } + + /** + * 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) { + Assert.notNull(document, "Web XML document required"); + Assert.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.hasText(comment)) { + addCommentBefore(sessionConfigElement, comment, document); + } + } + appendChildIfNotPresent(sessionConfigElement, new XmlElementBuilder("session-timeout", document).setText(String.valueOf(timeout)).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) { + Assert.notNull(document, "Web XML document required"); + Assert.hasText("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.hasText(comment)) { + addCommentBefore(welcomeFileElement, comment, document); + } + } + appendChildIfNotPresent(welcomeFileElement, new XmlElementBuilder("welcome-file", document).setText(path).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) { + Assert.notNull(document, "Web XML document required"); + Assert.hasText(exceptionType, "Fully qualified exception type name required"); + Assert.hasText(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.hasText(comment)) { + addCommentBefore(errorPageElement, comment, document); + } + } + appendChildIfNotPresent(errorPageElement, new XmlElementBuilder("location", document).setText(location).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) { + Assert.notNull(document, "Web XML document required"); + Assert.notNull(errorCode, "Error code required"); + Assert.hasText(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.hasText(comment)) { + addCommentBefore(errorPageElement, comment, document); + } + } + appendChildIfNotPresent(errorPageElement, new XmlElementBuilder("location", document).setText(location).build()); + } + + /** + * 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) { + Assert.notNull(document, "Web XML document required"); + Assert.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.hasText(comment)) { + addCommentBefore(securityConstraintElement, comment, document); + } + } + + if (StringUtils.hasText(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); + Assert.hasText(webResourceCollection.getWebResourceName(), "web-resource-name is required"); + webResourceCollectionBuilder.addChild(new XmlElementBuilder("web-resource-name", document).setText(webResourceCollection.getWebResourceName()).build()); + if (StringUtils.hasText(webResourceCollection.getDescription())) { + webResourceCollectionBuilder.addChild(new XmlElementBuilder("description", document).setText(webResourceCollection.getWebResourceName()).build()); + } + for (final String urlPattern : webResourceCollection.getUrlPatterns()) { + if (StringUtils.hasText(urlPattern)) { + webResourceCollectionBuilder.addChild(new XmlElementBuilder("url-pattern", document).setText(urlPattern).build()); + } + } + for (final String httpMethod : webResourceCollection.getHttpMethods()) { + if (StringUtils.hasText(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.hasText(roleName)) { + authConstraintBuilder.addChild(new XmlElementBuilder("role-name", document).setText(roleName).build()); + } + } + appendChildIfNotPresent(securityConstraintElement, authConstraintBuilder.build()); + } + + if (StringUtils.hasText(transportGuarantee)) { + final XmlElementBuilder userDataConstraintBuilder = new XmlElementBuilder("user-data-constraint", document); + userDataConstraintBuilder.addChild(new XmlElementBuilder("transport-guarantee", document).setText(transportGuarantee).build()); + appendChildIfNotPresent(securityConstraintElement, userDataConstraintBuilder.build()); + } + } + + 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); + } + + 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 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); + } + + /** + * 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 addLineBreakBefore(final Element element, final Document document) { + document.getDocumentElement().insertBefore(document.createTextNode("\n "), element); + } + + 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); + } + } + + /** + * Value object that holds init-param style information + * + * @author Stefan Schmidt + * @since 1.1 + */ + public static class WebXmlParam extends Pair { + + /** + * Constructor + * + * @param name + * @param value + */ + public WebXmlParam(final String name, final String value) { + super(name, value); + } + + /** + * Returns the name of this parameter + * + * @return + */ + public String getName() { + return getKey(); + } + } + + /** + * Enum to define filter position + * + * @author Stefan Schmidt + * @since 1.1 + * + */ + public static enum FilterPosition { + FIRST, LAST, BEFORE, AFTER, BETWEEN; + } + + /** + * Enum to define dispatcher + * + * @author Stefan Schmidt + * @since 1.1.1 + * + */ + public static enum Dispatcher { + FORWARD, REQUEST, INCLUDE, ERROR; + } + + /** + * Convenience class for passing a web-resource-collection element's details + * + * @since 1.1.1 + */ + public static class WebResourceCollection { + private final String webResourceName; + private final String description; + private final List urlPatterns; + private final List httpMethods; + + 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 getWebResourceName() { + return webResourceName; + } + + public List getUrlPatterns() { + return urlPatterns; + } + + public List getHttpMethods() { + return httpMethods; + } + + public String getDescription() { + return description; + } + } + + /** + * Constructor is private to prevent instantiation + */ + private WebXmlUtils() { + } +} diff --git a/src/main/java/org/springframework/roo/support/util/XmlElementBuilder.java b/src/main/java/org/springframework/roo/support/util/XmlElementBuilder.java new file mode 100644 index 00000000..ab976767 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/XmlElementBuilder.java @@ -0,0 +1,77 @@ +package org.springframework.roo.support.util; + +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 { + + // Fields + 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) { + Assert.hasText(name, "Element name required"); + Assert.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) { + Assert.hasText(qName, "Attribute qName required"); + Assert.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) { + Assert.notNull(node, "Node required"); + this.element.appendChild(node); + return this; + } + + /** + * 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) { + Assert.hasText(text, "Text content required"); + element.setTextContent(text); + return this; + } + + /** + * Get the element instance. + * + * @return The element. + */ + public Element build() { + return element; + } +} diff --git a/src/main/java/org/springframework/roo/support/util/XmlRoundTripUtils.java b/src/main/java/org/springframework/roo/support/util/XmlRoundTripUtils.java new file mode 100644 index 00000000..fb33c232 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/XmlRoundTripUtils.java @@ -0,0 +1,211 @@ +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.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 (NoSuchAlgorithmException e) { + throw new IllegalStateException("Could not create hash key for identifier"); + } + } + + /** + * 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) { + StringBuilder sb = new StringBuilder(); + sb.append(element.getTagName()); + NamedNodeMap attributes = element.getAttributes(); + SortedMap attrKVStore = Collections.synchronizedSortedMap(new TreeMap()); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Node attr = attributes.item(i); + if (!"z".equals(attr.getNodeName()) && !attr.getNodeName().startsWith("_")) { + attrKVStore.put(attr.getNodeName(), attr.getNodeValue()); + } + } + for (Entry entry: attrKVStore.entrySet()) { + sb.append(entry.getKey()).append(entry.getValue()); + } + return base64(sha1(sb.toString().getBytes())); + } + + /** + * 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; + } + + /** + * 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; + 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; + } + + private static boolean addOrUpdateElements(final Element original, final Element proposed, boolean originalDocumentChanged) { + NodeList proposedChildren = proposed.getChildNodes(); + for (int i = 0, n = proposedChildren.getLength(); i < n; i++) { // Check proposed elements and compare to originals to find out if we need to add or replace elements + Node node = proposedChildren.item(i); + if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { + Element proposedElement = (Element) node; + String proposedId = proposedElement.getAttribute("id"); + if (proposedId.length() != 0) { // Only proposed elements with an id will be considered + Element originalElement = XmlUtils.findFirstElement("//*[@id='" + proposedId + "']", original); + if (null == originalElement) { // Insert proposed element given the original document has no element with a matching id + 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); + } else { // Find the best place to insert the element + if (proposed.getAttribute("id").length() != 0) { // Try to find the id of the proposed element's parent id in the original document + Element originalParent = XmlUtils.findFirstElement("//*[@id='" + proposed.getAttribute("id") + "']", original); + if (originalParent != null) { // Found parent with the same id, so we can just add it as new child + originalParent.appendChild(original.getOwnerDocument().importNode(proposedElement, false)); + } else { // No parent found so we add it as a child of the root element (last resort) + original.appendChild(original.getOwnerDocument().importNode(proposedElement, false)); + } + } else { // No parent found so we add it as a child of the root element (last resort) + original.appendChild(original.getOwnerDocument().importNode(proposedElement, false)); + } + } + originalDocumentChanged = true; + } else { // We found an element in the original document with a matching id + String originalElementHashCode = originalElement.getAttribute("z"); + if (originalElementHashCode.length() > 0) { // Only act if a hash code exists + if ("?".equals(originalElementHashCode) || originalElementHashCode.equals(calculateUniqueKeyFor(originalElement))) { // Only act if hash codes match (no user changes in the element) or the user requests for the hash code to be regenerated + if (!equalElements(originalElement, proposedElement)) { // Check if the elements have equal contents + originalElement.getParentNode().replaceChild(original.getOwnerDocument().importNode(proposedElement, false), originalElement); //replace the original with the proposed element + originalDocumentChanged = true; + } + if ("?".equals(originalElementHashCode)) { // Replace z if the user sets its value to '?' as an indication that roo should take over the management of this element again + originalElement.setAttribute("z", calculateUniqueKeyFor(proposedElement)); + originalDocumentChanged = true; + } + } else { // If hash codes don't match we will mark the element as z="user-managed" + if (!originalElementHashCode.equals("user-managed")) { + originalElement.setAttribute("z", "user-managed"); // Mark the element as 'user-managed' if the hash codes don't match any more + originalDocumentChanged = true; + } + } + } + } + } + originalDocumentChanged = addOrUpdateElements(original, proposedElement, originalDocumentChanged); // Walk through the document tree recursively + } + } + return originalDocumentChanged; + } + + private static boolean removeElements(final Element original, final Element proposed, boolean originalDocumentChanged) { + NodeList originalChildren = original.getChildNodes(); + for (int i = 0, n = originalChildren.getLength(); i < n; i++) { // Check original elements and compare to proposed to find out if we need to remove elements + Node node = originalChildren.item(i); + if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { + Element originalElement = (Element) node; + String originalId = originalElement.getAttribute("id"); + if (originalId.length() != 0) { // Only proposed elements with an id will be considered + 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; + } + } + originalDocumentChanged = removeElements(originalElement, proposed, originalDocumentChanged); // Walk through the document tree recursively + } + } + return originalDocumentChanged; + } + + private static boolean equalElements(final Element a, final Element b) { + if (!a.getTagName().equals(b.getTagName())) { + return false; + } + NamedNodeMap attributes = a.getAttributes(); + int customAttributeCounter = 0; + for (int i = 0, n = attributes.getLength(); i < n; i++) { + 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; + } + + /** + * 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) { + Assert.notNull(digest, "Could not create hash key for identifier"); + return digest.digest(data); + } + + private static String base64(final byte[] data) { + return Base64.encodeBytes(data); + } + + /** + * Constructor is private to prevent instantiation + */ + private XmlRoundTripUtils() {} +} diff --git a/src/main/java/org/springframework/roo/support/util/XmlUtils.java b/src/main/java/org/springframework/roo/support/util/XmlUtils.java new file mode 100644 index 00000000..34532431 --- /dev/null +++ b/src/main/java/org/springframework/roo/support/util/XmlUtils.java @@ -0,0 +1,558 @@ +package org.springframework.roo.support.util; + +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 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 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 { + + // Constants + private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + private static final Map compiledExpressionCache = new HashMap(); + private static final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + private static final XPath xpath = XPathFactory.newInstance().newXPath(); + + /** + * 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); + } + } + + /** + * 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; + } + + /** + * 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 + */ + public static Document readXml(InputStream inputStream) { + Assert.notNull(inputStream, "InputStream required"); + try { + if (!(inputStream instanceof BufferedInputStream)) { + inputStream = new BufferedInputStream(inputStream); + } + return getDocumentBuilder().parse(inputStream); + } catch (final Exception e) { + throw new IllegalStateException("Could not open input stream", e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + /** + * 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) { + Assert.notNull(transformer, "Transformer required"); + Assert.notNull(outputStream, "OutputStream required"); + Assert.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); + } + } + + /** + * 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); + } + } + + /** + * 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) { + Assert.notNull(node1, "First node required"); + Assert.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 {@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); + } + } + + /** + * 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 (StringUtils.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); + } + + /** + * 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 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) { + Assert.hasText(xPathExpression, "XPath expression required"); + Assert.notNull(root, "Root element required"); + Node node = null; + try { + XPathExpression expr = compiledExpressionCache.get(xPathExpression); + if (expr == null) { + expr = xpath.compile(xPathExpression); + compiledExpressionCache.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) { + Assert.hasText(xPathExpression, "XPath expression required"); + Assert.notNull(root, "Root element required"); + final Element element = findFirstElement(xPathExpression, root); + Assert.notNull(element, "Unable to obtain required element '" + xPathExpression + "' from element '" + root + "'"); + return element; + } + + /** + * 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 = compiledExpressionCache.get(xPathExpression); + if (expr == null) { + expr = xpath.compile(xPathExpression); + compiledExpressionCache.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 = compiledExpressionCache.get(xPathExpression); + if (expr == null) { + expr = xpath.compile(xPathExpression); + compiledExpressionCache.put(xPathExpression, expr); + } + attr = (Node) expr.evaluate(element, XPathConstants.NODE); + } catch (final XPathExpressionException e) { + throw new IllegalArgumentException("Unable evaluate xpath expression", e); + } + return attr; + } + + /** + * @return a transformer that indents entries by 4 characters (never null) + */ + public static Transformer createIndentingTransformer() { + Transformer transformer; + try { + transformerFactory.setAttribute("indent-number", 4); + transformer = transformerFactory.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; + } + + /** + * @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); + Assert.notNull(inputStream, "Could not open the file '" + xmlFilePath + "'"); + return readXml(inputStream).getDocumentElement(); + } + + /** + * 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"); + } + + /** + * 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("[:\\.-]", "_"); + } + + /** + * 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 (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) { + throw new IllegalArgumentException("Illegal name '" + element + "' (illegal character)"); + } + } + } + + 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; + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * Constructor is private to prevent instantiation + */ + private XmlUtils() {} +} \ No newline at end of file diff --git a/src/main/java/org/springframework/shell/Bootstrap.java b/src/main/java/org/springframework/shell/Bootstrap.java index 4b8dd217..3a9557d5 100644 --- a/src/main/java/org/springframework/shell/Bootstrap.java +++ b/src/main/java/org/springframework/shell/Bootstrap.java @@ -6,21 +6,31 @@ package org.springframework.shell; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; //import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.roo.shell.AbstractShell; import org.springframework.roo.shell.CommandMarker; import org.springframework.roo.shell.Converter; import org.springframework.roo.shell.ExitShellRequest; import org.springframework.roo.shell.Shell; +import org.springframework.roo.shell.converters.StringConverter; import org.springframework.roo.shell.event.ShellStatus; import org.springframework.roo.support.logging.HandlerUtils; import org.springframework.roo.support.util.Assert; +import org.springframework.shell.plugin.PluginConfigurationReader; +import org.springframework.shell.plugin.PluginInfo; +import org.springframework.util.ClassUtils; import org.springframework.util.StopWatch; //import ch.qos.logback.classic.LoggerContext; @@ -66,7 +76,7 @@ public class Bootstrap { Assert.hasText(applicationContextLocation, "Application context location required"); - ctx = new ClassPathXmlApplicationContext(applicationContextLocation); + createApplicationContext(applicationContextLocation); Map shells = ctx.getBeansOfType(JLineShellComponent.class); @@ -113,6 +123,54 @@ public class Bootstrap { } + private void createApplicationContext(String applicationContextLocation) { + //ctx = new ClassPathXmlApplicationContext(applicationContextLocation); + + AnnotationConfigApplicationContext annctx = new AnnotationConfigApplicationContext(); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.StringConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.AvailableCommandsConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.BigDecimalConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.BigIntegerConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.BooleanConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.CharacterConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.DateConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.DoubleConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.EnumConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.FloatConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.IntegerConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.LocaleConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.LongConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.ShortConverter.class); + createAndRegisterBeanDefinition(annctx, org.springframework.roo.shell.converters.StaticFieldConverterImpl.class); + + ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + PluginConfigurationReader configReader = new PluginConfigurationReader(resourcePatternResolver); + PluginInfo[] pluginInfos = configReader.readPluginInfos("classpath*:/META-INF/spring/spring-shell-plugin.xml"); + for (int i = 0; i < pluginInfos.length; i++) { + List configClassNames = pluginInfos[i].getConfigClassNames(); + for (String configClassName : configClassNames) { + try { + annctx.register(ClassUtils.forName(configClassName, ClassUtils.getDefaultClassLoader())); + } catch (ClassNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (LinkageError e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + //annctx.scan("org.springframework.shell"); + annctx.refresh(); + ctx = annctx; + } + + protected void createAndRegisterBeanDefinition(AnnotationConfigApplicationContext annctx, Class clazz) { + RootBeanDefinition rbd = new RootBeanDefinition(); + rbd.setBeanClass(clazz); + annctx.registerBeanDefinition(clazz.getSimpleName(), rbd); + } + protected ExitShellRequest run(String[] executeThenQuit) { ExitShellRequest exitShellRequest; diff --git a/src/main/java/org/springframework/shell/plugin/HelloWorldPlugin.java b/src/main/java/org/springframework/shell/plugin/HelloWorldPlugin.java new file mode 100644 index 00000000..fa79f398 --- /dev/null +++ b/src/main/java/org/springframework/shell/plugin/HelloWorldPlugin.java @@ -0,0 +1,12 @@ +package org.springframework.shell.plugin; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan({"org.springframework.shell.commands", "org.springframework.shell.converters"}) +public class HelloWorldPlugin { + + + +} diff --git a/src/main/java/org/springframework/shell/plugin/PluginConfigurationReader.java b/src/main/java/org/springframework/shell/plugin/PluginConfigurationReader.java new file mode 100644 index 00000000..74217882 --- /dev/null +++ b/src/main/java/org/springframework/shell/plugin/PluginConfigurationReader.java @@ -0,0 +1,129 @@ +package org.springframework.shell.plugin; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.util.xml.SimpleSaxErrorHandler; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; + +public class PluginConfigurationReader { + + + private static final String CONFIGURATION = "configuration"; + + private static final String CONFIGURATION_CLASS_NAME = "class"; + + private final Log logger = LogFactory.getLog(getClass()); + + private final ResourcePatternResolver resourcePatternResolver; + + public PluginConfigurationReader(ResourcePatternResolver resourcePatternResolver) { + Assert.notNull(resourcePatternResolver, "ResourceLoader must not be null"); + this.resourcePatternResolver = resourcePatternResolver; + } + + + public PluginInfo[] readPluginInfos(String... pluginInfoXmlLocations) { + ErrorHandler handler = new SimpleSaxErrorHandler(logger); + List infos = new LinkedList(); + String resourceLocation = null; + try { + for (String location : pluginInfoXmlLocations) { + Resource[] resources = this.resourcePatternResolver.getResources(location); + for (Resource resource : resources) { + resourceLocation = resource.toString(); + InputStream stream = resource.getInputStream(); + try { + Document document = buildDocument(handler, stream); + parseDocument(resource, document, infos); + } + finally { + stream.close(); + } + } + } + } + catch (IOException ex) { + throw new IllegalArgumentException("Cannot parse persistence unit from " + resourceLocation, ex); + } + catch (SAXException ex) { + throw new IllegalArgumentException("Invalid XML in persistence unit from " + resourceLocation, ex); + } + catch (ParserConfigurationException ex) { + throw new IllegalArgumentException("Internal error parsing persistence unit from " + resourceLocation); + } + + return infos.toArray(new PluginInfo[infos.size()]); + } + + + /** + * Parse the validated document and add entries to the given unit info list. + */ + protected List parseDocument( + Resource resource, Document document, List infos) throws IOException { + + Element persistence = document.getDocumentElement(); + List configurations = DomUtils.getChildElementsByTagName(persistence, CONFIGURATION); + for (Element configuration : configurations) { + PluginInfo info = parsePluginInfo(configuration); + infos.add(info); + + } + + return infos; + } + + /** + * Parse the plugin DOM element. + */ + protected PluginInfo parsePluginInfo(Element configuration) throws IOException { + PluginInfo pluginInfo = new PluginInfo(); + parseClass(configuration, pluginInfo); + return pluginInfo; + } + + + /** + * Parse the class XML elements. + */ + @SuppressWarnings("unchecked") + protected void parseClass(Element configuration, PluginInfo pluginInfo) { + List classes = DomUtils.getChildElementsByTagName(configuration, CONFIGURATION_CLASS_NAME); + for (Element element : classes) { + String value = DomUtils.getTextValue(element).trim(); + if (StringUtils.hasText(value)) + pluginInfo.addConfigurationClassName(value); + } + } + + /** + * Validate the given stream and return a valid DOM document for parsing. + */ + protected Document buildDocument(ErrorHandler handler, InputStream stream) + throws ParserConfigurationException, SAXException, IOException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder parser = dbf.newDocumentBuilder(); + parser.setErrorHandler(handler); + return parser.parse(stream); + } +} diff --git a/src/main/java/org/springframework/shell/plugin/PluginInfo.java b/src/main/java/org/springframework/shell/plugin/PluginInfo.java new file mode 100644 index 00000000..52bb8dce --- /dev/null +++ b/src/main/java/org/springframework/shell/plugin/PluginInfo.java @@ -0,0 +1,19 @@ +package org.springframework.shell.plugin; + +import java.util.LinkedList; +import java.util.List; + +public class PluginInfo { + + private List configClassNames = new LinkedList(); + + public void addConfigurationClassName(String configClassName) { + this.configClassNames.add(configClassName); + + } + + public List getConfigClassNames() { + return this.configClassNames; + } + +} diff --git a/src/main/resources/META-INF/spring/app-context.xml b/src/main/resources/META-INF/spring/app-context.xml index a6c93278..1dbc536d 100644 --- a/src/main/resources/META-INF/spring/app-context.xml +++ b/src/main/resources/META-INF/spring/app-context.xml @@ -2,9 +2,10 @@ + Example configuration to get you started. diff --git a/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/src/main/resources/META-INF/spring/spring-shell-plugin.xml new file mode 100644 index 00000000..ab94e0b4 --- /dev/null +++ b/src/main/resources/META-INF/spring/spring-shell-plugin.xml @@ -0,0 +1,6 @@ + + + + org.springframework.shell.plugin.HelloWorldPlugin + + \ No newline at end of file diff --git a/src/main/resources/META-INF/spring/test.xml b/src/main/resources/META-INF/spring/test.xml new file mode 100644 index 00000000..daac473b --- /dev/null +++ b/src/main/resources/META-INF/spring/test.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/test/java/org/springframework/roo/shell/AbstractShellTest.java b/src/test/java/org/springframework/roo/shell/AbstractShellTest.java new file mode 100644 index 00000000..0030b52c --- /dev/null +++ b/src/test/java/org/springframework/roo/shell/AbstractShellTest.java @@ -0,0 +1,31 @@ +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/src/test/java/org/springframework/roo/shell/CliOptionContextTest.java b/src/test/java/org/springframework/roo/shell/CliOptionContextTest.java new file mode 100644 index 00000000..461de817 --- /dev/null +++ b/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 { + + // Constants + private static final String OPTION_CONTEXT = "anything"; + + @Test + public void testGetOptionContextWhenNoneSet() { + assertNull(CliOptionContext.getOptionContext()); + } + + @Test + public void testSetAndGetOptionContext() { + // Set up + CliOptionContext.setOptionContext(OPTION_CONTEXT); + + // Invoke and check + assertEquals(OPTION_CONTEXT, CliOptionContext.getOptionContext()); + } + + @Test + public void testResetOptionContext() { + // Set up + CliOptionContext.setOptionContext(OPTION_CONTEXT); + + // Invoke + CliOptionContext.resetOptionContext(); + + // Check + assertNull(CliOptionContext.getOptionContext()); + } +} diff --git a/src/test/java/org/springframework/roo/shell/MethodTargetTest.java b/src/test/java/org/springframework/roo/shell/MethodTargetTest.java new file mode 100644 index 00000000..179aa2ba --- /dev/null +++ b/src/test/java/org/springframework/roo/shell/MethodTargetTest.java @@ -0,0 +1,52 @@ +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 { + + // Constants + 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 testInstanceEqualsItself() { + final MethodTarget instance = new MethodTarget(METHOD_1, TARGET_1); + assertEquals(instance, instance); + } + + @Test + public void testInstanceDoesNotEqualNull() { + assertFalse(new MethodTarget(METHOD_1, TARGET_1).equals(null)); + } + + @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()); + } + + @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))); + } +} diff --git a/src/test/java/org/springframework/roo/shell/SimpleParserTest.java b/src/test/java/org/springframework/roo/shell/SimpleParserTest.java new file mode 100644 index 00000000..75fa1491 --- /dev/null +++ b/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; + + @Before + public void setUp() { + this.simpleParser = new SimpleParser(); + } + + @Test + public void testNormaliseEmptyString() { + assertNormalised("", ""); + } + + @Test + public void testNormaliseSpaces() { + assertNormalised(" ", ""); + } + + @Test + public void testNormaliseSingleWord() { + assertNormalised("hint", "hint"); + } + + @Test + public void testNormaliseMultipleWords() { + assertNormalised(" security setup ", "security setup"); + } + + /** + * 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)); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/AnsiEscapeCodeTest.java b/src/test/java/org/springframework/roo/support/util/AnsiEscapeCodeTest.java new file mode 100644 index 00000000..370ec937 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/AnsiEscapeCodeTest.java @@ -0,0 +1,54 @@ +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 testDecorateNullText() { + assertNull(AnsiEscapeCode.decorate(null, AnsiEscapeCode.values()[0])); + } + + @Test + public void testDecorateEmptyText() { + assertEquals("", AnsiEscapeCode.decorate("", 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/src/test/java/org/springframework/roo/support/util/CollectionUtilsTest.java b/src/test/java/org/springframework/roo/support/util/CollectionUtilsTest.java new file mode 100644 index 00000000..5295a3bd --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/CollectionUtilsTest.java @@ -0,0 +1,156 @@ +package org.springframework.roo.support.util; + +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +/** + * Unit test of {@link CollectionUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class CollectionUtilsTest { + + // 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.hasText(instance); + } + }; + + private static class Parent { + @Override + public String toString() { + return getClass().getSimpleName(); + } + } + + private static class Child extends Parent {} + + @Test + public void testFilterNullCollection() { + assertEquals(0, CollectionUtils.filter(null, NON_BLANK_FILTER).size()); + } + + @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 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 testAddNullCollectionToNullCollection() { + assertFalse(CollectionUtils.addAll(null, null)); + } + + @Test + public void testAddNullCollectionToNonNullCollection() { + // Set up + final Parent parent = new Parent(); + final Collection parents = Arrays.asList(parent); + + // Invoke + final boolean added = CollectionUtils.addAll(null, parents); + + // Check + assertFalse(added); + } + + @Test + public void testAddNonNullCollectionToNonNullCollection() { + // Set up + final Parent parent = new Parent(); + final Child child = new Child(); + final Collection parents = new ArrayList(); + parents.add(parent); + + // Invoke + final boolean added = CollectionUtils.addAll(Arrays.asList(child), parents); + + // Check + assertTrue(added); + assertEquals(Arrays.asList(parent, child), parents); + } + + @Test + public void testPopulateNullCollectionWithNullCollection() { + assertNull(CollectionUtils.populate(null, null)); + } + + @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 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 testFirstElementOfNullCollection() { + assertNull(CollectionUtils.firstElementOf(null)); + } + + @Test + public void testFirstElementOfEmptyCollection() { + assertNull(CollectionUtils.firstElementOf(Collections.emptySet())); + } + + @Test + public void testFirstElementOfSingleElementCollection() { + final String member = "x"; + assertEquals(member, CollectionUtils.firstElementOf(Collections.singleton(member))); + } + + @Test + public void testFirstElementOfMultiElementCollection() { + final String[] members = {"x", "y", "z"}; + assertEquals(members[0], CollectionUtils.firstElementOf(Arrays.asList(members))); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/DomUtilsTest.java b/src/test/java/org/springframework/roo/support/util/DomUtilsTest.java new file mode 100644 index 00000000..b1796374 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/DomUtilsTest.java @@ -0,0 +1,77 @@ +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.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 { + + // Constants + private static final String DEFAULT_TEXT = "foo"; + private static final String NODE_TEXT = "bar"; + private static final String XML_BEFORE_REMOVAL = + "" + + " " + + " " + + " " + + " " + + ""; + private static final String XML_AFTER_REMOVAL = + "\n" + + " \n" + + ""; + + /** + * 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 + */ + private 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", StringUtils.LINE_SEPARATOR); + // Trim trailing whitespace as XmlUtils.nodeToString appends an extra newline. + final String actualXml = StringUtils.trimTrailingWhitespace(XmlUtils.nodeToString(actualNode)); + assertEquals(normalisedXml, actualXml); + } + + @Test + public void testGetTextContentOfNullNode() { + assertEquals(DEFAULT_TEXT, DomUtils.getTextContent(null, DEFAULT_TEXT)); + } + + @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 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 + assertXmlEquals(XML_AFTER_REMOVAL, root); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/FileUtilsTest.java b/src/test/java/org/springframework/roo/support/util/FileUtilsTest.java new file mode 100644 index 00000000..38c5eb5e --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/FileUtilsTest.java @@ -0,0 +1,221 @@ +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.io.InputStreamReader; + +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"; + + @Test(expected = NullPointerException.class) + public void testGetSystemDependentPathFromNullArray() { + FileUtils.getSystemDependentPath((String[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetSystemDependentPathFromNoElements() { + FileUtils.getSystemDependentPath(); + } + + @Test + public void testGetSystemDependentPathFromOneElement() { + assertEquals("foo", FileUtils.getSystemDependentPath("foo")); + } + + @Test + public void testGetSystemDependentPathFromMultipleElements() { + final String expectedPath = "foo" + File.separator + "bar"; + assertEquals(expectedPath, FileUtils.getSystemDependentPath("foo", "bar")); + } + + @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 testRemoveTrailingSeparatorFromNullPath() { + assertNull(FileUtils.removeTrailingSeparator(null)); + } + + @Test + public void testRemoveTrailingSeparatorFromEmptyPath() { + assertEquals("", FileUtils.removeTrailingSeparator("")); + } + + @Test + public void testRemoveTrailingSeparatorFromPathWithLeadingSeparator() { + final String path = File.separator + "foo"; + assertEquals(path, FileUtils.removeTrailingSeparator(path)); + } + + @Test + public void testRemoveTrailingSeparatorFromPathWithMultipleTrailingSeparators() { + final String path = "foo" + StringUtils.repeat(File.separator, 3); + assertEquals("foo", FileUtils.removeTrailingSeparator(path)); + } + + @Test(expected = IllegalArgumentException.class) + public void testEnsureTrailingSeparatorForNullPath() { + FileUtils.ensureTrailingSeparator(null); + } + + @Test + public void testEnsureTrailingSeparatorForEmptyPath() { + assertEquals(File.separator, FileUtils.ensureTrailingSeparator("")); + } + + @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 + public void testGetCanonicalPathForNullFile() { + assertNull(FileUtils.getCanonicalPath(null)); + } + + @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 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 testRemoveLeadingAndTrailingSeparatorsFromNullPath() { + assertNull(FileUtils.removeLeadingAndTrailingSeparators(null)); + } + + @Test + public void testRemoveLeadingAndTrailingSeparatorsFromEmptyPath() { + assertEquals("", FileUtils.removeLeadingAndTrailingSeparators("")); + } + + @Test + public void testRemoveLeadingAndTrailingSeparatorsFromPlainPath() { + final String path = "foo"; + assertEquals(path, FileUtils.removeLeadingAndTrailingSeparators(path)); + } + + @Test + public void testRemoveLeadingAndTrailingSeparatorsFromPathWithBoth() { + // Set up + final String separators = StringUtils.repeat(File.separator, 4); + final String path = separators + "foo" + separators; + + // Invoke and check + assertEquals("foo", FileUtils.removeLeadingAndTrailingSeparators(path)); + } + + @Test + public void testGetFile() { + assertTrue(FileUtils.getFile(Loader.class, TEST_FILE).isFile()); + } + + @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 testGetInputStreamOfFileInSubDirectory() throws Exception { + // Invoke + final InputStream inputStream = FileUtils.getInputStream(Loader.class, TEST_FILE); + + // Check + final String contents = FileCopyUtils.copyToString(new InputStreamReader(inputStream)); + assertEquals("This file is required for FileUtilsTest.", contents); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetInputStreamOfInvalidFile() throws Exception { + FileUtils.getInputStream(Loader.class, MISSING_FILE); + } + + private void assertFirstDirectory(final String path, final String expectedFirstDirectory) { + // Invoke + final String firstDirectory = FileUtils.getFirstDirectory(path); + + // Check + assertEquals(expectedFirstDirectory, firstDirectory); + } + + @Test + public void testGetFirstDirectoryOfExistingDirectory() { + // Set up + final String directory = FileUtils.getFile(Loader.class, TEST_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 testBackOneDirectory() { + assertEquals("foo" + File.separator + "bar", FileUtils.backOneDirectory("foo" + File.separator + "bar" + File.separator + "baz" + File.separator)); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/IOUtilsTest.java b/src/test/java/org/springframework/roo/support/util/IOUtilsTest.java new file mode 100644 index 00000000..d233836d --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/IOUtilsTest.java @@ -0,0 +1,51 @@ +package org.springframework.roo.support.util; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.Closeable; +import java.io.IOException; + +import org.junit.Test; + +/** + * Unit test of {@link IOUtils}. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class IOUtilsTest { + + @Test + public void testCloseNullCloseable() { + IOUtils.closeQuietly((Closeable) null); // Shouldn't throw an exception + } + + @Test + public void testCloseNonNullCloseableWithoutError() throws Exception { + // Set up + final Closeable mockCloseable = mock(Closeable.class); + + // Invoke + IOUtils.closeQuietly(mockCloseable); + + // Check + verify(mockCloseable).close(); + } + + @Test + public void testCloseTwoNonNullCloseableWithErrorOnFirst() throws Exception { + // Set up + final Closeable mockCloseable1 = mock(Closeable.class); + doThrow(new IOException("dummy")).when(mockCloseable1).close(); + final Closeable mockCloseable2 = mock(Closeable.class); + + // Invoke + IOUtils.closeQuietly(mockCloseable1, mockCloseable2); + + // Check + verify(mockCloseable1).close(); + verify(mockCloseable2).close(); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/NumberUtilsTest.java b/src/test/java/org/springframework/roo/support/util/NumberUtilsTest.java new file mode 100644 index 00000000..849af69c --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/NumberUtilsTest.java @@ -0,0 +1,73 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Unit test of {@link NumberUtils}. + * + * @author Alan Stewart + * @since 1.2.0 + */ +public class NumberUtilsTest { + + @Test + public void testMinValueOfEmptyArray() { + assertNull(NumberUtils.min(new Number[0])); + } + + @Test + public void testNullMinValues() { + assertNull(NumberUtils.min(null, null)); + } + + @Test + public void testOneMinValue() { + assertEquals(BigDecimal.ONE, NumberUtils.min(1)); + } + + @Test + public void testMinValues() { + assertEquals(new BigDecimal("11"), NumberUtils.min(21, 11, 20L, 33.3D, new Short("55"), 11.3)); + } + + @Test + public void testMinValues2() { + assertEquals(new BigDecimal("3"), NumberUtils.min(null, 3, null, 4)); + } + + @Test + public void testMultipleMinValues() { + assertEquals(new BigDecimal(-10), NumberUtils.min(0, 10, null, -10, Integer.MAX_VALUE, BigInteger.TEN)); + } + + @Test + public void testMultipleSameMinValues() { + assertEquals(-1, NumberUtils.min(-1, -1F, -1L, -1D).intValueExact()); + } + + @Test + public void testNullMaxValues() { + assertNull(NumberUtils.max(null, null)); + } + + @Test + public void testOneMaxValue() { + assertEquals(BigDecimal.ONE, NumberUtils.max(1)); + } + + @Test + public void testMaxValues() { + assertEquals(BigDecimal.ONE, NumberUtils.max(null, 1, -1, null)); + } + + @Test + public void testMultipleMaxValues() { + assertEquals(new BigDecimal(String.valueOf(Double.MAX_VALUE)), NumberUtils.max(0, null, Integer.MIN_VALUE, 10, -10, Integer.MAX_VALUE, Long.MAX_VALUE, Double.MAX_VALUE)); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/ObjectUtilsTest.java b/src/test/java/org/springframework/roo/support/util/ObjectUtilsTest.java new file mode 100644 index 00000000..8195e553 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/ObjectUtilsTest.java @@ -0,0 +1,105 @@ +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 org.junit.Test; + +/** + * Unit test of {@link ObjectUtils} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class ObjectUtilsTest { + + @Test + public void testCompareTwoNulls() { + assertEquals(0, ObjectUtils.nullSafeComparison(null, null)); + } + + @Test + public void testCompareNullWithNonNull() { + // Invoke + final int result = ObjectUtils.nullSafeComparison(null, ""); + + // Check + assertTrue(result < 0); + } + + @Test + public void testCompareNonNullWithNull() { + // Invoke + final int result = ObjectUtils.nullSafeComparison("", null); + + // Check + assertTrue(result > 0); + } + + @Test + public void testCompareLesserWithGreater() { + // Invoke + final int result = ObjectUtils.nullSafeComparison(100, 200); + + // Check + assertTrue(result < 0); + } + + @Test + public void testCompareGreaterWithLesser() { + // Invoke + final int result = ObjectUtils.nullSafeComparison(300, 200); + + // Check + assertTrue(result > 0); + } + + @Test + public void testCompareTwoEqualObjects() { + assertEquals(0, ObjectUtils.nullSafeComparison(400, 400)); + } + + @Test + public void testToStringWithNullObjectAndNullDefault() { + assertNull(ObjectUtils.toString(null, null)); + } + + @Test + public void testToStringWithNullObjectAndEmptyDefault() { + assertEquals("", ObjectUtils.toString(null, "")); + } + + @Test + public void testToStringWithNullObjectAndNonEmptyDefault() { + assertEquals("x", ObjectUtils.toString(null, "x")); + } + + @Test + public void testToStringWithNonNullObjectAndNullDefault() { + assertEquals("1", ObjectUtils.toString(1, "anything")); + } + + @Test + public void testDefaultIfNullWhenObjectIsNullAndDefaultIsNull() { + assertNull(ObjectUtils.defaultIfNull(null, null)); + } + + @Test + public void testDefaultIfNullWhenObjectIsNullAndDefaultIsNotNull() { + final Object defaultValue = 27; + assertEquals(defaultValue, ObjectUtils.defaultIfNull(null, defaultValue)); + } + + @Test + public void testDefaultIfNullWhenObjectIsNotNullAndDefaultIsNull() { + final Object value = 27; + assertEquals(value, ObjectUtils.defaultIfNull(value, null)); + } + + @Test + public void testDefaultIfNullWhenObjectIsNotNullAndDefaultIsNotNull() { + final Integer value = 27; + assertEquals(value, ObjectUtils.defaultIfNull(value, value + 1)); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/PairListTest.java b/src/test/java/org/springframework/roo/support/util/PairListTest.java new file mode 100644 index 00000000..3c1fa193 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/PairListTest.java @@ -0,0 +1,59 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +/** + * Unit test of {@link PairList} + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PairListTest { + + // Constants + 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 Pair PAIR_1 = new Pair(KEY_1, VALUE_1); + private static final Pair PAIR_2 = new Pair(KEY_2, VALUE_2); + + @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)); + } + + @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()); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/PairTest.java b/src/test/java/org/springframework/roo/support/util/PairTest.java new file mode 100644 index 00000000..7970b46b --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/PairTest.java @@ -0,0 +1,51 @@ +package org.springframework.roo.support.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +/** + * Unit test of the {@link Pair} class. + * + * @author Andrew Swan + * @since 1.2.0 + */ +public class PairTest { + + @Test + public void testConstructWithNullKey() { + new Pair(null, ""); + } + + @Test + public void testConstructWithNullValue() { + new Pair("", null); + } + + @Test + public void testInstanceEqualsItself() { + final Pair pair = new Pair(1, "a"); + assertEquals(pair, pair); + } + + @Test + public void testEqualKeyAndValueAreEqual() { + assertEquals(new Pair(1, "a"), new Pair(1, "a")); + } + + @Test + public void testUnequalKeyIsNotEqual() { + assertFalse(new Pair(1, "a").equals(new Pair(2, "a"))); + } + + @Test + public void testUnequalValueIsNotEqual() { + assertFalse(new Pair(1, "a").equals(new Pair(1, "b"))); + } + + @Test + public void testOtherClassIsNotAPair() { + assertFalse(new Pair(1, "a").equals("foo")); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/StringUtilsTest.java b/src/test/java/org/springframework/roo/support/util/StringUtilsTest.java new file mode 100644 index 00000000..837e3104 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/StringUtilsTest.java @@ -0,0 +1,517 @@ +package org.springframework.roo.support.util; + +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 java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +/** + * Unit tests for {@link StringUtils}. + * + * @author Alan Stewart + * @since 1.1.3 + */ +public class StringUtilsTest { + + @Test + public void testPadRight1() { + assertEquals("9999", StringUtils.padRight("9", 4, '9')); + } + + @Test + public void testPadRight2() { + assertEquals("Foo999", StringUtils.padRight("Foo", 6, '9')); + } + + @Test + public void testPadLeft1() { + assertEquals("999", StringUtils.padLeft("9", 3, '9')); + } + + @Test + public void testPadLeft2() { + assertEquals("99Foo", StringUtils.padLeft("Foo", 5, '9')); + } + + @Test + public void testHasText1() { + assertTrue(StringUtils.hasText("11111")); + } + + @Test + public void testHasText2() { + assertFalse(StringUtils.hasText(" ")); + } + + @Test + public void testCountOccurrences() { + assertEquals(4, StringUtils.countOccurrencesOf("Alan Keith Stewart - Triathlete", " ")); + } + + @Test + public void testCountOccurrencesNull() { + assertEquals(0, StringUtils.countOccurrencesOf("Alan Keith Stewart - Triathlete", null)); + } + + @Test + public void testRepeatNull() { + assertNull(StringUtils.repeat(null, 27)); + } + + @Test + public void testRepeatEmptyString() { + assertEquals("", StringUtils.repeat("", 42)); + } + + @Test + public void testRepeatSpace() { + assertEquals(" ", StringUtils.repeat(" ", 4)); + } + + @Test + public void testRepeatSingleCharacter() { + assertEquals("qqq", StringUtils.repeat("q", 3)); + } + + @Test + public void testRepeatMultipleCharacters() { + assertEquals("xyzxyzxyzxyz", StringUtils.repeat("xyz", 4)); + } + + @Test + public void testPrefixNullWithNull() { + assertNull(StringUtils.prefix(null, null)); + } + + @Test + public void testPrefixNullWithEmpty() { + assertNull(StringUtils.prefix(null, "")); + } + + @Test + public void testPrefixNullWithNonEmpty() { + assertNull(StringUtils.prefix(null, "anything")); + } + + @Test + public void testPrefixEmptyWithNull() { + assertEquals("", StringUtils.prefix("", null)); + } + + @Test + public void testPrefixEmptyWithEmpty() { + assertEquals("", StringUtils.prefix("", "")); + } + + @Test + public void testPrefixEmptyWithNonEmpty() { + assertEquals("x", StringUtils.prefix("", "x")); + } + + @Test + public void testPrefixNonEmptyWithNewPrefix() { + assertEquals("pre-old", StringUtils.prefix("old", "pre-")); + } + + @Test + public void testPrefixNonEmptyWithExistingPrefix() { + assertEquals("pre-old", StringUtils.prefix("pre-old", "pre-")); + } + + @Test + public void testRemoveNullSuffixFromNullString() { + assertNull(StringUtils.removeSuffix(null, null)); + } + + @Test + public void testRemoveEmptySuffixFromNullString() { + assertNull(StringUtils.removeSuffix(null, "")); + } + + @Test + public void testRemoveNonEmptySuffixFromNullString() { + assertNull(StringUtils.removeSuffix(null, "anything")); + } + + @Test + public void testRemoveNullSuffixFromEmptyString() { + assertEquals("", StringUtils.removeSuffix("", null)); + } + + @Test + public void testRemoveEmptySuffixFromEmptyString() { + assertEquals("", StringUtils.removeSuffix("", "")); + } + + @Test + public void testRemoveNonEmptySuffixFromEmptyString() { + assertEquals("", StringUtils.removeSuffix("", "anything")); + } + + @Test + public void testRemoveMatchingSuffixFromString() { + assertEquals("a", StringUtils.removeSuffix("abc", "bc")); + } + + @Test + public void testRemoveNonMatchingSuffixFromString() { + assertEquals("abc", StringUtils.removeSuffix("abc", "BC")); + } + + @Test + public void testRemoveNullPrefixFromNullString() { + assertNull(StringUtils.removePrefix(null, null)); + } + + @Test + public void testRemoveEmptyPrefixFromNullString() { + assertNull(StringUtils.removePrefix(null, "")); + } + + @Test + public void testRemoveNonEmptyPrefixFromNullString() { + assertNull(StringUtils.removePrefix(null, "anything")); + } + + @Test + public void testRemoveNullPrefixFromEmptyString() { + assertEquals("", StringUtils.removePrefix("", null)); + } + + @Test + public void testRemoveEmptyPrefixFromEmptyString() { + assertEquals("", StringUtils.removePrefix("", "")); + } + + @Test + public void testRemoveNonEmptyPrefixFromEmptyString() { + assertEquals("", StringUtils.removePrefix("", "anything")); + } + + @Test + public void testRemoveMatchingPrefixFromString() { + assertEquals("c", StringUtils.removePrefix("abc", "ab")); + } + + @Test + public void testRemoveNonMatchingPrefixFromString() { + assertEquals("abc", StringUtils.removePrefix("abc", "AB")); + } + + @Test + public void testSuffixNullWithNull() { + assertNull(StringUtils.suffix(null, null)); + } + + @Test + public void testSuffixNullWithEmpty() { + assertNull(StringUtils.suffix(null, "")); + } + + @Test + public void testSuffixNullWithNonEmpty() { + assertNull(StringUtils.suffix(null, "anything")); + } + + @Test + public void testSuffixEmptyWithNull() { + assertEquals("", StringUtils.suffix("", null)); + } + + @Test + public void testSuffixEmptyWithEmpty() { + assertEquals("", StringUtils.suffix("", "")); + } + + @Test + public void testSuffixEmptyWithNonEmpty() { + assertEquals("x", StringUtils.suffix("", "x")); + } + + @Test + public void testSuffixNonEmptyWithNewSuffix() { + assertEquals("old-suf", StringUtils.suffix("old", "-suf")); + } + + @Test + public void testSuffixNonEmptyWithExistingSuffix() { + assertEquals("old-suf", StringUtils.suffix("old-suf", "-suf")); + } + + @Test + public void testNullEqualsNull() { + assertTrue(StringUtils.equals(null, null)); + } + + @Test + public void testEmptyDoesNotEqualNull() { + assertFalse(StringUtils.equals("", null)); + } + + @Test + public void testNullDoesNotEqualEmpty() { + assertFalse(StringUtils.equals(null, "")); + } + + @Test + public void testUpperDoesNotEqualLower() { + assertFalse(StringUtils.equals("E", "e")); + } + + @Test + public void testStringEqualsItself() { + assertTrue(StringUtils.equals("a", "a")); + } + + @Test + public void testNullCollectionToDelimitedString() { + assertEquals("", StringUtils.collectionToDelimitedString(null, "anything")); + } + + @Test + public void testEmptyCollectionToDelimitedString() { + assertEquals("", StringUtils.collectionToDelimitedString(Collections.emptySet(), "anything")); + } + + @Test + public void testSingletonCollectionToDelimitedString() { + assertEquals("foo", StringUtils.collectionToDelimitedString(Collections.singleton("foo"), "anything")); + } + + @Test + public void testDoubletonCollectionToDelimitedString() { + assertEquals("foo:bar", StringUtils.collectionToDelimitedString(Arrays.asList("foo", "bar"), ":")); + } + + @Test + public void testNullIsBlank() { + assertTrue(StringUtils.isBlank(null)); + } + + @Test + public void testEmptyStringIsBlank() { + assertTrue(StringUtils.isBlank("")); + } + + @Test + public void testSingleSpaceStringIsBlank() { + assertTrue(StringUtils.isBlank(" ")); + } + + @Test + public void testWhitespaceIsBlank() { + assertTrue(StringUtils.isBlank("\n\r\t ")); + } + + @Test + public void testNonBlankStringIsNotBlank() { + assertFalse(StringUtils.isBlank("x")); + } + + @Test + public void testArrayToDelimitedStringWithNullArray() { + assertEquals("", StringUtils.arrayToDelimitedString(";", new Object[0])); + } + + @Test + public void testArrayToDelimitedStringWithEmptyArray() { + assertEquals("", StringUtils.arrayToDelimitedString(";")); + } + + @Test + public void testArrayToDelimitedStringWithSingleElementArray() { + assertEquals("foo", StringUtils.arrayToDelimitedString(";", "foo")); + } + + @Test + public void testArrayToDelimitedStringWithMultiElementArray() { + assertEquals("foo;27", StringUtils.arrayToDelimitedString(";", "foo", 27)); + } + + @Test + public void testDefaultIfEmptyWhenValueIsNullAndNoDefaults() { + assertNull(StringUtils.defaultIfEmpty(null)); + } + + @Test + public void testDefaultIfEmptyWhenValueIsEmptyAndNoDefaults() { + assertEquals("", StringUtils.defaultIfEmpty("")); + } + + @Test + public void testDefaultIfEmptyWhenValueIsNullAndOneDefault() { + assertEquals("x", StringUtils.defaultIfEmpty(null, "x")); + } + + @Test + public void testDefaultIfEmptyWhenValueIsEmptyAndOneDefault() { + assertEquals("x", StringUtils.defaultIfEmpty("", "x")); + } + + @Test + public void testDefaultIfEmptyWhenAllValuesAreBlank() { + assertEquals("", StringUtils.defaultIfEmpty(null, null, null, "")); + } + + @Test + public void testDefaultIfEmptyWhenValueIsEmptyAndTwoDefaults() { + assertEquals("x", StringUtils.defaultIfEmpty("", null, "x")); + } + + @Test + public void testReplaceAllWhenNoArgumentsAreBlank() { + // Deliberately chose characters with special meaning to regexs + assertEquals("[a[b[c[", StringUtils.replace(".a.b.c.", ".", "[")); + } + + @Test + public void testReplaceAllWhenReplacementIsNull() { + assertEquals(" ", StringUtils.replace(" ", " ", null)); + } + + @Test + public void testReplaceAllWhenReplacementIsEmpty() { + assertEquals(" a ", StringUtils.replace(" a b ", "b", "")); + } + + @Test + public void testReplaceAllWhenReplacementIsWhitespace() { + assertEquals(" a ", StringUtils.replace(" a b ", "b", " ")); + } + + @Test + public void testReplaceAllWhenToReplaceIsNull() { + assertEquals(" ", StringUtils.replace(" ", null, "x")); + } + + @Test + public void testReplaceAllWhenToReplaceIsEmpty() { + assertEquals(" ", StringUtils.replace(" ", "", "x")); + } + + @Test + public void testReplaceAllWhenToReplaceIsWhiteSpace() { + assertEquals("x", StringUtils.replace(" ", " ", "x")); + } + + @Test + public void testReplaceAllWhenOriginalIsNull() { + assertNull(StringUtils.replace(null, "x", "y")); + } + + @Test + public void testReplaceAllWhenOriginalIsEmpty() { + assertEquals("", StringUtils.replace("", "x", "y")); + } + + @Test + public void testReplaceFirstWhenOriginalIsNull() { + assertNull(StringUtils.replaceFirst(null, "x", "y")); + } + + @Test + public void testReplaceFirstWhenOriginalIsEmpty() { + assertEquals("", StringUtils.replaceFirst("", "x", "y")); + } + + @Test + public void testReplaceFirstWhenOriginalIsWhitespace() { + assertEquals("[ ", StringUtils.replaceFirst(" ", " ", "[")); + } + + @Test + public void testReplaceFirstWhenToReplaceIsNull() { + assertEquals(" ", StringUtils.replaceFirst(" ", null, "x")); + } + + @Test + public void testReplaceFirstWhenToReplaceIsEmpty() { + assertEquals(" ", StringUtils.replaceFirst(" ", "", "x")); + } + + @Test + public void testReplaceFirstWhenToReplaceIsWhitespace() { + assertEquals("x", StringUtils.replaceFirst("x", " ", "y")); + } + + @Test + public void testReplaceFirstWhenReplacementIsNull() { + assertEquals("x", StringUtils.replaceFirst("x", "x", null)); + } + + @Test + public void testReplaceFirstWhenReplacementIsEmpty() { + assertEquals("x", StringUtils.replaceFirst("xx", "x", "")); + } + + @Test + public void testReplaceFirstWhenReplacementIsWhitespace() { + assertEquals("x ", StringUtils.replaceFirst("x y", "y", " ")); + } + + @Test + public void testReplaceFirstWhenNoArgumentsAreBlank() { + assertEquals("x-yz", StringUtils.replaceFirst("xyyz", "y", "-")); + } + + private static final String[][] SUBSTRING_AFTER_LAST_SCENARIOS = { + // 0 = original, 1 = separator, 2 = expected result + {null, "anything", null}, + {"", "anything", ""}, + {"anything", "", ""}, + {"anything", null, ""}, + {"abc", "a", "bc"}, + {"abcba", "b", "a"}, + {"abc", "c", ""}, + {"a", "a", ""}, + {"a", "z", ""} + }; + + @Test + public void testSubstringAfterLast() { + for (final String[] scenario : SUBSTRING_AFTER_LAST_SCENARIOS) { + assertEquals(scenario[2], StringUtils.substringAfterLast(scenario[0], scenario[1])); + } + } + + private static final String[][] CONTAINS_SCENARIOS = { + {null, "anything", "false"}, + {"anything", null, "false"}, + {"", "", "true"}, + {"abc", "", "true"}, + {"abc", "a", "true"}, + {"abc", "b", "true"}, + {"abc", "c", "true"}, + {"abc", "z", "false"} + }; + + @Test + public void testContains() { + for (final String[] scenario : CONTAINS_SCENARIOS) { + assertEquals("Failed on scenario " + Arrays.toString(scenario), Boolean.valueOf(scenario[2]), StringUtils.contains(scenario[0], scenario[1])); + } + } + + @Test + public void testTrimToEmpty() { + String path = " "; + assertEquals(" roo>", StringUtils.trimToEmpty(path) + " roo>"); + } + + @Test + public void testTrimToEmpty2() { + String path = null; + assertEquals(" roo>", StringUtils.trimToEmpty(path) + " roo>"); + } + + @Test + public void testTrimToEmpty3() { + String path = "core"; + assertEquals("core roo>", StringUtils.trimToEmpty(path) + " roo>"); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/WebXmlUtilsTest.java b/src/test/java/org/springframework/roo/support/util/WebXmlUtilsTest.java new file mode 100644 index 00000000..da88ed61 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/WebXmlUtilsTest.java @@ -0,0 +1,237 @@ +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.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()); + } + + @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()); + } + + @Test + public void validateElementSequence() { + final List contents = XmlUtils.findElements("/web-app/*", webXml.getDocumentElement()); + + assertEquals(17, contents.size()); + assertEquals("display-name", contents.get(0).getNodeName()); + assertEquals("description", contents.get(1).getNodeName()); + assertEquals("context-param", contents.get(2).getNodeName()); + assertEquals("filter2", contents.get(3).getChildNodes().item(0).getTextContent()); + assertEquals("filter3", contents.get(4).getChildNodes().item(0).getTextContent()); + assertEquals("filter1", contents.get(5).getChildNodes().item(0).getTextContent()); + assertEquals("filter2", contents.get(6).getChildNodes().item(0).getTextContent()); + assertEquals("filter3", contents.get(7).getChildNodes().item(0).getTextContent()); + assertEquals("filter1", contents.get(8).getChildNodes().item(0).getTextContent()); + assertEquals("listener", contents.get(9).getNodeName()); + assertEquals("servlet", contents.get(10).getNodeName()); + assertEquals("servlet-mapping", contents.get(11).getNodeName()); + assertEquals("session-config", contents.get(12).getNodeName()); + assertEquals("welcome-file-list", contents.get(13).getNodeName()); + assertEquals("error-page", contents.get(14).getNodeName()); + assertEquals("error-page", contents.get(15).getNodeName()); + assertEquals("security-constraint", contents.get(16).getNodeName()); + } +} diff --git a/src/test/java/org/springframework/roo/support/util/loader/Loader.java b/src/test/java/org/springframework/roo/support/util/loader/Loader.java new file mode 100644 index 00000000..ec7b1a56 --- /dev/null +++ b/src/test/java/org/springframework/roo/support/util/loader/Loader.java @@ -0,0 +1,11 @@ +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/src/test/resources/org/springframework/roo/support/util/loader/sub/file-utils-test.txt b/src/test/resources/org/springframework/roo/support/util/loader/sub/file-utils-test.txt new file mode 100644 index 00000000..501d1441 --- /dev/null +++ b/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