From 20e0670f38f2b37474fccf81b2ebf7ab655d8793 Mon Sep 17 00:00:00 2001 From: David Turanski Date: Mon, 11 Mar 2013 13:12:23 -0400 Subject: [PATCH] SGF-167 also, changed default to close='false' on RFB --- build.gradle | 5 + .../data/gemfire/RegionFactoryBean.java | 2 +- .../config/GemfireDataNamespaceHandler.java | 1 + .../config/GemfireRegionAutoProxyParser.java | 53 ++++ .../gemfire/support/JSONRegionAdvice.java | 246 ++++++++++++++++++ .../config/spring-data-gemfire-1.3.xsd | 31 +++ .../gemfire/config/spring-gemfire-1.3.xsd | 7 +- .../gemfire/support/JSONRegionAdviceTest.java | 74 ++++++ .../support/JSONRegionAdviceTest-context.xml | 27 ++ 9 files changed, 440 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/config/GemfireRegionAutoProxyParser.java create mode 100644 src/main/java/org/springframework/data/gemfire/support/JSONRegionAdvice.java create mode 100644 src/test/java/org/springframework/data/gemfire/support/JSONRegionAdviceTest.java create mode 100644 src/test/resources/org/springframework/data/gemfire/support/JSONRegionAdviceTest-context.xml diff --git a/build.gradle b/build.gradle index 9d50f8f8..c7b86d1c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,11 @@ dependencies { } compile "org.springframework:spring-context-support:$springVersion" compile "org.springframework:spring-tx:$springVersion" + compile "org.springframework:spring-aop:$springVersion" + compile 'org.aspectj:aspectjweaver:1.7.2' + compile 'org.aspectj:aspectjrt:1.7.2' + runtime 'org.codehaus.jackson:jackson-core-asl:1.9.12' + runtime 'org.codehaus.jackson:jackson-mapper-asl:1.9.12' // GemFire compile("com.gemstone.gemfire:gemfire:$gemfireVersion") diff --git a/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java b/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java index 83bc1cb3..33fbcb76 100644 --- a/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java @@ -251,7 +251,7 @@ public class RegionFactoryBean extends RegionLookupFactoryBean imple try { region.close(); region = null; - } catch (CacheClosedException cce) { + } catch (Exception cce) { // nothing to see folks, move on. } } diff --git a/src/main/java/org/springframework/data/gemfire/config/GemfireDataNamespaceHandler.java b/src/main/java/org/springframework/data/gemfire/config/GemfireDataNamespaceHandler.java index 78daac6e..b3655fb8 100644 --- a/src/main/java/org/springframework/data/gemfire/config/GemfireDataNamespaceHandler.java +++ b/src/main/java/org/springframework/data/gemfire/config/GemfireDataNamespaceHandler.java @@ -38,5 +38,6 @@ class GemfireDataNamespaceHandler extends NamespaceHandlerSupport { registerBeanDefinitionParser("repositories", new RepositoryBeanDefinitionParser(extension)); registerBeanDefinitionParser("function-executions", new FunctionExecutionBeanDefinitionParser()); registerBeanDefinitionParser("datasource", new GemfireDataSourceParser()); + registerBeanDefinitionParser("json-region-autoproxy", new GemfireRegionAutoProxyParser()); } } \ No newline at end of file diff --git a/src/main/java/org/springframework/data/gemfire/config/GemfireRegionAutoProxyParser.java b/src/main/java/org/springframework/data/gemfire/config/GemfireRegionAutoProxyParser.java new file mode 100644 index 00000000..0e5598b9 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/GemfireRegionAutoProxyParser.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.springframework.data.gemfire.config; + +import org.springframework.aop.config.AopNamespaceUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.gemfire.support.JSONRegionAdvice; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * @author David Turanski + * + */ +public class GemfireRegionAutoProxyParser implements BeanDefinitionParser { + + /* (non-Javadoc) + * @see org.springframework.beans.factory.xml.BeanDefinitionParser#parse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext) + */ + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(JSONRegionAdvice.class); + ParsingUtils.setPropertyValue(element, builder, "pretty-print"); + ParsingUtils.setPropertyValue(element, builder, "convert-returned-collections"); + String regionNames = element.getAttribute("included-regions"); + if (StringUtils.hasText(regionNames)) { + String[] regions = StringUtils.commaDelimitedListToStringArray(regionNames); + ManagedList regionList = new ManagedList(regions.length); + for (String regionRef : regions) { + regionList.add(regionRef); + } + builder.addPropertyValue("includedRegions", regionList); + } + BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), parserContext.getRegistry()); + return builder.getBeanDefinition(); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/support/JSONRegionAdvice.java b/src/main/java/org/springframework/data/gemfire/support/JSONRegionAdvice.java new file mode 100644 index 00000000..a5ee0447 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/support/JSONRegionAdvice.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.springframework.data.gemfire.support; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.codehaus.jackson.map.ObjectMapper; +import org.springframework.util.CollectionUtils; + +import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.pdx.JSONFormatter; +import com.gemstone.gemfire.pdx.PdxInstance; + +/** + * @author David Turanski + * + */ +@Aspect +public class JSONRegionAdvice { + private static Log log = LogFactory.getLog(JSONRegionAdvice.class); + private List includedRegions; + private boolean convertReturnedCollections = true; + private boolean prettyPrint = false; + + /** + * Sets names of regions to be included for JSON conversion. By default, all regions will be included + * @param regions a List of region names to include + */ + public void setIncludedRegionNames(List regionNames) { + this.includedRegions = regionNames; + } + + /** + * Sets regions to be included for JSON conversion. By default, all regions will be included + * @param regions a List of region names to include + */ + public void setIncludedRegions(List> regions) { + this.includedRegions = new ArrayList(); + for (Region region: regions) { + includedRegions.add(region.getName()); + } + } + /** + * Flag to convert collections returned from cache from @{link PdxInstance} to JSON String. If the returned + * collections are very large, overhead will be incurred to covert all the values from from + * Region.getAll() and Region.values() + * @param convertReturnedCollections true by default + */ + public void setConvertReturnedCollections(boolean convertReturnedCollections) { + this.convertReturnedCollections = convertReturnedCollections; + } + + /** + * Flag to print JSON Strings with proper indentation, etc. + * @param prettyPrint false be default + */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + } + + @Around("execution(* com.gemstone.gemfire.cache.Region.put(..)) || " + + "execution(* com.gemstone.gemfire.cache.Region.create(..)) ||" + + "execution(* com.gemstone.gemfire.cache.Region.putIfAbsent(..)) ||" + + "execution(* com.gemstone.gemfire.cache.Region.replace(..))") + public Object put(ProceedingJoinPoint pjp) { + System.out.println("intercepted " + pjp.getSignature().getName()); + + boolean JSONRegion = isIncludedSONRegion(pjp.getTarget()); + Object retVal = null; + try { + if (JSONRegion) { + Object[] newArgs = Arrays.copyOf(pjp.getArgs(), pjp.getArgs().length); + Object val = newArgs[1]; + newArgs[1] = convertArgumentToPdxInstance(val); + retVal = pjp.proceed(newArgs); + } else { + retVal = pjp.proceed(); + } + } catch (Throwable t) { + handleThrowable(t); + } + return retVal; + } + + @Around("execution(* com.gemstone.gemfire.cache.Region.putAll(..))") + public Object putAll(ProceedingJoinPoint pjp) { + boolean JSONRegion = isIncludedSONRegion(pjp.getTarget()); + + Object retVal = null; + try { + if (JSONRegion) { + Object[] newArgs = Arrays.copyOf(pjp.getArgs(), pjp.getArgs().length); + Map val = (Map) newArgs[0]; + Map newArg = new HashMap(); + for (Entry entry : val.entrySet()) { + newArg.put(entry.getKey(), convertArgumentToPdxInstance(entry.getValue())); + } + newArgs[0] = newArg; + retVal = pjp.proceed(newArgs); + } else { + retVal = pjp.proceed(); + } + } catch (Throwable t) { + handleThrowable(t); + } + return retVal; + } + + @Around("execution(* com.gemstone.gemfire.cache.Region.get(..)) " + + "|| execution(* com.gemstone.gemfire.cache.Region.selectValue(..))") + public Object get(ProceedingJoinPoint pjp) { + Object retVal = null; + try { + if (isIncludedSONRegion(pjp.getTarget())) { + retVal = pjp.proceed(); + log.debug("converting " + retVal + " to JSON string"); + return convertPdxInstanceToJSONString(retVal); + } else { + return pjp.proceed(); + } + } catch (Throwable t) { + handleThrowable(t); + } + return retVal; + + } + + @SuppressWarnings("unchecked") + @Around("execution(* com.gemstone.gemfire.cache.Region.getAll(..))") + public Map getAll(ProceedingJoinPoint pjp) { + Map result = null; + try { + Map retVal = (Map) pjp.proceed(); + if (!convertReturnedCollections || CollectionUtils.isEmpty(retVal) || !isIncludedSONRegion(pjp.getTarget())) { + result = retVal; + } else { + result = new HashMap(); + for (Entry entry : retVal.entrySet()) { + result.put(entry.getKey(), convertPdxInstanceToJSONString(entry.getValue())); + } + } + } catch (Throwable t) { + handleThrowable(t); + } + return result; + } + + @SuppressWarnings("unchecked") + @Around("execution(* com.gemstone.gemfire.cache.Region.values(..))") + public Collection values(ProceedingJoinPoint pjp) { + Collection result = null; + try { + Collection retVal = (Collection) pjp.proceed(); + if (!convertReturnedCollections || CollectionUtils.isEmpty(retVal) || !isIncludedSONRegion(pjp.getTarget())) { + result = retVal; + } else { + result = new ArrayList(); + for (Object obj : retVal) { + result.add(convertArgumentToPdxInstance(obj)); + } + } + } catch (Throwable t) { + handleThrowable(t); + } + return result; + } + + private PdxInstance convertArgumentToPdxInstance(Object value) { + PdxInstance val = null; + if (value instanceof PdxInstance) { + val = (PdxInstance) value; + } else if (value instanceof String) { + val = JSONFormatter.fromJSON((String) value); + } else { + ObjectMapper mapper = new ObjectMapper(); + try { + String json = mapper.writeValueAsString(value); + val = JSONFormatter.fromJSON(json); + } catch (Throwable t) { + handleThrowable(t); + } + } + return val; + } + + private boolean isIncludedSONRegion(Object target) { + Region region = (Region) target; + boolean result = false; + if (includedRegions == null || includedRegions.contains(region.getName())) { + if (log.isDebugEnabled()) { + log.debug(region.getName() + " is included for JSON conversion"); + } + result = true; + } + return result; + } + + private Object convertPdxInstanceToJSONString(Object retVal) { + Object result = retVal; + if (retVal != null && retVal instanceof PdxInstance) { + result = JSONFormatter.toJSON((PdxInstance) retVal); + } + if (!prettyPrint) { + result = flattenString(result); + } + return result; + } + + private Object flattenString(Object result) { + if (result instanceof String) { + String json = (String) result; + return json.replaceAll("\\s*", ""); + } + return result; + } + + private void handleThrowable(Throwable t) { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new RuntimeException(t); + } + + } +} diff --git a/src/main/resources/org/springframework/data/gemfire/config/spring-data-gemfire-1.3.xsd b/src/main/resources/org/springframework/data/gemfire/config/spring-data-gemfire-1.3.xsd index c8568411..cc2afd2a 100644 --- a/src/main/resources/org/springframework/data/gemfire/config/spring-data-gemfire-1.3.xsd +++ b/src/main/resources/org/springframework/data/gemfire/config/spring-data-gemfire-1.3.xsd @@ -140,4 +140,35 @@ Defines a connection from a Cache client to a set of GemFire Cache Servers. type="xsd:string" use="optional" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.3.xsd b/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.3.xsd index 7e0a18d1..c9aad6bc 100755 --- a/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.3.xsd +++ b/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.3.xsd @@ -602,22 +602,19 @@ After the destroy, this region object can not be used any more and any attempt t RegionDestroyedException. Default is false, meaning that regions are not destroyed. - -Note: destroy and close are mutually exclusive. Enabling one will automatically disable the other. ]]> + default="false"> diff --git a/src/test/java/org/springframework/data/gemfire/support/JSONRegionAdviceTest.java b/src/test/java/org/springframework/data/gemfire/support/JSONRegionAdviceTest.java new file mode 100644 index 00000000..c3e84f5d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/support/JSONRegionAdviceTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.springframework.data.gemfire.support; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Resource; + +import org.codehaus.jackson.JsonGenerationException; +import org.codehaus.jackson.map.JsonMappingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.gemfire.repository.sample.Person; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.gemstone.gemfire.cache.Region; + +/** + * @author David Turanski + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class JSONRegionAdviceTest { + @Resource(name="someRegion") + private Region region; + @Test + public void testPutString() { + String json = "{\"hello\":\"world\"}"; + region.put("key",json); + region.create("key2",json); + System.out.println(region.get("key")); + assertEquals(json,region.get("key")); + } + + @Test + public void testPutAll() { + Map map = new HashMap(); + map.put("key1", "{\"hello1\":\"world1\"}"); + map.put("key2", "{\"hello2\":\"world2\"}"); + region.putAll(map); + List keys = Arrays.asList(new String[]{"key1","key2"}); + Map results = region.getAll(keys); + assertEquals("{\"hello1\":\"world1\"}",results.get("key1")); + assertEquals("{\"hello2\":\"world2\"}",results.get("key2")); + } + + @Test + public void testObjectToJSon() throws JsonGenerationException, JsonMappingException, IOException { + Person dave = new Person(1L,"Dave","Turanski"); + region.put("dave",dave); + String json = (String)region.get("dave"); + assertEquals(json,"{\"id\":1,\"firstname\":\"Dave\",\"lastname\":\"Turanski\",\"address\":null}",json); + } + +} diff --git a/src/test/resources/org/springframework/data/gemfire/support/JSONRegionAdviceTest-context.xml b/src/test/resources/org/springframework/data/gemfire/support/JSONRegionAdviceTest-context.xml new file mode 100644 index 00000000..37661fab --- /dev/null +++ b/src/test/resources/org/springframework/data/gemfire/support/JSONRegionAdviceTest-context.xml @@ -0,0 +1,27 @@ + + + + + 127.0.0.1 + + + + + + + + + + +