shiro-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lhazlew...@apache.org
Subject svn commit: r888899 - in /incubator/shiro/trunk/core/src: main/java/org/apache/shiro/config/ main/java/org/apache/shiro/realm/ main/java/org/apache/shiro/realm/text/ test/java/org/apache/shiro/config/
Date Wed, 09 Dec 2009 18:07:16 GMT
Author: lhazlewood
Date: Wed Dec  9 18:07:16 2009
New Revision: 888899

URL: http://svn.apache.org/viewvc?rev=888899&view=rev
Log:
SHIRO-116 - ensured that an implicit IniRealm would be created if the startup configuration
contains [users] or [roles] ini sections.  Also provided the ability to turn caching on or
off at a global level via CachingRealm.setCachingEnabled.

Added:
    incubator/shiro/trunk/core/src/test/java/org/apache/shiro/config/IniSecurityManagerFactoryTest.java
Modified:
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/config/IniSecurityManagerFactory.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/AuthorizingRealm.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/CachingRealm.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/SimpleAccountRealm.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/text/IniRealm.java

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/config/IniSecurityManagerFactory.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/config/IniSecurityManagerFactory.java?rev=888899&r1=888898&r2=888899&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/config/IniSecurityManagerFactory.java
(original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/config/IniSecurityManagerFactory.java
Wed Dec  9 18:07:16 2009
@@ -23,9 +23,11 @@
 import org.apache.shiro.mgt.SecurityManager;
 import org.apache.shiro.realm.Realm;
 import org.apache.shiro.realm.RealmFactory;
+import org.apache.shiro.realm.text.IniRealm;
 import org.apache.shiro.util.CollectionUtils;
 import org.apache.shiro.util.Factory;
 import org.apache.shiro.util.LifecycleUtils;
+import org.apache.shiro.util.Nameable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,6 +43,8 @@
 
     public static final String MAIN_SECTION_NAME = "main";
 
+    public static final String SECURITY_MANAGER_NAME = "securityManager";
+
     private static transient final Logger log = LoggerFactory.getLogger(IniSecurityManagerFactory.class);
 
     /**
@@ -77,68 +81,125 @@
             //try the default:
             mainSection = ini.getSection(Ini.DEFAULT_SECTION_NAME);
         }
-        SecurityManager securityManager;
-        if (CollectionUtils.isEmpty(mainSection)) {
-            if (log.isInfoEnabled()) {
-                log.info("No main/default section was found in INI resource [" + ini + "].
 A simple default " +
-                        "SecurityManager instance will be created automatically.");
-            }
-            securityManager = createDefaultInstance();
-        } else {
-            securityManager = doCreateSecurityManager(mainSection);
-        }
-        return securityManager;
+        return doCreateSecurityManager(ini, mainSection);
     }
 
-    @SuppressWarnings({"unchecked"})
-    protected SecurityManager doCreateSecurityManager(Ini.Section mainSection) {
-
+    private Map<String, Object> buildMainInstances(Ini.Section main) {
         Map<String, Object> defaults = new LinkedHashMap<String, Object>();
-
         SecurityManager securityManager = createDefaultInstance();
         defaults.put("securityManager", securityManager);
+        return buildInstances(main, defaults);
+    }
 
-        ReflectionBuilder builder = new ReflectionBuilder(defaults);
-        Map<String, Object> objects = builder.buildObjects(mainSection);
+    @SuppressWarnings({"unchecked"})
+    protected Map<String, Object> buildInstances(Ini.Section section, Map<String,
Object> defaults) {
+        ReflectionBuilder builder;
+        if (CollectionUtils.isEmpty(defaults)) {
+            builder = new ReflectionBuilder();
+        } else {
+            builder = new ReflectionBuilder(defaults);
+        }
+        return builder.buildObjects(section);
+    }
+
+    private void addToRealms(Collection<Realm> realms, RealmFactory factory) {
+        LifecycleUtils.init(factory);
+        Collection<Realm> factoryRealms = factory.getRealms();
+        if (factoryRealms != null && !factoryRealms.isEmpty()) {
+            realms.addAll(factoryRealms);
+        }
+    }
+
+    private Collection<Realm> getRealms(Map<String, Object> instances) {
 
         //realms and realm factory might have been created - pull them out first so we can
         //initialize the securityManager:
         List<Realm> realms = new ArrayList<Realm>();
 
         //iterate over the map entries to pull out the realm factory(s):
-        for (Map.Entry<String, Object> entry : objects.entrySet()) {
+        for (Map.Entry<String, Object> entry : instances.entrySet()) {
+
             String name = entry.getKey();
             Object value = entry.getValue();
-            if (value instanceof SecurityManager) {
-                securityManager = (SecurityManager) value;
-            } else if (value instanceof RealmFactory) {
-                RealmFactory factory = (RealmFactory) value;
-                LifecycleUtils.init(factory);
-                Collection<Realm> factoryRealms = factory.getRealms();
-                if (factoryRealms != null && !factoryRealms.isEmpty()) {
-                    realms.addAll(factoryRealms);
-                }
+
+            if (value instanceof RealmFactory) {
+                addToRealms(realms, (RealmFactory) value);
             } else if (value instanceof Realm) {
                 Realm realm = (Realm) value;
                 //set the name if null:
                 String existingName = realm.getName();
                 if (existingName == null || existingName.startsWith(realm.getClass().getName()))
{
-                    try {
-                        builder.applyProperty(realm, "name", name);
-                    } catch (Exception ignored) {
-                        log.debug("Unable to apply 'name' property value {} to realm {}.",
name, realm);
+                    if (realm instanceof Nameable) {
+                        ((Nameable) realm).setName(name);
+                        log.debug("Applied name '{}' to Nameable realm instance {}", name,
realm);
+                    } else {
+                        log.info("Realm does not implement the {} interface.  Configured
name will not be applied.",
+                                Nameable.class.getName());
                     }
                 }
                 realms.add(realm);
             }
         }
 
+        return realms;
+    }
+
+    protected void applyRealmsToSecurityManager(Collection<Realm> realms, SecurityManager
securityManager) {
+        if (!(securityManager instanceof RealmSecurityManager)) {
+            String msg = realms.size() + " Realms were configured, but the underlying SecurityManager
" +
+                    "instance is not a " + RealmSecurityManager.class.getName() + " instance.
 This is required " +
+                    "to apply any configured Realms.";
+            throw new ConfigurationException(msg);
+        }
+        ((RealmSecurityManager) securityManager).setRealms(realms);
+        //initialize the realms now that they have been configured on the security manager
+        LifecycleUtils.init(realms);
+    }
+
+    /**
+     * Returns {@code true} if the Ini contains account data and a {@code Realm} should be
implicitly
+     * {@link #createRealm(Ini) created} to reflect the account data, {@code false} if no
realm should be implicitly
+     * created.
+     *
+     * @param ini the Ini instance to inspect for account data resulting in an implicitly
created realm.
+     * @return {@code true} if the Ini contains account data and a {@code Realm} should be
implicitly
+     *         {@link #createRealm(Ini) created} to reflect the account data, {@code false}
if no realm should be
+     *         implicitly created.
+     */
+    protected boolean shouldImplicitlyCreateRealm(Ini ini) {
+        return !CollectionUtils.isEmpty(ini.getSection(IniRealm.ROLES_SECTION_NAME)) ||
+                !CollectionUtils.isEmpty(ini.getSection(IniRealm.USERS_SECTION_NAME));
+    }
+
+    /**
+     * Creates a {@code Realm} from the Ini instance containing account data.
+     *
+     * @param ini the Ini instance from which to acquire the account data.
+     * @return a new Realm instance reflecting the account data discovered in the {@code
Ini}.
+     */
+    protected Realm createRealm(Ini ini) {
+        return new IniRealm(ini);
+    }
+
+    @SuppressWarnings({"unchecked"})
+    protected SecurityManager doCreateSecurityManager(Ini ini, Ini.Section mainSection) {
+
+        Map<String, Object> objects = buildMainInstances(mainSection);
+
+        SecurityManager securityManager = (SecurityManager) objects.get(SECURITY_MANAGER_NAME);
+
+        //realms and realm factory might have been created - pull them out first so we can
+        //initialize the securityManager:
+        Collection<Realm> realms = getRealms(objects);
+
+        if (shouldImplicitlyCreateRealm(ini)) {
+            Realm realm = createRealm(ini);
+            realms.add(realm);
+        }
+
         //set them on the SecurityManager
-        if (!realms.isEmpty()) {
-            if (securityManager instanceof RealmSecurityManager) {
-                ((RealmSecurityManager) securityManager).setRealms(realms);
-            }
-            LifecycleUtils.init(realms);
+        if (!CollectionUtils.isEmpty(realms)) {
+            applyRealmsToSecurityManager(realms, securityManager);
         }
 
         return securityManager;

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/AuthorizingRealm.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/AuthorizingRealm.java?rev=888899&r1=888898&r2=888899&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/AuthorizingRealm.java
(original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/AuthorizingRealm.java
Wed Dec  9 18:07:16 2009
@@ -77,6 +77,7 @@
     /**
      * The cache used by this realm to store AuthorizationInfo instances associated with
individual Subject principals.
      */
+    private boolean authorizationCachingEnabled = true;
     private Cache authorizationCache = null;
     private String authorizationCacheName = null;
 
@@ -122,6 +123,33 @@
         this.authorizationCacheName = authorizationCacheName;
     }
 
+    /**
+     * Returns {@code true} if authorization caching should be utilized if a {@link CacheManager}
has been
+     * {@link #setCacheManager(org.apache.shiro.cache.CacheManager) configured}, {@code false}
otherwise.
+     * <p/>
+     * The default value is {@code true}.
+     *
+     * @return {@code true} if authorization caching should be utilized, {@code false} otherwise.
+     */
+    public boolean isAuthorizationCachingEnabled() {
+        return isCachingEnabled() && authorizationCachingEnabled;
+    }
+
+    /**
+     * Sets whether or not authorization caching should be utilized if a {@link CacheManager}
has been
+     * {@link #setCacheManager(org.apache.shiro.cache.CacheManager) configured}, {@code false}
otherwise.
+     * <p/>
+     * The default value is {@code true}.
+     *
+     * @param authorizationCachingEnabled the value to set
+     */
+    public void setAuthorizationCachingEnabled(boolean authorizationCachingEnabled) {
+        this.authorizationCachingEnabled = authorizationCachingEnabled;
+        if (authorizationCachingEnabled) {
+            setCachingEnabled(true);
+        }
+    }
+
     public PermissionResolver getPermissionResolver() {
         return permissionResolver;
     }
@@ -154,7 +182,9 @@
      * </ol>
      */
     public final void init() {
-        initAuthorizationCache();
+        if (isAuthorizationCachingEnabled()) {
+            initAuthorizationCache();
+        }
         onInit();
     }
 
@@ -163,7 +193,9 @@
 
     protected void afterCacheManagerSet() {
         this.authorizationCache = null;
-        initAuthorizationCache();
+        if (isAuthorizationCachingEnabled()) {
+            initAuthorizationCache();
+        }
     }
 
     protected void afterAuthorizationCacheSet() {
@@ -175,6 +207,11 @@
     }
 
     public void initAuthorizationCache() {
+        if (!isAuthorizationCachingEnabled()) {
+            log.debug("Authorization caching is disabled.  Returning immediately.");
+            return;
+        }
+
         if (log.isTraceEnabled()) {
             log.trace("Initializing authorization cache.");
         }

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/CachingRealm.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/CachingRealm.java?rev=888899&r1=888898&r2=888899&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/CachingRealm.java (original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/CachingRealm.java Wed
Dec  9 18:07:16 2009
@@ -20,27 +20,31 @@
 
 import org.apache.shiro.cache.CacheManager;
 import org.apache.shiro.cache.CacheManagerAware;
+import org.apache.shiro.util.Nameable;
+
+import java.util.concurrent.atomic.AtomicInteger;
 
 
 /**
  * <p>A very basic abstract extension point for the {@link Realm} interface that provides
logging and caching support.
- *
+ * <p/>
  * <p>All actual Realm method implementations are left to subclasses.
  *
  * @author Les Hazlewood
  * @since 0.9
  */
-public abstract class CachingRealm implements Realm, CacheManagerAware {
+public abstract class CachingRealm implements Realm, Nameable, CacheManagerAware {
 
     //TODO - complete JavaDoc
 
-    private static int INSTANCE_COUNT = 0;
+    private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger();
 
     /*--------------------------------------------
     |    I N S T A N C E   V A R I A B L E S    |
     ============================================*/
-    private String name = getClass().getName() + "_" + INSTANCE_COUNT++;
+    private String name = getClass().getName() + "_" + INSTANCE_COUNT.getAndIncrement();
 
+    private boolean cachingEnabled = true;
     private CacheManager cacheManager;
 
     public CachingRealm() {
@@ -63,7 +67,7 @@
 
     /**
      * Sets the <tt>CacheManager</tt> to be used for data caching to reduce EIS
round trips.
-     *
+     * <p/>
      * <p>This property is <tt>null</tt> by default, indicating that caching
is turned off.
      *
      * @param authzInfoCacheManager the <tt>CacheManager</tt> to use for data
caching, or <tt>null</tt> to disable caching.
@@ -73,6 +77,31 @@
         afterCacheManagerSet();
     }
 
+    /**
+     * Returns {@code true} if caching should be used if a {@link CacheManager} has been
+     * {@link #setCacheManager(org.apache.shiro.cache.CacheManager) configured}, {@code false}
otherwise.
+     * <p/>
+     * The default value is {@code true} since the large majority of Realms will benefit
from caching if a CacheManager
+     * has been configured.  However, memory-only realms should set this value to {@code
false} since they would
+     * manage account data in memory already lookups would already be as efficient as possible.
+     *
+     * @return {@code true} if caching will be globally enabled if a {@link CacheManager}
has been
+     *         configured, {@code false} otherwise
+     */
+    public boolean isCachingEnabled() {
+        return cachingEnabled;
+    }
+
+    /**
+     * Sets whether or not caching should be used if a {@link CacheManager} has been
+     * {@link #setCacheManager(org.apache.shiro.cache.CacheManager) configured}.
+     *
+     * @param cachingEnabled whether or not to globally enable caching for this realm.
+     */
+    public void setCachingEnabled(boolean cachingEnabled) {
+        this.cachingEnabled = cachingEnabled;
+    }
+
     protected void afterCacheManagerSet() {
     }
 

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/SimpleAccountRealm.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/SimpleAccountRealm.java?rev=888899&r1=888898&r2=888899&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/SimpleAccountRealm.java
(original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/SimpleAccountRealm.java
Wed Dec  9 18:07:16 2009
@@ -52,6 +52,9 @@
     public SimpleAccountRealm() {
         this.users = new LinkedHashMap<String, SimpleAccount>();
         this.roles = new LinkedHashMap<String, SimpleRole>();
+        //SimpleAccountRealms are memory-only realms - no need for an additional cache mechanism
since we're
+        //already as memory-efficient as we can be:
+        setCachingEnabled(false);
     }
 
     public SimpleAccountRealm(String name) {
@@ -145,8 +148,4 @@
     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
         return this.users.get(getUsername(principals));
     }
-
-    protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
-        return getAvailablePrincipal(principals); //returns the username, being the only
principal from this Realm
-    }
 }
\ No newline at end of file

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/text/IniRealm.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/text/IniRealm.java?rev=888899&r1=888898&r2=888899&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/text/IniRealm.java (original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/text/IniRealm.java Wed
Dec  9 18:07:16 2009
@@ -36,6 +36,9 @@
  * ...
  * [roles]
  * # One or more {@link org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions(String)
role definitions}</pre>
+ * <p/>
+ * This class also supports setting the {@link #setResourcePath(String) resourcePath} property
to create account
+ * data from an .ini resource.  This will only be used if there isn't already account data
in the Realm.
  */
 public class IniRealm extends TextConfigurationRealm {
 
@@ -65,13 +68,25 @@
 
     @Override
     public void onInit() {
+        // We override init() instead of onInit() because we _don't_ want any caches to be
created
+        // (see the superclass init() code).
+        // This is an in-memory realm only - no need for an additional cache when we're already
+        // as memory-efficient as we can be.
         String resourcePath = getResourcePath();
-        if (StringUtils.hasText(resourcePath)) {
-            log.debug("Resource path {} defined.  Creating INI instance.", resourcePath);
-            Ini ini = Ini.fromResourcePath(resourcePath);
-            processDefinitions(ini);
+
+        if (CollectionUtils.isEmpty(this.users) && CollectionUtils.isEmpty(this.roles))
{
+            //no account data manually populated - try the resource path:
+            if (StringUtils.hasText(resourcePath)) {
+                log.debug("Resource path {} defined.  Creating INI instance.", resourcePath);
+                Ini ini = Ini.fromResourcePath(resourcePath);
+                processDefinitions(ini);
+            } else {
+                throw new IllegalStateException("No resource path was specified.  Cannot
load account data.");
+            }
         } else {
-            throw new IllegalStateException("No resource path was specified.  Cannot load
account data.");
+            if (StringUtils.hasText(resourcePath)) {
+                log.warn("Users or Roles are already populated.  Resource path property will
be ignored.");
+            }
         }
     }
 

Added: incubator/shiro/trunk/core/src/test/java/org/apache/shiro/config/IniSecurityManagerFactoryTest.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/test/java/org/apache/shiro/config/IniSecurityManagerFactoryTest.java?rev=888899&view=auto
==============================================================================
--- incubator/shiro/trunk/core/src/test/java/org/apache/shiro/config/IniSecurityManagerFactoryTest.java
(added)
+++ incubator/shiro/trunk/core/src/test/java/org/apache/shiro/config/IniSecurityManagerFactoryTest.java
Wed Dec  9 18:07:16 2009
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.shiro.config;
+
+import static junit.framework.Assert.*;
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.realm.Realm;
+import org.apache.shiro.realm.text.IniRealm;
+import org.apache.shiro.realm.text.PropertiesRealm;
+import org.junit.Test;
+
+import java.util.Collection;
+
+/**
+ * Unit tests for the {@link IniSecurityManagerFactory} implementation.
+ *
+ * @author The Apache Shiro Project (shiro-dev@incubator.apache.org)
+ * @since 1.0
+ */
+public class IniSecurityManagerFactoryTest {
+
+    IniSecurityManagerFactory factory;
+
+    @Test
+    public void testGetInstanceWithoutIni() {
+        IniSecurityManagerFactory factory = new IniSecurityManagerFactory();
+        SecurityManager sm = factory.getInstance();
+        assertNotNull(sm);
+        assertTrue(sm instanceof DefaultSecurityManager);
+    }
+
+    @Test
+    public void testGetInstanceWithEmptyIni() {
+        Ini ini = new Ini();
+        IniSecurityManagerFactory factory = new IniSecurityManagerFactory(ini);
+        SecurityManager sm = factory.getInstance();
+        assertNotNull(sm);
+        assertTrue(sm instanceof DefaultSecurityManager);
+    }
+
+    @Test
+    public void testGetInstanceWithSimpleIni() {
+        Ini ini = new Ini();
+        ini.setSectionProperty(IniSecurityManagerFactory.MAIN_SECTION_NAME, "securityManager.globalSessionTimeout",
"5000");
+        IniSecurityManagerFactory factory = new IniSecurityManagerFactory(ini);
+        SecurityManager sm = factory.getInstance();
+        assertNotNull(sm);
+        assertTrue(sm instanceof DefaultSecurityManager);
+        assertEquals(5000, ((DefaultSecurityManager) sm).getGlobalSessionTimeout());
+    }
+
+    @Test
+    public void testGetInstanceWithConfiguredRealm() {
+        Ini ini = new Ini();
+        Ini.Section section = ini.addSection(IniSecurityManagerFactory.MAIN_SECTION_NAME);
+        section.put("propsRealm", PropertiesRealm.class.getName());
+
+        IniSecurityManagerFactory factory = new IniSecurityManagerFactory(ini);
+        SecurityManager sm = factory.getInstance();
+        assertNotNull(sm);
+        assertTrue(sm instanceof DefaultSecurityManager);
+        Collection<Realm> realms = ((DefaultSecurityManager) sm).getRealms();
+        assertEquals(1, realms.size());
+        Realm realm = realms.iterator().next();
+        assertTrue(realm instanceof PropertiesRealm);
+    }
+
+    @Test
+    public void testGetInstanceWithAutomaticallyCreatedIniRealm() {
+        Ini ini = new Ini();
+        Ini.Section section = ini.addSection(IniRealm.USERS_SECTION_NAME);
+        section.put("admin", "admin");
+
+        IniSecurityManagerFactory factory = new IniSecurityManagerFactory(ini);
+        SecurityManager sm = factory.getInstance();
+        assertNotNull(sm);
+        assertTrue(sm instanceof DefaultSecurityManager);
+        Collection<Realm> realms = ((DefaultSecurityManager) sm).getRealms();
+        assertEquals(1, realms.size());
+        Realm realm = realms.iterator().next();
+        assertTrue(realm instanceof IniRealm);
+        assertTrue(((IniRealm) realm).accountExists("admin"));
+    }
+
+
+}



Mime
View raw message