From aa9051dd6a5622fc55bfb22d56f674eadaaac2f8 Mon Sep 17 00:00:00 2001 From: Nikita Koksharov Date: Wed, 23 Nov 2022 08:33:44 +0300 Subject: [PATCH] Fixed - hRandFieldWithValues() and hRandField() methods of Spring Data Redis module throw ClassCastException. #4688 --- .../ObjectMapEntryReplayDecoder.java | 55 +++++++++++++++++++ .../data/connection/RedissonConnection.java | 12 ++-- .../connection/SingleMapEntryDecoder.java | 47 ++++++++++++++++ .../ObjectMapEntryReplayDecoder.java | 55 +++++++++++++++++++ .../data/connection/RedissonConnection.java | 12 ++-- .../connection/SingleMapEntryDecoder.java | 47 ++++++++++++++++ .../ObjectMapEntryReplayDecoder.java | 52 ++++++++++++++++++ .../data/connection/RedissonConnection.java | 12 ++-- .../connection/SingleMapEntryDecoder.java | 46 ++++++++++++++++ .../connection/RedissonConnectionTest.java | 17 ++++++ .../convertor/EmptyListConvertor.java | 33 +++++++++++ 11 files changed, 376 insertions(+), 12 deletions(-) create mode 100644 redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java create mode 100644 redisson/src/main/java/org/redisson/client/protocol/convertor/EmptyListConvertor.java diff --git a/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java b/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java new file mode 100644 index 000000000..fe3d4be23 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class ObjectMapEntryReplayDecoder implements MultiDecoder>> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return codec.getMapValueDecoder(); + } + return codec.getMapKeyDecoder(); + } + + @Override + public List> decode(List parts, State state) { + Map result = new LinkedHashMap<>(parts.size() / 2); + for (int i = 0; i < parts.size(); i++) { + if (i % 2 != 0) { + result.put(parts.get(i-1), parts.get(i)); + } + } + return new ArrayList<>(result.entrySet()); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java b/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java index 800c3616b..8c2d70b44 100644 --- a/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java +++ b/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java @@ -2637,20 +2637,22 @@ public class RedissonConnection extends AbstractRedisConnection { return zUnionWithScores(null, (Weights) null, sets); } - private static final RedisCommand HRANDFIELD = new RedisCommand("HRANDFIELD"); + private static final RedisCommand HRANDFIELD = new RedisCommand<>("HRANDFIELD"); @Override public byte[] hRandField(byte[] key) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD, (Object) key); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD, key); } + private static final RedisCommand> HRANDFIELD_SINGLE = new RedisCommand("HRANDFIELD", new SingleMapEntryDecoder()); + @Override public Entry hRandFieldWithValues(byte[] key) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, (Object) key); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_SINGLE, key, 1, "WITHVALUES"); } private static final RedisCommand> HRANDFIELD_LIST = new RedisCommand<>("HRANDFIELD", new ObjectListReplayDecoder<>()); @@ -2662,11 +2664,13 @@ public class RedissonConnection extends AbstractRedisConnection { return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_LIST, key, count); } + private static final RedisCommand>> HRANDFIELD_VALUES = new RedisCommand("HRANDFIELD", + new ObjectMapEntryReplayDecoder(), new EmptyListConvertor()); @Override public List> hRandFieldWithValues(byte[] key, long count) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, (Object) key, count); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_VALUES, key, count, "WITHVALUES"); } @Override diff --git a/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java b/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java new file mode 100644 index 000000000..abdaefdd4 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-26/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +import java.util.AbstractMap; +import java.util.List; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class SingleMapEntryDecoder implements MultiDecoder> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return codec.getMapValueDecoder(); + } + return codec.getMapKeyDecoder(); + } + + @Override + public Entry decode(List parts, State state) { + return new AbstractMap.SimpleEntry<>(parts.get(0), parts.get(1)); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java b/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java new file mode 100644 index 000000000..fe3d4be23 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class ObjectMapEntryReplayDecoder implements MultiDecoder>> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return codec.getMapValueDecoder(); + } + return codec.getMapKeyDecoder(); + } + + @Override + public List> decode(List parts, State state) { + Map result = new LinkedHashMap<>(parts.size() / 2); + for (int i = 0; i < parts.size(); i++) { + if (i % 2 != 0) { + result.put(parts.get(i-1), parts.get(i)); + } + } + return new ArrayList<>(result.entrySet()); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java b/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java index 4ccee7448..8bee3103c 100644 --- a/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java +++ b/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java @@ -2636,20 +2636,22 @@ public class RedissonConnection extends AbstractRedisConnection { return zUnionWithScores(null, (Weights) null, sets); } - private static final RedisCommand HRANDFIELD = new RedisCommand("HRANDFIELD"); + private static final RedisCommand HRANDFIELD = new RedisCommand<>("HRANDFIELD"); @Override public byte[] hRandField(byte[] key) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD, (Object) key); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD, key); } + private static final RedisCommand> HRANDFIELD_SINGLE = new RedisCommand("HRANDFIELD", new SingleMapEntryDecoder()); + @Override public Entry hRandFieldWithValues(byte[] key) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, (Object) key); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_SINGLE, key, 1, "WITHVALUES"); } private static final RedisCommand> HRANDFIELD_LIST = new RedisCommand<>("HRANDFIELD", new ObjectListReplayDecoder<>()); @@ -2661,11 +2663,13 @@ public class RedissonConnection extends AbstractRedisConnection { return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_LIST, key, count); } + private static final RedisCommand>> HRANDFIELD_VALUES = new RedisCommand("HRANDFIELD", + new ObjectMapEntryReplayDecoder(), new EmptyListConvertor()); @Override public List> hRandFieldWithValues(byte[] key, long count) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, (Object) key, count); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_VALUES, key, count, "WITHVALUES"); } @Override diff --git a/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java b/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java new file mode 100644 index 000000000..abdaefdd4 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-27/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +import java.util.AbstractMap; +import java.util.List; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class SingleMapEntryDecoder implements MultiDecoder> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return codec.getMapValueDecoder(); + } + return codec.getMapKeyDecoder(); + } + + @Override + public Entry decode(List parts, State state) { + return new AbstractMap.SimpleEntry<>(parts.get(0), parts.get(1)); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java new file mode 100644 index 000000000..bcd163a1d --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectMapEntryReplayDecoder.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +import java.util.*; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class ObjectMapEntryReplayDecoder implements MultiDecoder>> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return codec.getMapValueDecoder(); + } + return codec.getMapKeyDecoder(); + } + + @Override + public List> decode(List parts, State state) { + Map result = new LinkedHashMap<>(parts.size() / 2); + for (int i = 0; i < parts.size(); i++) { + if (i % 2 != 0) { + result.put(parts.get(i-1), parts.get(i)); + } + } + return new ArrayList<>(result.entrySet()); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java index 6b76471c4..25202d7b9 100644 --- a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java @@ -2562,20 +2562,22 @@ public class RedissonConnection extends AbstractRedisConnection { return zUnionWithScores(null, (Weights) null, sets); } - private static final RedisCommand HRANDFIELD = new RedisCommand("HRANDFIELD"); + private static final RedisCommand HRANDFIELD = new RedisCommand<>("HRANDFIELD"); @Override public byte[] hRandField(byte[] key) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD, (Object) key); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD, key); } + private static final RedisCommand> HRANDFIELD_SINGLE = new RedisCommand("HRANDFIELD", new SingleMapEntryDecoder()); + @Override public Entry hRandFieldWithValues(byte[] key) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, (Object) key); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_SINGLE, key, 1, "WITHVALUES"); } private static final RedisCommand> HRANDFIELD_LIST = new RedisCommand<>("HRANDFIELD", new ObjectListReplayDecoder<>()); @@ -2587,11 +2589,13 @@ public class RedissonConnection extends AbstractRedisConnection { return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_LIST, key, count); } + private static final RedisCommand>> HRANDFIELD_VALUES = new RedisCommand("HRANDFIELD", + new ObjectMapEntryReplayDecoder(), new EmptyListConvertor()); @Override public List> hRandFieldWithValues(byte[] key, long count) { Assert.notNull(key, "Key must not be null!"); - return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, (Object) key, count); + return read(key, ByteArrayCodec.INSTANCE, HRANDFIELD_VALUES, key, count, "WITHVALUES"); } @Override diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java new file mode 100644 index 000000000..6f4f1105c --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SingleMapEntryDecoder.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +import java.util.*; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class SingleMapEntryDecoder implements MultiDecoder> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return codec.getMapValueDecoder(); + } + return codec.getMapKeyDecoder(); + } + + @Override + public Entry decode(List parts, State state) { + return new AbstractMap.SimpleEntry<>(parts.get(0), parts.get(1)); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java index 297a87ba5..e3cc8a862 100644 --- a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java @@ -16,6 +16,8 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.SetOperations; import org.springframework.data.redis.core.types.Expiration; +import java.util.List; +import java.util.Map; import java.util.Set; public class RedissonConnectionTest extends BaseConnectionTest { @@ -121,5 +123,20 @@ public class RedissonConnectionTest extends BaseConnectionTest { assertThat(t.next().getValue()).isEqualTo("value2".getBytes()); } + @Test + public void testRandFieldWithValues() { + connection.hSet("map".getBytes(), "key1".getBytes(), "value1".getBytes()); + connection.hSet("map".getBytes(), "key2".getBytes(), "value2".getBytes()); + connection.hSet("map".getBytes(), "key3".getBytes(), "value3".getBytes()); + + List> s = connection.hRandFieldWithValues("map".getBytes(), 2); + assertThat(s).hasSize(2); + + Map.Entry s2 = connection.hRandFieldWithValues("map".getBytes()); + assertThat(s2).isNotNull(); + + byte[] f = connection.hRandField("map".getBytes()); + assertThat((Object) f).isIn("key1".getBytes(), "key2".getBytes(), "key3".getBytes()); + } } diff --git a/redisson/src/main/java/org/redisson/client/protocol/convertor/EmptyListConvertor.java b/redisson/src/main/java/org/redisson/client/protocol/convertor/EmptyListConvertor.java new file mode 100644 index 000000000..5270f233b --- /dev/null +++ b/redisson/src/main/java/org/redisson/client/protocol/convertor/EmptyListConvertor.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.client.protocol.convertor; + +import java.util.Collections; + +/** + * @author Nikita Koksharov + */ +public class EmptyListConvertor implements Convertor { + + @Override + public Object convert(Object obj) { + if (obj == null) { + return Collections.emptyList(); + } + return obj; + } + +}