diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-20 22:46:03 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-20 22:47:41 +0100 |
commit | 2b43df61e8ab7ded55f565eb7f43528ece3d66f3 (patch) | |
tree | c45ac8c139e9ef2dc783fe5aafa70e1f4060550e | |
parent | 69515943c83b24c8dbefefe7420bab6d5de4c81f (diff) | |
download | sonarqube-2b43df61e8ab7ded55f565eb7f43528ece3d66f3.tar.gz sonarqube-2b43df61e8ab7ded55f565eb7f43528ece3d66f3.zip |
SONAR-3344 Display metadata of SonarSource licenses
16 files changed, 414 insertions, 17 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java b/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java index 356e4c1a65c..aacf82cd1ce 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java @@ -19,6 +19,52 @@ */ package org.sonar.api; +/** + * @since 2.15 + */ public enum PropertyType { - STRING, TEXT, PASSWORD, BOOLEAN, INTEGER, FLOAT, SINGLE_SELECT_LIST + /** + * Basic single line input field + */ + STRING, + + /** + * Multiple line text-area + */ + TEXT, + + /** + * Variation of {#STRING} with masked characters + */ + PASSWORD, + + /** + * True/False + */ + BOOLEAN, + + /** + * Integer value, positive or negative + */ + INTEGER, + + /** + * Floating point number + */ + FLOAT, + + /** + * Single select list with a list of options + */ + SINGLE_SELECT_LIST, + + /** + * Sonar Metric + */ + METRIC, + + /** + * SonarSource license + */ + LICENSE } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/License.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/License.java new file mode 100644 index 00000000000..bfc0cab23df --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/License.java @@ -0,0 +1,123 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.DateUtils; + +import javax.annotation.Nullable; +import java.io.StringReader; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * SonarSource license. This class aims to extract metadata but not to validate or - of course - + * to generate license + * + * @since 2.15 + */ +public final class License { + private String product; + private String organization; + private String expirationDate; + private String type; + private String server; + + private License(Map<String, String> properties) { + product = StringUtils.defaultString(properties.get("Product"), properties.get("Plugin")); + organization = StringUtils.defaultString(properties.get("Organisation"), properties.get("Name")); + expirationDate = StringUtils.defaultString(properties.get("Expiration"), properties.get("Expires")); + type = properties.get("Type"); + server = properties.get("Server"); + } + + @Nullable + public String getProduct() { + return product; + } + + @Nullable + public String getOrganization() { + return organization; + } + + @Nullable + public String getExpirationDateAsString() { + return expirationDate; + } + + @Nullable + public Date getExpirationDate() { + return DateUtils.parseDateQuietly(expirationDate); + } + + public boolean isExpired() { + return isExpired(new Date()); + } + + @VisibleForTesting + boolean isExpired(Date now) { + Date date = getExpirationDate(); + return date != null && !date.after(org.apache.commons.lang.time.DateUtils.truncate(now, Calendar.DATE)); + } + + @Nullable + public String getType() { + return type; + } + + @Nullable + public String getServer() { + return server; + } + + public static License readBase64(String base64) { + return readPlainText(new String(Base64.decodeBase64(base64.getBytes()))); + } + + @VisibleForTesting + static License readPlainText(String data) { + Map<String, String> props = Maps.newHashMap(); + StringReader reader = new StringReader(data); + try { + List<String> lines = IOUtils.readLines(reader); + for (String line : lines) { + if (StringUtils.isNotBlank(line) && line.indexOf(':') > 0) { + String key = StringUtils.substringBefore(line, ":"); + String value = StringUtils.substringAfter(line, ":"); + props.put(StringUtils.trimToEmpty(key), StringUtils.trimToEmpty(value)); + } + } + + } catch (Exception e) { + // silently ignore + + } finally { + IOUtils.closeQuietly(reader); + } + return new License(props); + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java index ab4446eb01e..658d328422c 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java @@ -82,12 +82,17 @@ public final class PropertyDefinition { } private PropertyType fixType(String key, PropertyType type) { - // Auto-detect passwords for old versions of plugins that - // do not declare the type - if (type==PropertyType.STRING && StringUtils.endsWith(key, ".password.secured")) { - return PropertyType.PASSWORD; + // Auto-detect passwords and licenses for old versions of plugins that + // do not declare property types + PropertyType fix = type; + if (type == PropertyType.STRING) { + if (StringUtils.endsWith(key, ".password.secured")) { + fix = PropertyType.PASSWORD; + } else if (StringUtils.endsWith(key, ".license.secured")) { + fix = PropertyType.LICENSE; + } } - return type; + return fix; } @VisibleForTesting diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/LicenseTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/LicenseTest.java new file mode 100644 index 00000000000..37e880e21fd --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/LicenseTest.java @@ -0,0 +1,125 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import org.apache.commons.codec.binary.Base64; +import org.hamcrest.core.Is; +import org.junit.Test; +import org.sonar.api.utils.DateUtils; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class LicenseTest { + + private static final String V2_FORMAT = "Foo: bar\n" + + "Organisation: ABC \n" + + "Server: 12345 \n" + + "Product: SQALE\n" + + " Expiration: 2012-05-18 \n" + + "Type: EVALUATION \n" + + "Other: field\n"; + + private static final String V1_FORMAT = "Foo: bar\n" + + "Name: ABC \n" + + "Plugin: SQALE\n" + + " Expires: 2012-05-18 \n" + + "Other: field\n"; + + @Test + public void readPlainTest() { + License license = License.readPlainText(V2_FORMAT); + + assertThat(license.getOrganization(), Is.is("ABC")); + assertThat(license.getServer(), Is.is("12345")); + assertThat(license.getProduct(), Is.is("SQALE")); + assertThat(license.getExpirationDateAsString(), Is.is("2012-05-18")); + assertThat(license.getType(), Is.is("EVALUATION")); + } + + @Test + public void readPlainText_empty_fields() { + License license = License.readPlainText(""); + + assertThat(license.getOrganization(), nullValue()); + assertThat(license.getServer(), nullValue()); + assertThat(license.getProduct(), nullValue()); + assertThat(license.getExpirationDateAsString(), nullValue()); + assertThat(license.getExpirationDate(), nullValue()); + assertThat(license.getType(), nullValue()); + } + + @Test + public void readPlainText_not_valid_input() { + License license = License.readPlainText("old pond ... a frog leaps in water’s sound"); + + assertThat(license.getOrganization(), nullValue()); + assertThat(license.getServer(), nullValue()); + assertThat(license.getProduct(), nullValue()); + assertThat(license.getExpirationDateAsString(), nullValue()); + assertThat(license.getExpirationDate(), nullValue()); + assertThat(license.getType(), nullValue()); + } + + @Test + public void readPlainTest_version_1() { + License license = License.readPlainText(V1_FORMAT); + + assertThat(license.getOrganization(), Is.is("ABC")); + assertThat(license.getServer(), nullValue()); + assertThat(license.getProduct(), Is.is("SQALE")); + assertThat(license.getExpirationDateAsString(), Is.is("2012-05-18")); + assertThat(license.getType(), nullValue()); + } + + @Test + public void readBase64() { + License license = License.readBase64(new String(Base64.encodeBase64(V2_FORMAT.getBytes()))); + + assertThat(license.getOrganization(), Is.is("ABC")); + assertThat(license.getServer(), Is.is("12345")); + assertThat(license.getProduct(), Is.is("SQALE")); + assertThat(license.getExpirationDateAsString(), Is.is("2012-05-18")); + assertThat(license.getType(), Is.is("EVALUATION")); + } + + @Test + public void readBase64_not_base64() { + License license = License.readBase64("çé '123$@"); + + assertThat(license.getOrganization(), nullValue()); + assertThat(license.getServer(), nullValue()); + assertThat(license.getProduct(), nullValue()); + assertThat(license.getExpirationDateAsString(), nullValue()); + assertThat(license.getExpirationDate(), nullValue()); + assertThat(license.getType(), nullValue()); + } + + @Test + public void isExpired() { + License license = License.readPlainText(V2_FORMAT); + + assertThat(license.isExpired(DateUtils.parseDate("2013-06-23")), is(true)); + assertThat(license.isExpired(DateUtils.parseDate("2012-05-18")), is(true)); + assertThat(license.isExpired(DateUtils.parseDateTime("2012-05-18T15:50:45+0100")), is(true)); + assertThat(license.isExpired(DateUtils.parseDate("2011-01-01")), is(false)); + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionTest.java index 427698793d4..00557bdc231 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionTest.java @@ -158,7 +158,7 @@ public class PropertyDefinitionTest { } @Test - public void autodetectPasswordType() { + public void autoDetectPasswordType() { Properties props = AnnotationUtils.getClassAnnotation(OldScmPlugin.class, Properties.class); Property prop = props.value()[0]; @@ -167,4 +167,21 @@ public class PropertyDefinitionTest { assertThat(def.getKey(), Is.is("scm.password.secured")); assertThat(def.getType(), Is.is(PropertyType.PASSWORD)); } + + @Properties({ + @Property(key = "views.license.secured", name = "Views license") + }) + static class ViewsPlugin { + } + + @Test + public void autoDetectLicenseType() { + Properties props = AnnotationUtils.getClassAnnotation(ViewsPlugin.class, Properties.class); + Property prop = props.value()[0]; + + PropertyDefinition def = PropertyDefinition.create(prop); + + assertThat(def.getKey(), Is.is("views.license.secured")); + assertThat(def.getType(), Is.is(PropertyType.LICENSE)); + } } diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 82f899f3e22..76517e30df8 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -21,6 +21,7 @@ package org.sonar.server.ui; import org.apache.commons.configuration.Configuration; import org.slf4j.LoggerFactory; +import org.sonar.api.config.License; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.config.Settings; import org.sonar.api.platform.ComponentContainer; @@ -410,6 +411,10 @@ public final class JRubyFacade { public String generateRandomSecretKey() { return getContainer().getComponentByType(Settings.class).getEncryption().generateRandomSecretKey(); } + + public License parseLicense(String base64) { + return License.readBase64(base64); + } public ReviewsNotificationManager getReviewsNotificationManager() { diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb index 2508e29c272..97fe97b7f60 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb @@ -38,10 +38,14 @@ value = Property.value(property.getKey(), (@project ? @project.id : nil), '') unless value # for backward-compatibility with properties that do not define the type TEXT - property_type = value.include?("\n") ? 'TEXT' : property.getType() + property_type = property.getType() + if property_type.to_s=='STRING' && value.include?("\n") + property_type = 'TEXT' + end + %> <tr class="<%= cycle('even', 'odd', :name => 'properties') -%>"> - <td style="padding: 10px" id="foo_<%= property.getKey() -%>"> + <td style="padding: 10px" id="block_<%= property.getKey() -%>"> <h3> <%= message("property.#{property.key()}.name", :default => property.name()) -%> <br/><span class="note"><%= property.getKey() -%></span> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb index 55e4fc408f3..9271bbd0c92 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb @@ -1,4 +1,4 @@ -<select name="<%= h property.key -%>" id="input_<%= h property.key-%>"> +<select name="<%= h property.getKey() -%>" id="input_<%= h property.getKey() -%>"> <option value="" <%= 'selected' if value.blank? -%>><%= message('default') -%></option> <option value="true" <%= 'selected' if value=='true' -%>><%= message('true') -%></option> <option value="false" <%= 'selected' if value=='false' -%>><%= message('false') -%></option> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb index 6706e88ecb2..163677b4478 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb @@ -1 +1 @@ -<input type="text" name="<%= h property.key -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.key-%>"/>
\ No newline at end of file +<input type="text" name="<%= h property.getKey() -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.getKey() -%>"/>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb index 6706e88ecb2..5603dfe4a75 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb @@ -1 +1 @@ -<input type="text" name="<%= h property.key -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.key-%>"/>
\ No newline at end of file +<input type="text" name="<%= h property.getKey() -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.getKey()-%>"/>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb new file mode 100644 index 00000000000..69996fa4419 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb @@ -0,0 +1,45 @@ +<% if !value || value.blank? %> + <textarea rows="5" cols="80" class="width100" name="<%= h property.getKey() -%>" id="input_<%= h property.getKey() -%>"></textarea> +<% + else + license = controller.java_facade.parseLicense(value) + date = license.getExpirationDateAsString() +%> + <div class="width100"> + <textarea rows="6" name="<%= h property.getKey() -%>" id="input_<%= h property.getKey() -%>" style="float: left;width: 390px"><%= h value -%></textarea> + + <div style="margin-left: 400px"> + <table> + <tr> + <td class="form-key-cell">Product:</td> + <td class="form-val-cell"><%= license.getProduct() || '-' -%></td> + </tr> + <tr> + <td class="form-key-cell">Organization:</td> + <td><%= license.getOrganization() || '-' -%></td> + </tr> + <tr> + <td class="form-key-cell">Expiration:</td> + <td> + <% if license.getExpirationDateAsString() + formatted_date = l(Date.parse(license.getExpirationDateAsString())) + %> + <%= license.isExpired() ? "<span class='error'>#{formatted_date}</span>" : formatted_date -%> + <% else %> + - + <% end %> + + </td> + </tr> + <tr> + <td class="form-key-cell">Type:</td> + <td><%= license.getType() || '-' -%></td> + </tr> + <tr> + <td class="form-key-cell">Server:</td> + <td><%= license.getServer() || '-' -%></td> + </tr> + </table> + </div> + </div> +<% end %>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb new file mode 100644 index 00000000000..45ed790ecf6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb @@ -0,0 +1,27 @@ +<select name="<%= h property.getKey() -%>" id="input_<%= h property.getKey() -%>"> + <option value=""><%= message('default') -%></option> + <% + metrics_per_domain={} + Metric.all.select { |m| m.display? }.sort_by { |m| m.short_name }.each do |metric| + domain=metric.domain || '' + metrics_per_domain[domain]||=[] + metrics_per_domain[domain]<<metric + end + + metrics_per_domain.keys.sort.each do |domain| +%> + + <optgroup label="<%= h domain -%>"> +<% + metrics_per_domain[domain].each do |m| + selected_attr = (m.key==value || m.id==value) ? " selected='selected'" : '' +%> + <option value="<%= m.key -%>" <%= selected_attr -%>><%= m.short_name -%></option> +<% + end +%> + </optgroup> +<% + end +%> +</select>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb index 3b6d49854b7..e8b70971e02 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb @@ -1 +1 @@ -<input type="password" name="<%= h property.key -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.key-%>"/>
\ No newline at end of file +<input type="password" name="<%= h property.getKey() -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.getKey() -%>"/>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb index a317e48ef38..ac0e0d3d867 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb @@ -1,4 +1,4 @@ -<select name="<%= h property.key -%>" id="input_<%= h property.key-%>"> +<select name="<%= h property.getKey() -%>" id="input_<%= h property.key-%>"> <option value=""><%= message('default') -%></option> <% property.options.each do |option| %> <option value="<%= h option -%>" <%= 'selected' if value && value==option -%>><%= h option -%></option> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb index 0dc07f7c872..b4c3b451f4d 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb @@ -1,2 +1,2 @@ -<input type="text" name="<%= h property.key -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.key-%>"/> -<%= link_to_function(image_tag('zoom.png'), "enlargeTextInput('#{property.key}')", :class => 'nolink') -%>
\ No newline at end of file +<input type="text" name="<%= h property.getKey() -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.getKey() -%>"/> +<%= link_to_function(image_tag('zoom.png'), "enlargeTextInput('#{property.getKey()}')", :class => 'nolink') -%>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb index c557137557e..dfefb03af9f 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb @@ -1 +1 @@ -<textarea rows="5" cols="80" class="width100" name="<%= h property.key -%>" id="input_<%= h property.key-%>"><%= h value if value -%></textarea>
\ No newline at end of file +<textarea rows="5" cols="80" class="width100" name="<%= h property.getKey() -%>" id="input_<%= h property.getKey() -%>"><%= h value if value -%></textarea>
\ No newline at end of file |