http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml index f0fd64e..afa6cd8 100644 --- a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -42,7 +42,7 @@ </cm:default-properties> </cm:property-placeholder> - <cm:property-placeholder persistent-id="org.oasis_open.contextserver.web" + <cm:property-placeholder persistent-id="org.apache.unomi.web" update-strategy="reload" placeholder-prefix="${web."> <cm:default-properties> <cm:property name="contextserver.address" value="localhost"/> @@ -54,23 +54,23 @@ <service id="elasticSearchPersistenceService" ref="elasticSearchPersistenceServiceImpl"> <interfaces> - <value>org.oasis_open.contextserver.persistence.spi.PersistenceService</value> - <value>org.oasis_open.contextserver.api.services.ClusterService</value> + <value>org.apache.unomi.persistence.spi.PersistenceService</value> + <value>org.apache.unomi.api.services.ClusterService</value> </interfaces> </service> <bean id="conditionESQueryBuilderDispatcher" - class="org.oasis_open.contextserver.persistence.elasticsearch.conditions.ConditionESQueryBuilderDispatcher"> + class="org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilderDispatcher"> <property name="bundleContext" ref="blueprintBundleContext"/> </bean> <bean id="conditionEvaluatorDispatcherImpl" - class="org.oasis_open.contextserver.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher"> + class="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher"> <property name="bundleContext" ref="blueprintBundleContext"/> </bean> <bean id="elasticSearchPersistenceServiceImpl" - class="org.oasis_open.contextserver.persistence.elasticsearch.ElasticSearchPersistenceServiceImpl" + class="org.apache.unomi.persistence.elasticsearch.ElasticSearchPersistenceServiceImpl" init-method="start" destroy-method="stop"> <property name="bundleContext" ref="blueprintBundleContext"/>
http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/IPRangeMatcher.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/IPRangeMatcher.java b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/IPRangeMatcher.java new file mode 100644 index 0000000..85b1983 --- /dev/null +++ b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/IPRangeMatcher.java @@ -0,0 +1,158 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Edin Dazdarevic ([email protected]) + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * + */ + +package org.apache.unomi.elasticsearch.plugin.security; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A class that enables to get an IP range from CIDR specification. It supports + * both IPv4 and IPv6. + * <p/> + * This class was adapted from the CIDRUtils code at https://github.com/edazdarevic/CIDRUtils + * and support for IP ranges was added. + */ +public class IPRangeMatcher { + + private InetAddress inetAddress; + private InetAddress startAddress; + private InetAddress endAddress; + private int prefixLength; + + public IPRangeMatcher(String cidrOrIPRange) throws UnknownHostException { + + if (cidrOrIPRange.contains("/")) { + /* split CIDR to address and prefix part */ + int index = cidrOrIPRange.indexOf("/"); + String addressPart = cidrOrIPRange.substring(0, index); + String networkPart = cidrOrIPRange.substring(index + 1); + + inetAddress = InetAddress.getByName(addressPart); + prefixLength = Integer.parseInt(networkPart); + + calculate(); + } else if (cidrOrIPRange.contains("-")) { + String[] rangeParts = cidrOrIPRange.split("-"); + this.startAddress = InetAddress.getByName(rangeParts[0].trim()); + this.endAddress = InetAddress.getByName(rangeParts[1].trim()); + } else { + // we don't handle a range, we just match a single IP address + this.startAddress = InetAddress.getByName(cidrOrIPRange); + this.endAddress = this.startAddress; + } + } + + public IPRangeMatcher(InetAddress startAddress, InetAddress endAddress) { + this.startAddress = startAddress; + this.endAddress = endAddress; + } + + public IPRangeMatcher(String startAddress, String endAddress) throws UnknownHostException { + this.startAddress = InetAddress.getByName(startAddress); + this.endAddress = InetAddress.getByName(endAddress); + } + + + private void calculate() throws UnknownHostException { + + ByteBuffer maskBuffer; + int targetSize; + if (inetAddress.getAddress().length == 4) { + maskBuffer = + ByteBuffer + .allocate(4) + .putInt(-1); + targetSize = 4; + } else { + maskBuffer = ByteBuffer.allocate(16) + .putLong(-1L) + .putLong(-1L); + targetSize = 16; + } + + BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength); + + ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress()); + BigInteger ipVal = new BigInteger(1, buffer.array()); + + BigInteger startIp = ipVal.and(mask); + BigInteger endIp = startIp.add(mask.not()); + + byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize); + byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize); + + this.startAddress = InetAddress.getByAddress(startIpArr); + this.endAddress = InetAddress.getByAddress(endIpArr); + + } + + private byte[] toBytes(byte[] array, int targetSize) { + int counter = 0; + List<Byte> newArr = new ArrayList<Byte>(); + while (counter < targetSize && (array.length - 1 - counter >= 0)) { + newArr.add(0, array[array.length - 1 - counter]); + counter++; + } + + int size = newArr.size(); + for (int i = 0; i < (targetSize - size); i++) { + + newArr.add(0, (byte) 0); + } + + byte[] ret = new byte[newArr.size()]; + for (int i = 0; i < newArr.size(); i++) { + ret[i] = newArr.get(i); + } + return ret; + } + + public String getNetworkAddress() { + + return this.startAddress.getHostAddress(); + } + + public String getBroadcastAddress() { + return this.endAddress.getHostAddress(); + } + + public boolean isInRange(String ipAddress) throws UnknownHostException { + InetAddress address = InetAddress.getByName(ipAddress); + BigInteger start = new BigInteger(1, this.startAddress.getAddress()); + BigInteger end = new BigInteger(1, this.endAddress.getAddress()); + BigInteger target = new BigInteger(1, address.getAddress()); + + int st = start.compareTo(target); + int te = target.compareTo(end); + + return (st == -1 || st == 0) && (te == -1 || te == 0); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPlugin.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPlugin.java b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPlugin.java new file mode 100644 index 0000000..61814a1 --- /dev/null +++ b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPlugin.java @@ -0,0 +1,61 @@ +/* + * 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.unomi.elasticsearch.plugin.security; + +import org.elasticsearch.common.component.LifecycleComponent; +import org.elasticsearch.common.inject.Module; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.AbstractPlugin; + +import java.util.Collection; + +import static org.elasticsearch.common.collect.Lists.newArrayList; + +public class SecurityPlugin extends AbstractPlugin { + + public SecurityPlugin(Settings settings) { + super(); + } + + public String name() { + return "contextserver-security"; + } + + public String description() { + return "A plugin that provides some basic security to the Context Server elasticsearch HTTP and Transport connectors"; + } + + @Override + public Collection<Class<? extends Module>> modules() { + Collection<Class<? extends Module>> modules = newArrayList(); + // if (settings.getAsBoolean("security.enabled", true)) { + modules.add(SecurityPluginModule.class); + // } + return modules; + } + + @SuppressWarnings("rawtypes") + @Override + public Collection<Class<? extends LifecycleComponent>> services() { + Collection<Class<? extends LifecycleComponent>> services = newArrayList(); + // if (settings.getAsBoolean("security.enabled", true)) { + services.add(SecurityPluginService.class); + // } + return services; + } +} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginModule.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginModule.java b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginModule.java new file mode 100644 index 0000000..67e1770 --- /dev/null +++ b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginModule.java @@ -0,0 +1,33 @@ +/* + * 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.unomi.elasticsearch.plugin.security; + +import org.elasticsearch.common.inject.AbstractModule; +import org.elasticsearch.common.settings.Settings; + +public class SecurityPluginModule extends AbstractModule { + + public SecurityPluginModule(Settings settings) { + super(); + } + + @Override + protected void configure() { + bind(SecurityPluginService.class).asEagerSingleton(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginService.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginService.java b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginService.java new file mode 100644 index 0000000..f3941ea --- /dev/null +++ b/persistence-elasticsearch/plugins/security/src/main/java/org/apache/unomi/elasticsearch/plugin/security/SecurityPluginService.java @@ -0,0 +1,157 @@ +/* + * 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.unomi.elasticsearch.plugin.security; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.*; +import org.elasticsearch.transport.TransportConnectionListener; +import org.elasticsearch.transport.TransportService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.*; +import java.util.ArrayList; +import java.util.List; + +/** + * ElasticSearch plugin that simply rejects connection from non-authorized IP ranges + */ +public class SecurityPluginService extends AbstractLifecycleComponent<SecurityPluginService> { + + private static final Logger logger = LoggerFactory.getLogger(SecurityPluginService.class.getName()); + + RestController restController; + TransportService transportService; + RestFilter restFilter; + TransportConnectionListener transportConnectionListener; + String publishHost; + List<IPRangeMatcher> ipRangeMatchers = new ArrayList<IPRangeMatcher>(); + + @Inject + public SecurityPluginService(Settings settings, + RestController restController, + TransportService transportService, + NetworkService networkService) { + super(settings); + this.restController = restController; + this.transportService = transportService; + this.publishHost = componentSettings.get("publish_host", settings.get("transport.publish_host", settings.get("transport.host"))); + InetAddress publishHostAddress = null; + try { + publishHostAddress = networkService.resolvePublishHostAddress(publishHost); + } catch (IOException e) { + logger.error("Error trying to resolve publish host address " + publishHost); + } + + initIPRangeMatcher(settings, publishHostAddress); + } + + protected void initIPRangeMatcher(Settings settings, InetAddress publishHostAddress) { + String hostAddressRange = null; + if (publishHostAddress != null) { + String hostAddress = publishHostAddress.getHostAddress(); + if (publishHostAddress instanceof Inet4Address) { + int lastDotPos = hostAddress.lastIndexOf("."); + if (lastDotPos > -1) { + hostAddressRange = hostAddress.substring(0, lastDotPos) + ".0-" + hostAddress.substring(0, lastDotPos) + ".255"; + } + } else if (publishHostAddress instanceof Inet6Address) { + int lastColonPos = hostAddress.lastIndexOf(":"); + if (lastColonPos > -1) { + hostAddressRange = hostAddress.substring(0, lastColonPos) + ":0-" + hostAddress.substring(0, lastColonPos) + ":ffff"; + } + } + } + String defaultIpRanges = "localhost,127.0.0.1,127.0.1.1,::1"; + if (hostAddressRange != null) { + defaultIpRanges += "," + hostAddressRange; + } + String[] ipRanges = settings.get("security.ipranges", defaultIpRanges).split(","); + for (String ipRange : ipRanges) { + try { + IPRangeMatcher iprangeMatcher = new IPRangeMatcher(ipRange.trim()); + ipRangeMatchers.add(iprangeMatcher); + } catch (UnknownHostException e) { + logger.error("Error in specified IP range " + ipRange, e); + } + } + } + + @Override + protected void doStart() throws ElasticsearchException { + restFilter = new RestFilter() { + @Override + public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) throws Exception { + logger.info("Processing REST request=" + request + " channel=" + channel); + if (request.getRemoteAddress() instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) request.getRemoteAddress(); + if (!isIPAllowed(inetSocketAddress.getHostName())) { + logger.warn("Rejecting request from unauthorized IP " + request.getRemoteAddress()); + return; + } + } else { + logger.warn("Unexpected SocketAddress that is not an InetSocketAddress (but an instance of " + request.getRemoteAddress().getClass().getName() + "), IP range filtering is DISABLED !"); + } + filterChain.continueProcessing(request, channel); + } + }; + restController.registerFilter(restFilter); + transportConnectionListener = new TransportConnectionListener() { + public void onNodeConnected(DiscoveryNode node) { + logger.info("Node connected " + node); + if (!isIPAllowed(node.getHostAddress())) { + logger.warn("Rejecting connection from unauthorized IP " + node.getHostAddress()); + transportService.disconnectFromNode(node); + } + } + + public void onNodeDisconnected(DiscoveryNode node) { + } + }; + transportService.addConnectionListener(transportConnectionListener); + } + + @Override + protected void doStop() throws ElasticsearchException { + transportService.removeConnectionListener(transportConnectionListener); + } + + @Override + protected void doClose() throws ElasticsearchException { + + } + + public boolean isIPAllowed(String ipAddress) { + for (IPRangeMatcher ipRangeMatcher : ipRangeMatchers) { + try { + if (ipRangeMatcher.isInRange(ipAddress)) { + return true; + } + } catch (UnknownHostException e) { + logger.error("Error checking IP range for " + ipAddress + " connection will NOT be allowed", e); + } + } + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/IPRangeMatcher.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/IPRangeMatcher.java b/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/IPRangeMatcher.java deleted file mode 100644 index 114c86a..0000000 --- a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/IPRangeMatcher.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2013 Edin Dazdarevic ([email protected]) - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * - */ - -package org.elasticsearch.contextserver; - -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * A class that enables to get an IP range from CIDR specification. It supports - * both IPv4 and IPv6. - * <p/> - * This class was adapted from the CIDRUtils code at https://github.com/edazdarevic/CIDRUtils - * and support for IP ranges was added. - */ -public class IPRangeMatcher { - - private InetAddress inetAddress; - private InetAddress startAddress; - private InetAddress endAddress; - private int prefixLength; - - public IPRangeMatcher(String cidrOrIPRange) throws UnknownHostException { - - if (cidrOrIPRange.contains("/")) { - /* split CIDR to address and prefix part */ - int index = cidrOrIPRange.indexOf("/"); - String addressPart = cidrOrIPRange.substring(0, index); - String networkPart = cidrOrIPRange.substring(index + 1); - - inetAddress = InetAddress.getByName(addressPart); - prefixLength = Integer.parseInt(networkPart); - - calculate(); - } else if (cidrOrIPRange.contains("-")) { - String[] rangeParts = cidrOrIPRange.split("-"); - this.startAddress = InetAddress.getByName(rangeParts[0].trim()); - this.endAddress = InetAddress.getByName(rangeParts[1].trim()); - } else { - // we don't handle a range, we just match a single IP address - this.startAddress = InetAddress.getByName(cidrOrIPRange); - this.endAddress = this.startAddress; - } - } - - public IPRangeMatcher(InetAddress startAddress, InetAddress endAddress) { - this.startAddress = startAddress; - this.endAddress = endAddress; - } - - public IPRangeMatcher(String startAddress, String endAddress) throws UnknownHostException { - this.startAddress = InetAddress.getByName(startAddress); - this.endAddress = InetAddress.getByName(endAddress); - } - - - private void calculate() throws UnknownHostException { - - ByteBuffer maskBuffer; - int targetSize; - if (inetAddress.getAddress().length == 4) { - maskBuffer = - ByteBuffer - .allocate(4) - .putInt(-1); - targetSize = 4; - } else { - maskBuffer = ByteBuffer.allocate(16) - .putLong(-1L) - .putLong(-1L); - targetSize = 16; - } - - BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength); - - ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress()); - BigInteger ipVal = new BigInteger(1, buffer.array()); - - BigInteger startIp = ipVal.and(mask); - BigInteger endIp = startIp.add(mask.not()); - - byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize); - byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize); - - this.startAddress = InetAddress.getByAddress(startIpArr); - this.endAddress = InetAddress.getByAddress(endIpArr); - - } - - private byte[] toBytes(byte[] array, int targetSize) { - int counter = 0; - List<Byte> newArr = new ArrayList<Byte>(); - while (counter < targetSize && (array.length - 1 - counter >= 0)) { - newArr.add(0, array[array.length - 1 - counter]); - counter++; - } - - int size = newArr.size(); - for (int i = 0; i < (targetSize - size); i++) { - - newArr.add(0, (byte) 0); - } - - byte[] ret = new byte[newArr.size()]; - for (int i = 0; i < newArr.size(); i++) { - ret[i] = newArr.get(i); - } - return ret; - } - - public String getNetworkAddress() { - - return this.startAddress.getHostAddress(); - } - - public String getBroadcastAddress() { - return this.endAddress.getHostAddress(); - } - - public boolean isInRange(String ipAddress) throws UnknownHostException { - InetAddress address = InetAddress.getByName(ipAddress); - BigInteger start = new BigInteger(1, this.startAddress.getAddress()); - BigInteger end = new BigInteger(1, this.endAddress.getAddress()); - BigInteger target = new BigInteger(1, address.getAddress()); - - int st = start.compareTo(target); - int te = target.compareTo(end); - - return (st == -1 || st == 0) && (te == -1 || te == 0); - } -} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPlugin.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPlugin.java b/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPlugin.java deleted file mode 100644 index 6c62ea7..0000000 --- a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPlugin.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.elasticsearch.contextserver; - -import org.elasticsearch.common.component.LifecycleComponent; -import org.elasticsearch.common.inject.Module; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.plugins.AbstractPlugin; - -import java.util.Collection; - -import static org.elasticsearch.common.collect.Lists.newArrayList; - -public class SecurityPlugin extends AbstractPlugin { - - public SecurityPlugin(Settings settings) { - super(); - } - - public String name() { - return "contextserver-security"; - } - - public String description() { - return "A plugin that provides some basic security to the Context Server elasticsearch HTTP and Transport connectors"; - } - - @Override - public Collection<Class<? extends Module>> modules() { - Collection<Class<? extends Module>> modules = newArrayList(); - // if (settings.getAsBoolean("security.enabled", true)) { - modules.add(SecurityPluginModule.class); - // } - return modules; - } - - @SuppressWarnings("rawtypes") - @Override - public Collection<Class<? extends LifecycleComponent>> services() { - Collection<Class<? extends LifecycleComponent>> services = newArrayList(); - // if (settings.getAsBoolean("security.enabled", true)) { - services.add(SecurityPluginService.class); - // } - return services; - } -} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginModule.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginModule.java b/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginModule.java deleted file mode 100644 index 04d81ca..0000000 --- a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginModule.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.elasticsearch.contextserver; - -import org.elasticsearch.common.inject.AbstractModule; -import org.elasticsearch.common.settings.Settings; - -public class SecurityPluginModule extends AbstractModule { - - public SecurityPluginModule(Settings settings) { - super(); - } - - @Override - protected void configure() { - bind(SecurityPluginService.class).asEagerSingleton(); - } -} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginService.java ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginService.java b/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginService.java deleted file mode 100644 index d3f306f..0000000 --- a/persistence-elasticsearch/plugins/security/src/main/java/org/elasticsearch/contextserver/SecurityPluginService.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.elasticsearch.contextserver; - -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.component.AbstractLifecycleComponent; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.network.NetworkService; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.rest.*; -import org.elasticsearch.transport.TransportConnectionListener; -import org.elasticsearch.transport.TransportService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.*; -import java.util.ArrayList; -import java.util.List; - -/** - * ElasticSearch plugin that simply rejects connection from non-authorized IP ranges - */ -public class SecurityPluginService extends AbstractLifecycleComponent<SecurityPluginService> { - - private static final Logger logger = LoggerFactory.getLogger(SecurityPluginService.class.getName()); - - RestController restController; - TransportService transportService; - RestFilter restFilter; - TransportConnectionListener transportConnectionListener; - String publishHost; - List<IPRangeMatcher> ipRangeMatchers = new ArrayList<IPRangeMatcher>(); - - @Inject - public SecurityPluginService(Settings settings, - RestController restController, - TransportService transportService, - NetworkService networkService) { - super(settings); - this.restController = restController; - this.transportService = transportService; - this.publishHost = componentSettings.get("publish_host", settings.get("transport.publish_host", settings.get("transport.host"))); - InetAddress publishHostAddress = null; - try { - publishHostAddress = networkService.resolvePublishHostAddress(publishHost); - } catch (IOException e) { - logger.error("Error trying to resolve publish host address " + publishHost); - } - - initIPRangeMatcher(settings, publishHostAddress); - } - - protected void initIPRangeMatcher(Settings settings, InetAddress publishHostAddress) { - String hostAddressRange = null; - if (publishHostAddress != null) { - String hostAddress = publishHostAddress.getHostAddress(); - if (publishHostAddress instanceof Inet4Address) { - int lastDotPos = hostAddress.lastIndexOf("."); - if (lastDotPos > -1) { - hostAddressRange = hostAddress.substring(0, lastDotPos) + ".0-" + hostAddress.substring(0, lastDotPos) + ".255"; - } - } else if (publishHostAddress instanceof Inet6Address) { - int lastColonPos = hostAddress.lastIndexOf(":"); - if (lastColonPos > -1) { - hostAddressRange = hostAddress.substring(0, lastColonPos) + ":0-" + hostAddress.substring(0, lastColonPos) + ":ffff"; - } - } - } - String defaultIpRanges = "localhost,127.0.0.1,127.0.1.1,::1"; - if (hostAddressRange != null) { - defaultIpRanges += "," + hostAddressRange; - } - String[] ipRanges = settings.get("security.ipranges", defaultIpRanges).split(","); - for (String ipRange : ipRanges) { - try { - IPRangeMatcher iprangeMatcher = new IPRangeMatcher(ipRange.trim()); - ipRangeMatchers.add(iprangeMatcher); - } catch (UnknownHostException e) { - logger.error("Error in specified IP range " + ipRange, e); - } - } - } - - @Override - protected void doStart() throws ElasticsearchException { - restFilter = new RestFilter() { - @Override - public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) throws Exception { - logger.info("Processing REST request=" + request + " channel=" + channel); - if (request.getRemoteAddress() instanceof InetSocketAddress) { - InetSocketAddress inetSocketAddress = (InetSocketAddress) request.getRemoteAddress(); - if (!isIPAllowed(inetSocketAddress.getHostName())) { - logger.warn("Rejecting request from unauthorized IP " + request.getRemoteAddress()); - return; - } - } else { - logger.warn("Unexpected SocketAddress that is not an InetSocketAddress (but an instance of " + request.getRemoteAddress().getClass().getName() + "), IP range filtering is DISABLED !"); - } - filterChain.continueProcessing(request, channel); - } - }; - restController.registerFilter(restFilter); - transportConnectionListener = new TransportConnectionListener() { - public void onNodeConnected(DiscoveryNode node) { - logger.info("Node connected " + node); - if (!isIPAllowed(node.getHostAddress())) { - logger.warn("Rejecting connection from unauthorized IP " + node.getHostAddress()); - transportService.disconnectFromNode(node); - } - } - - public void onNodeDisconnected(DiscoveryNode node) { - } - }; - transportService.addConnectionListener(transportConnectionListener); - } - - @Override - protected void doStop() throws ElasticsearchException { - transportService.removeConnectionListener(transportConnectionListener); - } - - @Override - protected void doClose() throws ElasticsearchException { - - } - - public boolean isIPAllowed(String ipAddress) { - for (IPRangeMatcher ipRangeMatcher : ipRangeMatchers) { - try { - if (ipRangeMatcher.isInRange(ipAddress)) { - return true; - } - } catch (UnknownHostException e) { - logger.error("Error checking IP range for " + ipAddress + " connection will NOT be allowed", e); - } - } - return false; - } -} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-elasticsearch/plugins/security/src/main/resources/es-plugin.properties ---------------------------------------------------------------------- diff --git a/persistence-elasticsearch/plugins/security/src/main/resources/es-plugin.properties b/persistence-elasticsearch/plugins/security/src/main/resources/es-plugin.properties index d24783e..0ac5643 100644 --- a/persistence-elasticsearch/plugins/security/src/main/resources/es-plugin.properties +++ b/persistence-elasticsearch/plugins/security/src/main/resources/es-plugin.properties @@ -14,5 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -plugin=org.elasticsearch.contextserver.SecurityPlugin +plugin=org.apache.unomi.elasticsearch.plugin.security.SecurityPlugin version=${project.version} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java ---------------------------------------------------------------------- diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java new file mode 100644 index 0000000..f17f8b2 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java @@ -0,0 +1,92 @@ +/* + * 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.unomi.persistence.spi; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.util.ISO8601DateFormat; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; +import org.apache.unomi.api.*; +import org.apache.unomi.api.campaigns.Campaign; +import org.apache.unomi.api.campaigns.events.CampaignEvent; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.goals.Goal; +import org.apache.unomi.api.rules.Rule; +import org.apache.unomi.api.segments.Scoring; +import org.apache.unomi.api.segments.Segment; + +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +/** + * Custom object mapper to be able to configure Jackson to our needs. + */ +public class CustomObjectMapper extends ObjectMapper { + + private static final long serialVersionUID = 4578277612897061535L; + + public CustomObjectMapper() { + super(); + super.registerModule(new JaxbAnnotationModule()); + configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + ISO8601DateFormat dateFormat = new ISO8601DateFormat(); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + setDateFormat(dateFormat); + SimpleModule deserializerModule = + new SimpleModule("PropertyTypedObjectDeserializerModule", + new Version(1, 0, 0, null, "org.apache.unomi.rest", "deserializer")); + + PropertyTypedObjectDeserializer propertyTypedObjectDeserializer = new PropertyTypedObjectDeserializer(); + propertyTypedObjectDeserializer.registerMapping("type=.*Condition", Condition.class); + deserializerModule.addDeserializer(Object.class, propertyTypedObjectDeserializer); + + ItemDeserializer itemDeserializer = new ItemDeserializer(); + deserializerModule.addDeserializer(Item.class, itemDeserializer); + + + Map<String,Class<? extends Item>> classes = new HashMap<>(); + classes.put(Campaign.ITEM_TYPE,Campaign.class); + classes.put(CampaignEvent.ITEM_TYPE,CampaignEvent.class); + classes.put(Event.ITEM_TYPE,Event.class); + classes.put(Goal.ITEM_TYPE,Goal.class); + classes.put(Persona.ITEM_TYPE,Persona.class); + classes.put(Rule.ITEM_TYPE,Rule.class); + classes.put(Scoring.ITEM_TYPE,Scoring.class); + classes.put(Segment.ITEM_TYPE,Segment.class); + classes.put(Session.ITEM_TYPE, Session.class); + for (Map.Entry<String, Class<? extends Item>> entry : classes.entrySet()) { + propertyTypedObjectDeserializer.registerMapping("itemType="+entry.getKey(), entry.getValue()); + itemDeserializer.registerMapping(entry.getKey(), entry.getValue()); + } + propertyTypedObjectDeserializer.registerMapping("itemType=.*", CustomItem.class); + + + super.registerModule(deserializerModule); + } + + public static ObjectMapper getObjectMapper() { + return Holder.INSTANCE; + } + + private static class Holder { + static final CustomObjectMapper INSTANCE = new CustomObjectMapper(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/ItemDeserializer.java ---------------------------------------------------------------------- diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/ItemDeserializer.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/ItemDeserializer.java new file mode 100644 index 0000000..9080907 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/ItemDeserializer.java @@ -0,0 +1,60 @@ +/* + * 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.unomi.persistence.spi; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.unomi.api.CustomItem; +import org.apache.unomi.api.Item; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ItemDeserializer extends StdDeserializer<Item> { + + private static final long serialVersionUID = -7040054009670771266L; + private Map<String,Class<? extends Item>> classes = new HashMap<>(); + + public ItemDeserializer() { + super(Item.class); + } + + public void registerMapping(String type, Class<? extends Item> clazz) { + classes.put(type, clazz); + } + + @Override + public Item deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectCodec codec = jp.getCodec(); + ObjectNode treeNode = codec.readTree(jp); + String type = treeNode.get("itemType").textValue(); + Class<? extends Item> objectClass = classes.get(type); + if (objectClass == null) { + objectClass = CustomItem.class; + } else { + treeNode.remove("itemType"); + } + Item item = codec.treeToValue(treeNode, objectClass); + item.setItemId(treeNode.get("itemId").asText()); + return item; + } +} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java ---------------------------------------------------------------------- diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java new file mode 100644 index 0000000..4a44c63 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java @@ -0,0 +1,399 @@ +/* + * 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.unomi.persistence.spi; + +import org.apache.unomi.api.Item; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.persistence.spi.aggregate.BaseAggregate; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * A service to provide persistence and retrieval of context server entities. + */ +public interface PersistenceService { + + /** + * Retrieves all known items of the specified class. + * <em>WARNING</em>: this method can be quite computationally intensive and calling the paged version {@link #getAllItems(Class, int, int, String)} is preferred. + * + * @param <T> the type of the {@link Item}s we want to retrieve + * @param clazz the {@link Item} subclass of entities we want to retrieve + * @return a list of all known items with the given type + */ + <T extends Item> List<T> getAllItems(Class<T> clazz); + + /** + * Retrieves all known items of the specified class, ordered according to the specified {@code sortBy} String and and paged: only {@code size} of them are retrieved, + * starting with the {@code offset}-th one. + * + * TODO: use a Query object instead of distinct parameters? + * + * @param <T> the type of the {@link Item}s we want to retrieve + * @param clazz the {@link Item} subclass of entities we want to retrieve + * @param offset zero or a positive integer specifying the position of the first item in the total ordered collection of matching items + * @param size a positive integer specifying how many matching items should be retrieved or {@code -1} if all of them should be retrieved + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @return a {@link PartialList} of pages items with the given type + */ + <T extends Item> PartialList<T> getAllItems(Class<T> clazz, int offset, int size, String sortBy); + + /** + * Persists the specified Item in the context server. + * + * @param item the item to persist + * @return {@code true} if the item was properly persisted, {@code false} otherwise + */ + boolean save(Item item); + + /** + * Updates the item of the specified class and identified by the specified identifier with new property values provided as name - value pairs in the specified Map. + * + * @param itemId the identifier of the item we want to update + * @param dateHint a Date helping in identifying where the item is located + * @param clazz the Item subclass of the item to update + * @param source a Map with entries specifying as key the property name to update and as value its new value + * @return {@code true} if the update was successful, {@code false} otherwise + */ + boolean update(String itemId, Date dateHint, Class<?> clazz, Map<?, ?> source); + + /** + * Updates the item of the specified class and identified by the specified identifier with a new property value for the specified property name. Same as + * {@code update(itemId, dateHint, clazz, Collections.singletonMap(propertyName, propertyValue))} + * + * @param itemId the identifier of the item we want to update + * @param dateHint a Date helping in identifying where the item is located + * @param clazz the Item subclass of the item to update + * @param propertyName the name of the property to update + * @param propertyValue the new value of the property + * @return {@code true} if the update was successful, {@code false} otherwise + */ + boolean update(String itemId, Date dateHint, Class<?> clazz, String propertyName, Object propertyValue); + + /** + * Updates the item of the specified class and identified by the specified identifier with a new property value for the specified property name. Same as + * {@code update(itemId, dateHint, clazz, Collections.singletonMap(propertyName, propertyValue))} + * + * @param itemId the identifier of the item we want to update + * @param dateHint a Date helping in identifying where the item is located + * @param clazz the Item subclass of the item to update + * @param script inline script + * @param scriptParams script params + * @return {@code true} if the update was successful, {@code false} otherwise + */ + boolean update(String itemId, Date dateHint, Class<?> clazz, String script, Map<String, Object> scriptParams); + + /** + * Retrieves the item identified with the specified identifier and with the specified Item subclass if it exists. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param itemId the identifier of the item we want to retrieve + * @param clazz the {@link Item} subclass of the item we want to retrieve + * @return the item identified with the specified identifier and with the specified Item subclass if it exists, {@code null} otherwise + */ + <T extends Item> T load(String itemId, Class<T> clazz); + + /** + * Retrieves the item identified with the specified identifier and with the specified Item subclass if it exists. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param itemId the identifier of the item we want to retrieve + * @param dateHint a Date helping in identifying where the item is located + * @param clazz the {@link Item} subclass of the item we want to retrieve + * @return the item identified with the specified identifier and with the specified Item subclass if it exists, {@code null} otherwise + */ + <T extends Item> T load(String itemId, Date dateHint, Class<T> clazz); + + /** + * Deletes the item identified with the specified identifier and with the specified Item subclass if it exists. + * + * @param <T> the type of the Item subclass we want to delete + * @param itemId the identifier of the item we want to delete + * @param clazz the {@link Item} subclass of the item we want to delete + * @return {@code true} if the deletion was successful, {@code false} otherwise + */ + <T extends Item> boolean remove(String itemId, Class<T> clazz); + + /** + * Deletes items with the specified Item subclass matching the specified {@link Condition}. + * + * @param <T> the type of the Item subclass we want to delete + * @param query a {@link Condition} identifying which elements we want to delete + * @param clazz the {@link Item} subclass of the items we want to delete + * @return {@code true} if the deletion was successful, {@code false} otherwise + */ + <T extends Item> boolean removeByQuery(Condition query, Class<T> clazz); + + /** + * Persists the specified query under the specified name. + * + * @param queryName the name under which the specified query should be recorded + * @param query the query to be recorded + * @return {@code true} if the query was properly saved, {@code false} otherwise + */ + boolean saveQuery(String queryName, Condition query); + + /** + * Deletes the query identified by the specified name. + * + * @param queryName the name under which the specified query was recorded + * @return {@code true} if the deletion was successful, {@code false} otherwise + */ + boolean removeQuery(String queryName); + + /** + * TODO + * + * @param itemType + * @return + */ + Map<String, Map<String, Object>> getMapping(String itemType); + + /** + * TODO + * + * @param item + * @return + */ + List<String> getMatchingSavedQueries(Item item); + + /** + * Checks whether the specified item satisfies the provided condition. + * + * TODO: rename to isMatching? + * + * @param query the condition we're testing the specified item against + * @param item the item we're checking against the specified condition + * @return {@code true} if the item satisfies the condition, {@code false} otherwise + */ + boolean testMatch(Condition query, Item item); + + /** + * Same as {@code query(fieldName, fieldValue, sortBy, clazz, 0, -1).getList()} + * + * @see #query(Condition, String, Class, int, int) + */ + <T extends Item> List<T> query(String fieldName, String fieldValue, String sortBy, Class<T> clazz); + + /** + * Retrieves a list of items with the specified field having the specified values. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param fieldName the name of the field which we want items to have the specified values + * @param fieldValues the values the items to retrieve should have for the specified field + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @return a list of items matching the specified criteria + */ + <T extends Item> List<T> query(String fieldName, String[] fieldValues, String sortBy, Class<T> clazz); + + /** + * Retrieves a list of items with the specified field having the specified value. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param fieldName the name of the field which we want items to have the specified value + * @param fieldValue the value the items to retrieve should have for the specified field + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @return a {@link PartialList} of items matching the specified criteria + */ + <T extends Item> PartialList<T> query(String fieldName, String fieldValue, String sortBy, Class<T> clazz, int offset, int size); + + /** + * Retrieves a list of items with the specified field having the specified value and having at least a field with the specified full text value in it, ordered according to the + * specified {@code sortBy} String and and paged: only {@code size} of them are retrieved, starting with the {@code offset}-th one. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param fieldName the name of the field which we want items to have the specified value + * @param fieldValue the value the items to retrieve should have for the specified field + * @param fulltext the text that the item must have in one of its fields to be considered a match + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @param offset zero or a positive integer specifying the position of the first item in the total ordered collection of matching items + * @param size a positive integer specifying how many matching items should be retrieved or {@code -1} if all of them should be retrieved + * @return a {@link PartialList} of items matching the specified criteria + */ + <T extends Item> PartialList<T> queryFullText(String fieldName, String fieldValue, String fulltext, String sortBy, Class<T> clazz, int offset, int size); + + /** + * Retrieves a list of items having at least a field with the specified full text value in it, ordered according to the specified {@code sortBy} String and and paged: only + * {@code size} of them are retrieved, starting with the {@code offset}-th one. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param fulltext the text that the item must have in one of its fields to be considered a match + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @param offset zero or a positive integer specifying the position of the first item in the total ordered collection of matching items + * @param size a positive integer specifying how many matching items should be retrieved or {@code -1} if all of them should be retrieved + * @return a {@link PartialList} of items matching the specified criteria + */ + <T extends Item> PartialList<T> queryFullText(String fulltext, String sortBy, Class<T> clazz, int offset, int size); + + /** + * Same as {@code query(query, sortBy, clazz, 0, -1).getList()} + * + * @see #query(Condition, String, Class, int, int) + */ + <T extends Item> List<T> query(Condition query, String sortBy, Class<T> clazz); + + /** + * Retrieves a list of items satisfying the specified {@link Condition}, ordered according to the specified {@code sortBy} String and and paged: only {@code size} of them + * are retrieved, starting with the {@code offset}-th one. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param query the {@link Condition} the items must satisfy to be retrieved + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @param offset zero or a positive integer specifying the position of the first item in the total ordered collection of matching items + * @param size a positive integer specifying how many matching items should be retrieved or {@code -1} if all of them should be retrieved + * @return a {@link PartialList} of items matching the specified criteria + */ + <T extends Item> PartialList<T> query(Condition query, String sortBy, Class<T> clazz, int offset, int size); + + /** + * Retrieves the same items as {@code query(query, sortBy, clazz, 0, -1)} with the added constraints that the matching elements must also have at least a field matching the + * specified full text query. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param fulltext the text that the item must have in one of its fields to be considered a match + * @param query the {@link Condition} the items must satisfy to be retrieved + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the + * String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by + * a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @param offset zero or a positive integer specifying the position of the first item in the total ordered collection of matching items + * @param size a positive integer specifying how many matching items should be retrieved or {@code -1} if all of them should be retrieved + * @return a {@link PartialList} of items matching the specified criteria + */ + <T extends Item> PartialList<T> queryFullText(String fulltext, Condition query, String sortBy, Class<T> clazz, int offset, int size); + + /** + * Retrieves the number of items of the specified type as defined by the Item subclass public field {@code ITEM_TYPE} and matching the specified {@link Condition}. + * + * @param query the condition the items must satisfy + * @param itemType the String representation of the item type we want to retrieve the count of, as defined by its class' {@code ITEM_TYPE} field + * @return the number of items of the specified type + * @see Item Item for a discussion of {@code ITEM_TYPE} + */ + long queryCount(Condition query, String itemType); + + /** + * Retrieves the number of items with the specified type as defined by the Item subclass public field {@code ITEM_TYPE}. + * + * @param itemType the String representation of the item type we want to retrieve the count of, as defined by its class' {@code ITEM_TYPE} field + * @return the number of items of the specified type + * @see Item Item for a discussion of {@code ITEM_TYPE} + */ + long getAllItemsCount(String itemType); + + /** + * Retrieves the number of items with the specified type as defined by the Item subclass public field {@code ITEM_TYPE} matching the optional specified condition and + * aggregated according to the specified {@link BaseAggregate}. + * + * @param filter the condition the items must match or {@code null} if no filtering is needed + * @param aggregate an aggregate specifying how matching items must be bundled + * @param itemType the String representation of the item type we want to retrieve the count of, as defined by its class' {@code ITEM_TYPE} field + * @return a Map associating aggregation dimension name as key and cardinality for that dimension as value + */ + Map<String, Long> aggregateQuery(Condition filter, BaseAggregate aggregate, String itemType); + + /** + * Updates the persistence's engine indices if needed. + */ + void refresh(); + + /** + * Purges all data in the context server up to the specified date, not included. + * + * @param date the date (not included) before which we want to erase all data + */ + void purge(Date date); + + /** + * Retrieves all items of the specified Item subclass which specified ranged property is within the specified bounds, ordered according to the specified {@code sortBy} String + * and and paged: only {@code size} of them are retrieved, starting with the {@code offset}-th one. + * + * @param <T> the type of the Item subclass we want to retrieve + * @param s the name of the range property we want items to retrieve to be included between the specified start and end points + * @param from the beginning of the range we want to consider + * @param to the end of the range we want to consider + * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering + * elements according to the property order in the String, considering each in turn and moving on to the next one in case of equality of all preceding ones. + * Each property name is optionally followed by a column ({@code :}) and an order specifier: {@code asc} or {@code desc}. + * @param clazz the {@link Item} subclass of the items we want to retrieve + * @param offset zero or a positive integer specifying the position of the first item in the total ordered collection of matching items + * @param size a positive integer specifying how many matching items should be retrieved or {@code -1} if all of them should be retrieved + * @return a {@link PartialList} of items matching the specified criteria + */ + <T extends Item> PartialList<T> rangeQuery(String s, String from, String to, String sortBy, Class<T> clazz, int offset, int size); + + /** + * Retrieves the specified metrics for the specified field of items of the specified type as defined by the Item subclass public field {@code ITEM_TYPE} and matching the + * specified {@link Condition}. + * + * @param condition the condition the items must satisfy + * @param metrics a String array which metrics should be computed (possible values: {@code sum} for the sum of the values, {@code avg} for the average of the values, {@code + * min} for the minimum value and {@code max} for the maximum value) + * @param field the name of the field for which the metrics should be computed + * @param type the String representation of the item type we want to retrieve the count of, as defined by its class' {@code ITEM_TYPE} field + * @return a Map associating computed metric name as key to its associated value + */ + Map<String, Double> getSingleValuesMetrics(Condition condition, String[] metrics, String field, String type); + + /** + * Creates an index with the specified name in the persistence engine. + * + * TODO: remove from API? + * + * @param indexName the index name + * @return {@code true} if the operation was successful, {@code false} otherwise + */ + boolean createIndex(final String indexName); + + /** + * Removes the index with the specified name. + * + * TODO: remove from API? + * + * @param indexName the index name + * @return {@code true} if the operation was successful, {@code false} otherwise + */ + boolean removeIndex(final String indexName); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyHelper.java ---------------------------------------------------------------------- diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyHelper.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyHelper.java new file mode 100644 index 0000000..5b24a62 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyHelper.java @@ -0,0 +1,91 @@ +/* + * 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.unomi.persistence.spi; + +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.beanutils.PropertyUtils; +import org.apache.commons.beanutils.expression.DefaultResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * Helper method for properties + */ +public class PropertyHelper { + + private static final Logger logger = LoggerFactory.getLogger(PropertyHelper.class.getName()); + private static DefaultResolver resolver = new DefaultResolver(); + + public static boolean setProperty(Object target, String propertyName, Object propertyValue, String setPropertyStrategy) { + try { + while (resolver.hasNested(propertyName)) { + Object v = PropertyUtils.getProperty(target, resolver.next(propertyName)); + if (v == null) { + v = new LinkedHashMap<>(); + PropertyUtils.setProperty(target, resolver.next(propertyName), v); + } + propertyName = resolver.remove(propertyName); + target = v; + } + + if (setPropertyStrategy != null && setPropertyStrategy.equals("addValue")) { + Object previousValue = PropertyUtils.getProperty(target, propertyName); + List<Object> values = new ArrayList<>(); + if (previousValue != null && previousValue instanceof List) { + values.addAll((List) previousValue); + } else if (previousValue != null) { + values.add(previousValue); + } + if (!values.contains(propertyValue)) { + values.add(propertyValue); + BeanUtils.setProperty(target, propertyName, values); + return true; + } + } else if (propertyValue != null && !propertyValue.equals(BeanUtils.getProperty(target, propertyName))) { + if (setPropertyStrategy == null || + setPropertyStrategy.equals("alwaysSet") || + (setPropertyStrategy.equals("setIfMissing") && BeanUtils.getProperty(target, propertyName) == null)) { + BeanUtils.setProperty(target, propertyName, propertyValue); + return true; + } + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + logger.error("Cannot set property", e); + } + return false; + } + + public static Integer getInteger(Object value) { + if (value instanceof Number) { + return ((Number)value).intValue(); + } else { + try { + return Integer.parseInt(value.toString()); + } catch (NumberFormatException e) { + // Not a number + } + } + return null; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyTypedObjectDeserializer.java ---------------------------------------------------------------------- diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyTypedObjectDeserializer.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyTypedObjectDeserializer.java new file mode 100644 index 0000000..f9d2613 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PropertyTypedObjectDeserializer.java @@ -0,0 +1,114 @@ +/* + * 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.unomi.persistence.spi; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonTokenId; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.util.*; + +/** + * This Jackson deserializer makes it possible to register field matching + * regular expressions that can be matched to class names, as in the following + * example: + * + * SimpleModule deserializerModule = + * new SimpleModule("PropertyTypedObjectDeserializerModule", + * new Version(1, 0, 0, null, "org.apache.unomi.rest", "deserializer")); + * PropertyTypedObjectDeserializer propertyTypedObjectDeserializer = new PropertyTypedObjectDeserializer(); + * propertyTypedObjectDeserializer.registerMapping("type=.*Condition", Condition.class); + * deserializerModule.addDeserializer(Object.class, propertyTypedObjectDeserializer); + * objectMapper.registerModule(deserializerModule); + * + * In this example any JSON object that has a "type" property that matches the + * ".*Condition" regular expression will be parsed and mapped to a Condition class + * + * Note that there exists a way to map properties as type identifiers in Jackson, + * but this feature is very limited and requires hardcoding possible values. + * This deserializer is much more flexible and powerful. + */ +public class PropertyTypedObjectDeserializer extends UntypedObjectDeserializer { + + private static final long serialVersionUID = -2561171359946902967L; + + private Map<String, Class<? extends Object>> registry = + new LinkedHashMap<String, Class<? extends Object>>(); + + private Map<String,Set<String>> fieldValuesToMatch = new LinkedHashMap<String,Set<String>>(); + + public void registerMapping(String matchExpression, + Class<? extends Object> mappedClass) { + registry.put(matchExpression, mappedClass); + String[] fieldParts = matchExpression.split("="); + Set<String> valuesToMatch = fieldValuesToMatch.get(fieldParts[0]); + if (valuesToMatch == null) { + valuesToMatch = new LinkedHashSet<String>(); + } + valuesToMatch.add(fieldParts[1]); + fieldValuesToMatch.put(fieldParts[0], valuesToMatch); + } + + @Override + public Object deserialize( + JsonParser jp, DeserializationContext ctxt) + throws IOException { + if (jp.getCurrentTokenId() != JsonTokenId.ID_START_OBJECT) { + return super.deserialize(jp, ctxt); + } + ObjectCodec codec = jp.getCodec(); + TreeNode treeNode = codec.readTree(jp); + Class<? extends Object> objectClass = null; + if (treeNode instanceof ObjectNode) { + ObjectNode root = (ObjectNode) treeNode; + Iterator<Map.Entry<String, JsonNode>> elementsIterator = + root.fields(); + while (elementsIterator.hasNext()) { + Map.Entry<String, JsonNode> element = elementsIterator.next(); + String name = element.getKey(); + if (fieldValuesToMatch.containsKey(name)) { + Set<String> valuesToMatch = fieldValuesToMatch.get(name); + for (String valueToMatch : valuesToMatch) { + if (element.getValue().asText().matches(valueToMatch)) { + objectClass = registry.get(name + "=" + valueToMatch); + break; + } + } + if (objectClass != null) { + break; + } + } + } + if (objectClass == null) { + objectClass = HashMap.class; + } + } else { + + } + if (objectClass == null) { + return super.deserialize(codec.treeAsTokens(treeNode), ctxt); + } + return codec.treeToValue(treeNode, objectClass); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/dc1d1520/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/aggregate/BaseAggregate.java ---------------------------------------------------------------------- diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/aggregate/BaseAggregate.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/aggregate/BaseAggregate.java new file mode 100644 index 0000000..4bd26a2 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/aggregate/BaseAggregate.java @@ -0,0 +1,30 @@ +/* + * 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.unomi.persistence.spi.aggregate; + +public abstract class BaseAggregate { + private String field; + + public BaseAggregate(String field) { + this.field = field; + } + + public String getField() { + return field; + } +}
