From: David Gageot Date: Thu, 20 Sep 2012 15:05:20 +0000 (+0200) Subject: SONAR-3754 API: ability to define a cardinality on a property X-Git-Tag: 3.3~270 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bb889f52fb08f10a175ff64b65edd0929a5a69c6;p=sonarqube.git SONAR-3754 API: ability to define a cardinality on a property --- diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties index 339e93dec12..a8210dbff9f 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -564,6 +564,7 @@ dashboard.username.default=[Sonar] # SETTINGS # #------------------------------------------------------------------------------ +settings.add=Add value settings.save_category=Save {0} Settings property.category.email=Email property.category.encryption=Encryption diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/Property.java b/sonar-plugin-api/src/main/java/org/sonar/api/Property.java index 7cff4f9b49f..46f9d0ebe86 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/Property.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/Property.java @@ -92,4 +92,10 @@ public @interface Property { */ String[] options() default {}; + /** + * Can the property take multiple values. Eg: list of email addresses. + * + * @since 3.3 + */ + boolean multiValues() default false; } 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 2e4b646429c..69a8f16c879 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 @@ -66,6 +66,7 @@ public final class PropertyDefinition { private boolean onProject = false; private boolean onModule = false; private boolean isGlobal = true; + private boolean multiValues; private PropertyDefinition(Property annotation) { this.key = annotation.key(); @@ -78,20 +79,20 @@ public final class PropertyDefinition { this.category = annotation.category(); this.type = fixType(annotation.key(), annotation.type()); this.options = annotation.options(); + this.multiValues = annotation.multiValues(); } - private PropertyType fixType(String key, PropertyType type) { + private static PropertyType fixType(String key, PropertyType type) { // 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; + return PropertyType.PASSWORD; } else if (StringUtils.endsWith(key, ".license.secured")) { - fix = PropertyType.LICENSE; + return PropertyType.LICENSE; } } - return fix; + return type; } private PropertyDefinition(String key, PropertyType type, String[] options) { @@ -109,30 +110,28 @@ public final class PropertyDefinition { } public Result validate(@Nullable String value) { - // TODO REFACTORING REQUIRED HERE - Result result = Result.SUCCESS; if (StringUtils.isNotBlank(value)) { if (type == PropertyType.BOOLEAN) { if (!StringUtils.equalsIgnoreCase(value, "true") && !StringUtils.equalsIgnoreCase(value, "false")) { - result = Result.newError("notBoolean"); + return Result.newError("notBoolean"); } } else if (type == PropertyType.INTEGER) { if (!NumberUtils.isDigits(value)) { - result = Result.newError("notInteger"); + return Result.newError("notInteger"); } } else if (type == PropertyType.FLOAT) { try { Double.parseDouble(value); } catch (NumberFormatException e) { - result = Result.newError("notFloat"); + return Result.newError("notFloat"); } } else if (type == PropertyType.SINGLE_SELECT_LIST) { if (!ArrayUtils.contains(options, value)) { - result = Result.newError("notInOptions"); + return Result.newError("notInOptions"); } } } - return result; + return Result.SUCCESS; } public String getKey() { @@ -174,4 +173,11 @@ public final class PropertyDefinition { public boolean isGlobal() { return isGlobal; } + + /** + * @since 3.3 + */ + public boolean isMultiValues() { + return multiValues; + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java index fb541565056..6d7144a65e0 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java @@ -19,7 +19,9 @@ */ package org.sonar.api.config; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import org.apache.commons.lang.StringUtils; import org.sonar.api.BatchComponent; import org.sonar.api.Properties; @@ -97,12 +99,57 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen return definitions.values(); } + /** + * since 3.3 + */ + public Map> getGlobalPropertiesByCategory() { + Multimap byCategory = ArrayListMultimap.create(); + + for (PropertyDefinition definition : getAll()) { + if (definition.isGlobal()) { + byCategory.put(getCategory(definition.getKey()), definition); + } + } + + return byCategory.asMap(); + } + + /** + * since 3.3 + */ + public Map> getProjectPropertiesByCategory() { + Multimap byCategory = ArrayListMultimap.create(); + + for (PropertyDefinition definition : getAll()) { + if (definition.isOnProject()) { + byCategory.put(getCategory(definition.getKey()), definition); + } + } + + return byCategory.asMap(); + } + + /** + * since 3.3 + */ + public Map> getModulePropertiesByCategory() { + Multimap byCategory = ArrayListMultimap.create(); + + for (PropertyDefinition definition : getAll()) { + if (definition.isOnModule()) { + byCategory.put(getCategory(definition.getKey()), definition); + } + } + + return byCategory.asMap(); + } + public String getDefaultValue(String key) { PropertyDefinition def = get(key); - if (def != null) { - return StringUtils.defaultIfEmpty(def.getDefaultValue(), null); + if (def == null) { + return null; } - return null; + return StringUtils.defaultIfEmpty(def.getDefaultValue(), null); } public String getCategory(String key) { diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java index 5b118ecd4fc..3ec41a5cf6e 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java @@ -19,6 +19,8 @@ */ package org.sonar.api.config; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -29,7 +31,12 @@ import org.sonar.api.ServerComponent; import org.sonar.api.utils.DateUtils; import javax.annotation.Nullable; -import java.util.*; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Properties; /** * Project Settings on batch side, Global Settings on server side. This component does not access to database, so @@ -160,6 +167,20 @@ public class Settings implements BatchComponent, ServerComponent { * */ public final String[] getStringArray(String key) { + PropertyDefinition property = getDefinitions().get(key); + if ((null != property) && (property.isMultiValues())) { + String value = getString(key); + if (value == null) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + + List values = Lists.newArrayList(); + for (String v : Splitter.on(",").trimResults().split(value)) { + values.add(v.replace("%2C", ",")); + } + return values.toArray(new String[values.size()]); + } + return getStringArrayBySeparator(key, ","); } @@ -213,6 +234,32 @@ public class Settings implements BatchComponent, ServerComponent { return setProperty(key, newValue); } + public final Settings setProperty(String key, @Nullable String[] values) { + PropertyDefinition property = getDefinitions().get(key); + if ((null == property) || (!property.isMultiValues())) { + throw new IllegalStateException("Fail to set multiple values on a single value property " + key); + } + + if (values == null) { + properties.remove(key); + doOnRemoveProperty(key); + } else { + List escaped = Lists.newArrayList(); + for (String value : values) { + if (null != value) { + escaped.add(value.replace(",", "%2C")); + } else { + escaped.add(""); + } + } + + String escapedValue = Joiner.on(',').join(escaped); + properties.put(key, StringUtils.trim(escapedValue)); + doOnSetProperty(key, escapedValue); + } + return this; + } + public final Settings setProperty(String key, @Nullable String value) { if (value == null) { properties.remove(key); 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 793b4077356..d65d3af3c86 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 @@ -19,21 +19,15 @@ */ package org.sonar.api.config; -import org.hamcrest.core.Is; import org.junit.Test; import org.sonar.api.Properties; import org.sonar.api.Property; import org.sonar.api.PropertyType; import org.sonar.api.utils.AnnotationUtils; -import java.util.Arrays; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; -import static org.junit.matchers.JUnitMatchers.hasItems; +import static org.fest.assertions.Assertions.assertThat; public class PropertyDefinitionTest { - @Test public void createFromAnnotation() { Properties props = AnnotationUtils.getAnnotation(Init.class, Properties.class); @@ -41,23 +35,22 @@ public class PropertyDefinitionTest { PropertyDefinition def = PropertyDefinition.create(prop); - assertThat(def.getKey(), Is.is("hello")); - assertThat(def.getName(), Is.is("Hello")); - assertThat(def.getDefaultValue(), Is.is("world")); - assertThat(def.getCategory(), Is.is("categ")); - assertThat(def.getOptions().length, Is.is(2)); - assertThat(Arrays.asList(def.getOptions()), hasItems("de", "en")); - assertThat(def.getDescription(), Is.is("desc")); - assertThat(def.getType(), Is.is(PropertyType.FLOAT)); - assertThat(def.isGlobal(), Is.is(false)); - assertThat(def.isOnProject(), Is.is(true)); - assertThat(def.isOnModule(), Is.is(true)); + assertThat(def.getKey()).isEqualTo("hello"); + assertThat(def.getName()).isEqualTo("Hello"); + assertThat(def.getDefaultValue()).isEqualTo("world"); + assertThat(def.getCategory()).isEqualTo("categ"); + assertThat(def.getOptions()).hasSize(2); + assertThat(def.getOptions()).contains("de", "en"); + assertThat(def.getDescription()).isEqualTo("desc"); + assertThat(def.getType()).isEqualTo(PropertyType.FLOAT); + assertThat(def.isGlobal()).isFalse(); + assertThat(def.isOnProject()).isTrue(); + assertThat(def.isOnModule()).isTrue(); + assertThat(def.isMultiValues()).isTrue(); } - @Properties({ - @Property(key = "hello", name = "Hello", defaultValue = "world", description = "desc", - options = {"de", "en"}, category = "categ", type = PropertyType.FLOAT, global = false, project = true, module = true) - }) + @Properties(@Property(key = "hello", name = "Hello", defaultValue = "world", description = "desc", + options = {"de", "en"}, category = "categ", type = PropertyType.FLOAT, global = false, project = true, module = true, multiValues = true)) static class Init { } @@ -68,21 +61,20 @@ public class PropertyDefinitionTest { PropertyDefinition def = PropertyDefinition.create(prop); - assertThat(def.getKey(), Is.is("hello")); - assertThat(def.getName(), Is.is("Hello")); - assertThat(def.getDefaultValue(), Is.is("")); - assertThat(def.getCategory(), Is.is("")); - assertThat(def.getOptions().length, Is.is(0)); - assertThat(def.getDescription(), Is.is("")); - assertThat(def.getType(), Is.is(PropertyType.STRING)); - assertThat(def.isGlobal(), Is.is(true)); - assertThat(def.isOnProject(), Is.is(false)); - assertThat(def.isOnModule(), Is.is(false)); + assertThat(def.getKey()).isEqualTo("hello"); + assertThat(def.getName()).isEqualTo("Hello"); + assertThat(def.getDefaultValue()).isEmpty(); + assertThat(def.getCategory()).isEmpty(); + assertThat(def.getOptions()).isEmpty(); + assertThat(def.getDescription()).isEmpty(); + assertThat(def.getType()).isEqualTo(PropertyType.STRING); + assertThat(def.isGlobal()).isTrue(); + assertThat(def.isOnProject()).isFalse(); + assertThat(def.isOnModule()).isFalse(); + assertThat(def.isMultiValues()).isFalse(); } - @Properties({ - @Property(key = "hello", name = "Hello") - }) + @Properties(@Property(key = "hello", name = "Hello")) static class DefaultValues { } @@ -90,70 +82,68 @@ public class PropertyDefinitionTest { public void validate_string() { PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.STRING, new String[0]); - assertThat(def.validate(null).isValid(), is(true)); - assertThat(def.validate("").isValid(), is(true)); - assertThat(def.validate(" ").isValid(), is(true)); - assertThat(def.validate("foo").isValid(), is(true)); + assertThat(def.validate(null).isValid()).isTrue(); + assertThat(def.validate("").isValid()).isTrue(); + assertThat(def.validate(" ").isValid()).isTrue(); + assertThat(def.validate("foo").isValid()).isTrue(); } @Test public void validate_boolean() { PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.BOOLEAN, new String[0]); - assertThat(def.validate(null).isValid(), is(true)); - assertThat(def.validate("").isValid(), is(true)); - assertThat(def.validate(" ").isValid(), is(true)); - assertThat(def.validate("true").isValid(), is(true)); - assertThat(def.validate("false").isValid(), is(true)); + assertThat(def.validate(null).isValid()).isTrue(); + assertThat(def.validate("").isValid()).isTrue(); + assertThat(def.validate(" ").isValid()).isTrue(); + assertThat(def.validate("true").isValid()).isTrue(); + assertThat(def.validate("false").isValid()).isTrue(); - assertThat(def.validate("foo").isValid(), is(false)); - assertThat(def.validate("foo").getErrorKey(), is("notBoolean")); + assertThat(def.validate("foo").isValid()).isFalse(); + assertThat(def.validate("foo").getErrorKey()).isEqualTo("notBoolean"); } @Test public void validate_integer() { PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.INTEGER, new String[0]); - assertThat(def.validate(null).isValid(), is(true)); - assertThat(def.validate("").isValid(), is(true)); - assertThat(def.validate(" ").isValid(), is(true)); - assertThat(def.validate("123456").isValid(), is(true)); + assertThat(def.validate(null).isValid()).isTrue(); + assertThat(def.validate("").isValid()).isTrue(); + assertThat(def.validate(" ").isValid()).isTrue(); + assertThat(def.validate("123456").isValid()).isTrue(); - assertThat(def.validate("foo").isValid(), is(false)); - assertThat(def.validate("foo").getErrorKey(), is("notInteger")); + assertThat(def.validate("foo").isValid()).isFalse(); + assertThat(def.validate("foo").getErrorKey()).isEqualTo("notInteger"); } @Test public void validate_float() { PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.FLOAT, new String[0]); - assertThat(def.validate(null).isValid(), is(true)); - assertThat(def.validate("").isValid(), is(true)); - assertThat(def.validate(" ").isValid(), is(true)); - assertThat(def.validate("123456").isValid(), is(true)); - assertThat(def.validate("3.14").isValid(), is(true)); + assertThat(def.validate(null).isValid()).isTrue(); + assertThat(def.validate("").isValid()).isTrue(); + assertThat(def.validate(" ").isValid()).isTrue(); + assertThat(def.validate("123456").isValid()).isTrue(); + assertThat(def.validate("3.14").isValid()).isTrue(); - assertThat(def.validate("foo").isValid(), is(false)); - assertThat(def.validate("foo").getErrorKey(), is("notFloat")); + assertThat(def.validate("foo").isValid()).isFalse(); + assertThat(def.validate("foo").getErrorKey()).isEqualTo("notFloat"); } @Test public void validate_single_select_list() { - PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.SINGLE_SELECT_LIST, new String[]{"de", "en"}); + PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.SINGLE_SELECT_LIST, new String[] {"de", "en"}); - assertThat(def.validate(null).isValid(), is(true)); - assertThat(def.validate("").isValid(), is(true)); - assertThat(def.validate(" ").isValid(), is(true)); - assertThat(def.validate("de").isValid(), is(true)); - assertThat(def.validate("en").isValid(), is(true)); + assertThat(def.validate(null).isValid()).isTrue(); + assertThat(def.validate("").isValid()).isTrue(); + assertThat(def.validate(" ").isValid()).isTrue(); + assertThat(def.validate("de").isValid()).isTrue(); + assertThat(def.validate("en").isValid()).isTrue(); - assertThat(def.validate("fr").isValid(), is(false)); - assertThat(def.validate("fr").getErrorKey(), is("notInOptions")); + assertThat(def.validate("fr").isValid()).isFalse(); + assertThat(def.validate("fr").getErrorKey()).isEqualTo("notInOptions"); } - @Properties({ - @Property(key = "scm.password.secured", name = "SCM password") - }) + @Properties(@Property(key = "scm.password.secured", name = "SCM password")) static class OldScmPlugin { } @@ -164,13 +154,11 @@ public class PropertyDefinitionTest { PropertyDefinition def = PropertyDefinition.create(prop); - assertThat(def.getKey(), Is.is("scm.password.secured")); - assertThat(def.getType(), Is.is(PropertyType.PASSWORD)); + assertThat(def.getKey()).isEqualTo("scm.password.secured"); + assertThat(def.getType()).isEqualTo(PropertyType.PASSWORD); } - @Properties({ - @Property(key = "views.license.secured", name = "Views license") - }) + @Properties(@Property(key = "views.license.secured", name = "Views license")) static class ViewsPlugin { } @@ -181,7 +169,7 @@ public class PropertyDefinitionTest { PropertyDefinition def = PropertyDefinition.create(prop); - assertThat(def.getKey(), Is.is("views.license.secured")); - assertThat(def.getType(), Is.is(PropertyType.LICENSE)); + assertThat(def.getKey()).isEqualTo("views.license.secured"); + assertThat(def.getType()).isEqualTo(PropertyType.LICENSE); } } diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionsTest.java index 1320d9e376d..9f30157e3bd 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionsTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionsTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.sonar.api.Properties; import org.sonar.api.Property; +import static org.fest.assertions.Assertions.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; @@ -87,4 +88,23 @@ public class PropertyDefinitionsTest { }) static final class Categories { } + + @Test + public void should_group_by_category() { + PropertyDefinitions def = new PropertyDefinitions(ByCategory.class); + + assertThat(def.getGlobalPropertiesByCategory().keySet()).containsOnly("catGlobal1", "catGlobal2"); + assertThat(def.getProjectPropertiesByCategory().keySet()).containsOnly("catProject"); + assertThat(def.getModulePropertiesByCategory().keySet()).containsOnly("catModule"); + } + + @Properties({ + @Property(key = "global1", name = "Global1", category = "catGlobal1", global = true, project = false, module = false), + @Property(key = "global2", name = "Global2", category = "catGlobal1", global = true, project = false, module = false), + @Property(key = "global3", name = "Global3", category = "catGlobal2", global = true, project = false, module = false), + @Property(key = "project", name = "Project", category = "catProject", global = false, project = true, module = false), + @Property(key = "module", name = "Module", category = "catModule", global = false, project = false, module = true) + }) + static final class ByCategory { + } } diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/SettingsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/SettingsTest.java index 00757d04646..cef3a3fbdfb 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/config/SettingsTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/SettingsTest.java @@ -40,7 +40,8 @@ public class SettingsTest { @Property(key = "boolean", name = "Boolean", defaultValue = "true"), @Property(key = "falseboolean", name = "False Boolean", defaultValue = "false"), @Property(key = "integer", name = "Integer", defaultValue = "12345"), - @Property(key = "array", name = "Array", defaultValue = "one,two,three") + @Property(key = "array", name = "Array", defaultValue = "one,two,three"), + @Property(key = "multi_values", name = "Array", defaultValue = "1,2,3", multiValues = true) }) static class Init { } @@ -153,6 +154,52 @@ public class SettingsTest { assertThat(array).isEqualTo(new String[]{"one", "two", "three"}); } + @Test + public void setStringArray() { + Settings settings = new Settings(definitions); + settings.setProperty("multi_values", new String[] {"A", "B"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A", "B"}); + } + + @Test + public void setStringArrayTrimValues() { + Settings settings = new Settings(definitions); + settings.setProperty("multi_values", new String[] {" A ", " B "}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A", "B"}); + } + + @Test + public void setStringArrayEscapeCommas() { + Settings settings = new Settings(definitions); + settings.setProperty("multi_values", new String[] {"A,B", "C,D"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A,B", "C,D"}); + } + + @Test + public void setStringArrayWithEmptyValues() { + Settings settings = new Settings(definitions); + settings.setProperty("multi_values", new String[] {"A,B", "", "C,D"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"}); + } + + @Test + public void setStringArrayWithNullValues() { + Settings settings = new Settings(definitions); + settings.setProperty("multi_values", new String[] {"A,B", null, "C,D"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"}); + } + + @Test(expected = IllegalStateException.class) + public void shouldFailToSetArrayValueOnSingleValueProperty() { + Settings settings = new Settings(definitions); + settings.setProperty("array", new String[] {"A", "B", "C"}); + } + @Test public void getStringArray_no_value() { Settings settings = new Settings(); diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb index 92d1c85d95c..5554c8f4728 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb @@ -49,7 +49,7 @@ class Api::AuthenticationController < Api::ApiController end def force_authentication? - property = Property.find(:first, :conditions => {:prop_key => org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY, :resource_id => nil, :user_id => nil}) + property = Property.by_key(org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY) property ? property.value == 'true' : false end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/user_properties_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/user_properties_controller.rb index fb98e5b6f1f..4cab0f299cf 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/user_properties_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/user_properties_controller.rb @@ -43,7 +43,7 @@ class Api::UserPropertiesController < Api::ApiController # curl http://localhost:9000/api/user_properties/ -v -u admin:admin # def show - property = Property.find(:first, :conditions => ['user_id=? and prop_key=?', current_user.id, params[:id]]) + property = Property.by_key(params[:id], nil, current_user.id) if property respond_to do |format| format.json { render :json => jsonp(properties_to_json([property])) } @@ -65,7 +65,7 @@ class Api::UserPropertiesController < Api::ApiController value = params[:value] if key begin - Property.delete_all(['prop_key=? AND user_id=?', key,current_user.id]) + Property.clear(key, nil, current_user.id) property=Property.create(:prop_key => key, :text_value => value, :user_id => current_user.id) respond_to do |format| format.json { render :json => jsonp(properties_to_json([property])) } @@ -88,10 +88,9 @@ class Api::UserPropertiesController < Api::ApiController def destroy begin if params[:id] - Property.delete_all(['prop_key=? AND user_id=?', params[:id], current_user.id]) + Property.clear(params[:id], nil, current_user.id) end render_success("Property deleted") - rescue Exception => e logger.error("Fails to execute #{request.url} : #{e.message}") render_error(e.message) diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index de19cb7b14f..76918622d64 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -18,7 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 # class ProjectController < ApplicationController - verify :method => :post, :only => [:set_links, :set_exclusions, :delete_exclusions, :update_key, :perform_key_bulk_update, :update_quality_profile], + verify :method => :post, :only => [:set_links, :set_exclusions, :delete_exclusions, :update_key, :perform_key_bulk_update, :update_quality_profile], :redirect_to => {:action => :index} verify :method => :delete, :only => [:delete], :redirect_to => {:action => :index} @@ -33,11 +33,11 @@ class ProjectController < ApplicationController if java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'deletable') deletion_manager = ResourceDeletionManager.instance - if deletion_manager.currently_deleting_resources? || + if deletion_manager.currently_deleting_resources? || (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?) # a deletion is happening or it has just finished with errors => display the message from the Resource Deletion Manager render :template => 'project/pending_deletion' - else + else @snapshot=@project.last_snapshot end else @@ -47,19 +47,19 @@ class ProjectController < ApplicationController def delete @project = get_current_project(params[:id]) - + # Ask the resource deletion manager to start the migration # => this is an asynchronous AJAX call ResourceDeletionManager.instance.delete_resources([@project.id]) - + # and return some text that will actually never be displayed render :text => ResourceDeletionManager.instance.message end - + def pending_deletion deletion_manager = ResourceDeletionManager.instance - - if deletion_manager.currently_deleting_resources? || + + if deletion_manager.currently_deleting_resources? || (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?) # display the same page again and again # => implicit render "pending_deletion.html.erb" @@ -67,24 +67,24 @@ class ProjectController < ApplicationController redirect_to_default end end - + def dismiss_deletion_message # It is important to reinit the ResourceDeletionManager so that the deletion screens can be available again ResourceDeletionManager.instance.reinit - + redirect_to :action => 'deletion', :id => params[:id] end - + def quality_profile @project = get_current_project(params[:id]) @profiles = Profile.find(:all, :conditions => {:language => @project.language, :enabled => true}) end - + def update_quality_profile project = get_current_project(params[:id]) - + selected_profile = Profile.find(:first, :conditions => {:id => params[:quality_profile].to_i}) - if selected_profile && selected_profile.language == project.language + if selected_profile && selected_profile.language == project.language project.profile = selected_profile project.save! flash[:notice] = message('project_quality_profile.profile_successfully_updated') @@ -92,17 +92,17 @@ class ProjectController < ApplicationController selected_profile_name = selected_profile ? selected_profile.name + "(" + selected_profile.language + ")" : "Unknown profile" flash[:error] = message('project_quality_profile.project_cannot_be_update_with_profile_x', :params => selected_profile_name) end - + redirect_to :action => 'quality_profile', :id => project.id end - + def key @project = get_current_project(params[:id]) end - + def update_key project = get_current_project(params[:id]) - + new_key = params[:new_key].strip if new_key.blank? flash[:error] = message('update_key.new_key_cant_be_blank_for_x', :params => project.key) @@ -115,17 +115,17 @@ class ProjectController < ApplicationController java_facade.updateResourceKey(project.id, new_key) flash[:notice] = message('update_key.key_updated') rescue Exception => e - flash[:error] = message('update_key.error_occured_while_renaming_key_of_x', + flash[:error] = message('update_key.error_occured_while_renaming_key_of_x', :params => [project.key, Api::Utils.exception_message(e, :backtrace => false)]) end end - + redirect_to :action => 'key', :id => project.root_project.id end - + def prepare_key_bulk_update @project = get_current_project(params[:id]) - + @string_to_replace = params[:string_to_replace].strip @replacement_string = params[:replacement_string].strip if @string_to_replace.blank? || @replacement_string.blank? @@ -148,20 +148,20 @@ class ProjectController < ApplicationController def perform_key_bulk_update project = get_current_project(params[:id]) - + string_to_replace = params[:string_to_replace].strip replacement_string = params[:replacement_string].strip - + unless string_to_replace.blank? || replacement_string.blank? begin java_facade.bulkUpdateKey(project.id, string_to_replace, replacement_string) flash[:notice] = message('update_key.key_updated') rescue Exception => e - flash[:error] = message('update_key.error_occured_while_renaming_key_of_x', + flash[:error] = message('update_key.error_occured_while_renaming_key_of_x', :params => [project.key, Api::Utils.exception_message(e, :backtrace => false)]) end end - + redirect_to :action => 'key', :id => project.id end @@ -224,32 +224,34 @@ class ProjectController < ApplicationController redirect_to :action => 'links', :id => project.id end - def settings - @project = get_current_project(params[:id]) + @resource = get_current_project(params[:id]) - @snapshot=@project.last_snapshot - if !@project.project? && !@project.module? + @snapshot = @resource.last_snapshot + if !@resource.project? && !@resource.module? redirect_to :action => 'index', :id => params[:id] end - @category=params[:category] ||= 'general' - @definitions_per_category={} - definitions = java_facade.getPropertyDefinitions() - properties = definitions.getAll().select { |property| (@project.module? && property.isOnModule()) || (@project.project? && property.isOnProject()) } - properties.each do |property| - category = definitions.getCategory(property.getKey()) - @definitions_per_category[category]||=[] - @definitions_per_category[category]< @snapshot.project_id} unless @category.blank? @@ -260,7 +262,7 @@ class ProjectController < ApplicationController snapshots_to_be_deleted = Snapshot.find(:all, :conditions => ["status='U' AND project_id=?", @snapshot.project_id]) unless snapshots_to_be_deleted.empty? conditions << " AND snapshot_id NOT IN (:sids)" - values[:sids] = snapshots_to_be_deleted.map {|s| s.id} + values[:sids] = snapshots_to_be_deleted.map { |s| s.id } end category_names=@categories.map { |cat| cat.name } @@ -342,7 +344,7 @@ class ProjectController < ApplicationController snapshot=Snapshot.find(params[:sid]) not_found("Snapshot not found") unless snapshot access_denied unless is_admin?(snapshot) - + # We update all the related snapshots to have the same version as the next snapshot next_snapshot = Snapshot.find(:first, :conditions => ['created_at>? and project_id=?', snapshot.created_at, snapshot.project_id], :order => 'created_at asc') snapshots = find_project_snapshots(snapshot.id) @@ -371,11 +373,11 @@ class ProjectController < ApplicationController else snapshots = find_project_snapshots(snapshot.id) snapshots.each do |s| - e = Event.new({:name => params[:event_name], - :category => EventCategory::KEY_OTHER, - :snapshot => s, - :resource_id => s.project_id, - :event_date => s.created_at}) + e = Event.new({:name => params[:event_name], + :category => EventCategory::KEY_OTHER, + :snapshot => s, + :resource_id => s.project_id, + :event_date => s.created_at}) e.save! end flash[:notice] = message('project_history.event_created', :params => params[:event_name]) @@ -425,7 +427,7 @@ class ProjectController < ApplicationController access_denied unless is_admin?(project) project end - + def find_project_snapshots(root_snapshot_id) snapshots = Snapshot.find(:all, :include => 'events', :conditions => ["(root_snapshot_id = ? OR id = ?) AND scope = 'PRJ'", root_snapshot_id, root_snapshot_id]) end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb index 949775f5e82..50282117749 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb @@ -21,80 +21,54 @@ class SettingsController < ApplicationController SECTION=Navigation::SECTION_CONFIGURATION - SPECIAL_CATEGORIES=['email', 'encryption', 'server_id'] + SPECIAL_CATEGORIES=%w(email encryption server_id) - verify :method => :post, :only => ['update'], :redirect_to => {:action => :index} + verify :method => :post, :only => %w(update), :redirect_to => {:action => :index} + before_filter :admin_required, :only => %w(index) def index - access_denied unless is_admin? - load_properties(nil) - @category ||= 'general' + load_properties() end def update - @project=nil - resource_id=nil - if params[:resource_id] - @project=Project.by_key(params[:resource_id]) - access_denied unless (@project && is_admin?(@project)) - resource_id=@project.id - else - access_denied unless is_admin? - end - is_global=(@project.nil?) - - load_properties(@project) - - @persisted_properties_per_key={} - if @category && @definitions_per_category[@category] - @definitions_per_category[@category].each do |property| - value=params[property.getKey()] - persisted_property = Property.find(:first, :conditions => {:prop_key=> property.key(), :resource_id => resource_id, :user_id => nil}) - - # update the property - if persisted_property - if value.empty? - Property.delete_all('prop_key' => property.key(), 'resource_id' => resource_id, 'user_id' => nil) - java_facade.setGlobalProperty(property.getKey(), nil) if is_global - elsif persisted_property.text_value != value.to_s - persisted_property.text_value = value.to_s - if persisted_property.save && is_global - java_facade.setGlobalProperty(property.getKey(), value.to_s) - end - @persisted_properties_per_key[persisted_property.key]=persisted_property - end - - # create the property - elsif value.present? - persisted_property=Property.new(:prop_key => property.key(), :text_value => value.to_s, :resource_id => resource_id) - if persisted_property.save && is_global - java_facade.setGlobalProperty(property.getKey(), value.to_s) - end - @persisted_properties_per_key[persisted_property.key]=persisted_property - end - end + resource_id = params[:resource_id] + @resource = Project.by_key(resource_id) if resource_id + + access_denied if (@resource && !is_admin?(@resource)) + access_denied if (@resource.nil? && !is_admin?) - params[:layout]='false' - render :partial => 'settings/properties' + load_properties() + + @definitions.map(&:key).each do |key| + value = params[key] + + if value.blank? + Property.clear(key, resource_id) + else + Property.set(key, value, resource_id) + end end + + render :partial => 'settings/properties' end private - def load_properties(project) - @category=params[:category] - @definitions_per_category={} - definitions = java_facade.getPropertyDefinitions() - definitions.getAll().select { |property_definition| - (project.nil? && property_definition.isGlobal()) || (project && project.module? && property_definition.isOnModule()) || (project && project.project? && property_definition.isOnProject()) - }.each do |property_definition| - category = definitions.getCategory(property_definition.getKey()) - @definitions_per_category[category]||=[] - @definitions_per_category[category]< default) + end + + def by_name(categories) + categories.sort_by { |category| category_name(category) } + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb index 3f8dfb72de6..39f617657ee 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb @@ -20,6 +20,10 @@ class Property < ActiveRecord::Base validates_presence_of :prop_key + named_scope :with_key, lambda { |value| {:conditions => {:prop_key, value}} } + named_scope :with_resource, lambda { |value| {:conditions => {:resource_id => value}} } + named_scope :with_user, lambda { |value| {:conditions => {:user_id => value}} } + def key prop_key end @@ -28,33 +32,48 @@ class Property < ActiveRecord::Base text_value end - def self.hash(resource_id=nil) - hash={} - Property.find(:all, :conditions => {'resource_id' => resource_id, 'user_id' => nil}).each do |prop| - hash[prop.key]=prop.value - end - hash + def self.hash(resource_id=nil, user_id=nil) + properties = Property.with_resource(resource_id).with_user(user_id) + + Hash[properties.map { |prop| [prop.key, prop.value] }] end - def self.value(key, resource_id=nil, default_value=nil) - prop=Property.find(:first, :conditions => {'prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil}) - if prop - prop.text_value || default_value - else - default_value - end + def self.clear(key, resource_id=nil, user_id=nil) + all(key, resource_id, user_id).delete_all + Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, nil) unless resource_id + end + + def self.by_key(key, resource_id=nil, user_id=nil) + all(key, resource_id, user_id).first + end + + def self.by_key_prefix(prefix) + Property.find(:all, :conditions => ['prop_key like ?', prefix + '%']) + end + + def self.value(key, resource_id=nil, default_value=nil, user_id=nil) + property = by_key(key, resource_id, user_id) + return default_value unless property + + property.text_value || default_value end - def self.values(key, resource_id=nil) - Property.find(:all, :conditions => {'prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil}).collect { |p| p.text_value } + def self.values(key, resource_id=nil, user_id=nil) + value = value(key, resource_id, '', user_id) + values = value.split(',') + values.empty? ? [nil] : values.map { |v| v.gsub('%2C', ',') } end - def self.set(key, value, resource_id=nil) + def self.set(key, value, resource_id=nil, user_id=nil) + if value.kind_of? Array + value = value.map { |v| v.gsub(',', '%2C') }.join(',') + end + text_value = (value.nil? ? nil : value.to_s) - prop = Property.new(:prop_key => key, :text_value => text_value, :resource_id => resource_id) + prop = Property.new(:prop_key => key, :text_value => text_value, :resource_id => resource_id, :user_id => user_id) if prop.valid? Property.transaction do - Property.delete_all('prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil) + Property.delete_all(:prop_key => key, :resource_id => resource_id, :user_id => user_id) prop.save end Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, text_value) unless resource_id @@ -62,25 +81,8 @@ class Property < ActiveRecord::Base prop end - def self.clear(key, resource_id=nil) - Property.delete_all('prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil) - Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, nil) unless resource_id - end - - def self.by_key(key, resource_id=nil) - Property.find(:first, :conditions => {'prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil}) - end - - def self.by_key_prefix(prefix) - Property.find(:all, :conditions => ["prop_key like ?", prefix + '%']) - end - - def self.update(key, value) - property = Property.find(:first, :conditions => {:prop_key => key, :resource_id => nil, :user_id => nil}); - property.text_value = value - property.save - Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, value) - property + def self.update(key, value, resource_id=nil, user_id=nil) + set(key, value, resource_id, user_id) end def to_hash_json @@ -112,6 +114,10 @@ class Property < ActiveRecord::Base private + def self.all(key, resource_id=nil, user_id=nil) + Property.with_key(key).with_resource(resource_id).with_user(user_id) + end + def validate if java_definition validation_result=java_definition.validate(text_value) diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb new file mode 100644 index 00000000000..a571f937090 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb @@ -0,0 +1,5 @@ +
+ <%= render "settings/type_#{property_type(property, value)}", :property => property, :value => value -%> + +
+
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 e77b7583790..3e9602d4820 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 @@ -1,87 +1,67 @@ -<% if @category && @definitions_per_category[@category] - category_name = message("property.category.#{@category}", :default => @category) - if SettingsController::SPECIAL_CATEGORIES.include?(@category) -%> - <%= render :partial => 'special', :locals => {:url => url_for(:controller => "#{@category}_configuration")} -%> - <% - elsif !@definitions_per_category[@category].empty? - %> - <% form_remote_tag :url => {:controller => 'settings', :action => 'update', :category => @category, :resource_id => @project ? @project.id : nil}, - :method => :post, - :before => "$('submit_settings').hide();$('loading_settings').show()", - :update => 'properties' do -%> +<% if SettingsController::SPECIAL_CATEGORIES.include?(@category) -%> + <%= render 'special', :url => url_for(:controller => "#{@category}_configuration") -%> +<% else -%> + <% form_remote_tag :url => {:controller => 'settings', :action => 'update', :category => @category, :resource_id => @resource ? @resource.id : nil}, + :method => :post, + :before => "$('submit_settings').hide();$('loading_settings').show();", + :update => 'properties', + :script => false do -%> - - - - - - - - <% - if @definitions_per_category[@category] - @definitions_per_category[@category].each do |property| - value = nil - - # set when form has been submitted but some errors have been raised - if @persisted_properties_per_key - p = @persisted_properties_per_key[property.key] - if p - value = p.text_value - end - end +
- <%= h(category_name) -%> -
+ + + + + + + <% @definitions.each do |property| -%> + + + + <% end -%> + +
<%= h category_name(@category) -%>
+

+
<%= property_name(property) -%>
+
<%= property.key -%>
+

+ <% desc=property_description(property) -%> + <% unless desc.blank? %> +

<%= desc -%>

+ <% end -%> + <% value = property_value(property) -%> + <% if property.multi_values -%> + <% value.each do |sub_value| -%> + <%= render "settings/multi_value", :property => property, :value => sub_value -%> + <% end -%> + + <%= message('settings.add') -%> + <% else -%> + <%= render "settings/type_#{property_type(property, value)}", :property => property, :value => value -%> + <% end -%> - # if fresh form or no error, get the current value - value = Property.value(property.getKey(), (@project ? @project.id : nil), '') unless value + <% default_prop_value = (@resource ? Property.value(property.key, nil, property.defaultValue) : property.defaultValue) -%> + <% unless default_prop_value.blank? %> +

Default: <%= property.type.to_s=='PASSWORD' ? '********' : h(default_prop_value) -%>

+ <% end -%> +
+
+ <%= submit_tag(message('settings.save_category', :params => [category_name(@category)]), :id => 'submit_settings') -%> + +
+ <% end -%> +<% end -%> - # for backward-compatibility with properties that do not define the type TEXT - property_type = property.getType() - if property_type.to_s=='STRING' && value.include?("\n") - property_type = 'TEXT' - end + \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb index b9975d9d0ab..3411c37143a 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb @@ -1,28 +1,33 @@ - -
-

<%= message('settings.page') -%>

- - - - - - -
- - - - - - - - <% - @definitions_per_category.keys.sort_by { |category| message("property.category.#{category}", :default => category).upcase }.each do |category| - if !@definitions_per_category[category].empty? || SettingsController::SPECIAL_CATEGORIES.include?(category) - %> - - - - <% end - end - %> - -
- Category -
<%= link_to message("property.category.#{category}", :default => category), :overwrite_params => {:category => category} -%>
-
-
-
- <%= render :partial => 'settings/properties' -%> -
-
-
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_sidebar.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_sidebar.html.erb deleted file mode 100644 index e69de29bb2d..00000000000 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 b4c3b451f4d..7292d327860 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 @@ - -<%= link_to_function(image_tag('zoom.png'), "enlargeTextInput('#{property.getKey()}')", :class => 'nolink') -%> \ No newline at end of file + +<%= link_to_function(image_tag('zoom.png'), "enlargeTextInput('#{property.key}')", :class => 'nolink') -%> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/index.html.erb index 81fa7362a09..d2c6ebcae2c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/index.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/index.html.erb @@ -1 +1 @@ -<%= render :partial => 'settings', :locals => {:project=>nil} %> \ No newline at end of file +<%= render 'settings', :project => nil %> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/stylesheets/style.css b/sonar-server/src/main/webapp/stylesheets/style.css index 74584a1093b..ff58b01e301 100644 --- a/sonar-server/src/main/webapp/stylesheets/style.css +++ b/sonar-server/src/main/webapp/stylesheets/style.css @@ -2087,6 +2087,11 @@ table.nowrap td, td.nowrap, th.nowrap { padding: 2px 0 2px 20px; } +.delete { + background: url("../images/cross.png") no-repeat scroll left 50% transparent; + padding: 2px 0 2px 20px; +} + .restore { display: block; background: url("../images/restore.gif") no-repeat scroll left 50% transparent;