Java tutorial
/* * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. 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.wso2.carbon.device.mgt.iot.output.adapter.ui; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.databridge.commons.Attribute; import org.wso2.carbon.databridge.commons.Event; import org.wso2.carbon.databridge.commons.StreamDefinition; import org.wso2.carbon.device.mgt.iot.output.adapter.ui.internal.UIEventAdaptorServiceDataHolder; import org.wso2.carbon.device.mgt.iot.output.adapter.ui.util.UIEventAdapterConstants; import org.wso2.carbon.event.output.adapter.core.EventAdapterUtil; import org.wso2.carbon.event.output.adapter.core.OutputEventAdapter; import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterConfiguration; import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterException; import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterRuntimeException; import org.wso2.carbon.event.output.adapter.core.exception.TestConnectionNotSupportedException; import org.wso2.carbon.device.mgt.iot.output.adapter.ui.util.WebSocketSessionRequest; import org.wso2.carbon.event.stream.core.EventStreamService; import org.wso2.carbon.event.stream.core.exception.EventStreamConfigurationException; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Contains the life cycle of executions regarding the UI Adapter */ public class UIEventAdapter implements OutputEventAdapter { private static final Log log = LogFactory.getLog(UIEventAdapter.class); private OutputEventAdapterConfiguration eventAdapterConfiguration; private Map<String, String> globalProperties; private int queueSize; private LinkedBlockingDeque<Object> streamSpecificEvents; private static ThreadPoolExecutor executorService; private int tenantId; private boolean doLogDroppedMessage; private String streamId; private List<Attribute> streamMetaAttributes; private List<Attribute> streamCorrelationAttributes; private List<Attribute> streamPayloadAttributes; public UIEventAdapter(OutputEventAdapterConfiguration eventAdapterConfiguration, Map<String, String> globalProperties) { this.eventAdapterConfiguration = eventAdapterConfiguration; this.globalProperties = globalProperties; this.doLogDroppedMessage = true; } @Override public void init() throws OutputEventAdapterException { tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); //ExecutorService will be assigned if it is null if (executorService == null) { int minThread; int maxThread; long defaultKeepAliveTime; int jobQueSize; //If global properties are available those will be assigned else constant values will be assigned if (globalProperties.get(UIEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME) != null) { minThread = Integer .parseInt(globalProperties.get(UIEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME)); } else { minThread = UIEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE; } if (globalProperties.get(UIEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME) != null) { maxThread = Integer .parseInt(globalProperties.get(UIEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME)); } else { maxThread = UIEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE; } if (globalProperties.get(UIEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME) != null) { defaultKeepAliveTime = Integer .parseInt(globalProperties.get(UIEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME)); } else { defaultKeepAliveTime = UIEventAdapterConstants.DEFAULT_KEEP_ALIVE_TIME_IN_MILLIS; } if (globalProperties.get(UIEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME) != null) { jobQueSize = Integer.parseInt( globalProperties.get(UIEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME)); } else { jobQueSize = UIEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE; } executorService = new ThreadPoolExecutor(minThread, maxThread, defaultKeepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(jobQueSize)); } streamId = eventAdapterConfiguration.getOutputStreamIdOfWso2eventMessageFormat(); if (streamId == null || streamId.isEmpty()) { throw new OutputEventAdapterRuntimeException("UI event adapter needs a output stream id"); } // fetch the "streamDefinition" corresponding to the "streamId" and then fetch the different attribute types // of the streamDefinition corresponding to the event's streamId. They are required when validating values in // the events against the streamDef attributes. StreamDefinition streamDefinition = getStreamDefinition(streamId); streamMetaAttributes = streamDefinition.getMetaData(); streamCorrelationAttributes = streamDefinition.getCorrelationData(); streamPayloadAttributes = streamDefinition.getPayloadData(); ConcurrentHashMap<Integer, ConcurrentHashMap<String, String>> tenantSpecifcEventOutputAdapterMap = UIEventAdaptorServiceDataHolder .getTenantSpecificOutputEventStreamAdapterMap(); ConcurrentHashMap<String, String> streamSpecifAdapterMap = tenantSpecifcEventOutputAdapterMap.get(tenantId); if (streamSpecifAdapterMap == null) { streamSpecifAdapterMap = new ConcurrentHashMap<>(); if (null != tenantSpecifcEventOutputAdapterMap.putIfAbsent(tenantId, streamSpecifAdapterMap)) { streamSpecifAdapterMap = tenantSpecifcEventOutputAdapterMap.get(tenantId); } } String adapterName = streamSpecifAdapterMap.get(streamId); if (adapterName != null) { throw new OutputEventAdapterException(("An Output ui event adapter \"" + adapterName + "\" is already" + " exist for stream id \"" + streamId + "\"")); } else { streamSpecifAdapterMap.put(streamId, eventAdapterConfiguration.getName()); ConcurrentHashMap<Integer, ConcurrentHashMap<String, LinkedBlockingDeque<Object>>> tenantSpecificStreamMap = UIEventAdaptorServiceDataHolder .getTenantSpecificStreamEventMap(); ConcurrentHashMap<String, LinkedBlockingDeque<Object>> streamSpecificEventsMap = tenantSpecificStreamMap .get(tenantId); if (streamSpecificEventsMap == null) { streamSpecificEventsMap = new ConcurrentHashMap<>(); if (null != tenantSpecificStreamMap.putIfAbsent(tenantId, streamSpecificEventsMap)) { streamSpecificEventsMap = tenantSpecificStreamMap.get(tenantId); } } streamSpecificEvents = streamSpecificEventsMap.get(streamId); if (streamSpecificEvents == null) { streamSpecificEvents = new LinkedBlockingDeque<>(); if (null != streamSpecificEventsMap.putIfAbsent(streamId, streamSpecificEvents)) { streamSpecificEvents = streamSpecificEventsMap.get(streamId); } } } if (globalProperties.get(UIEventAdapterConstants.ADAPTER_EVENT_QUEUE_SIZE_NAME) != null) { try { queueSize = Integer .parseInt(globalProperties.get(UIEventAdapterConstants.ADAPTER_EVENT_QUEUE_SIZE_NAME)); } catch (NumberFormatException e) { log.error("String does not have the appropriate format for conversion." + e.getMessage()); queueSize = UIEventAdapterConstants.EVENTS_QUEUE_SIZE; } } else { queueSize = UIEventAdapterConstants.EVENTS_QUEUE_SIZE; } } @Override public void testConnect() throws TestConnectionNotSupportedException { throw new TestConnectionNotSupportedException("Test connection is not available"); } @Override public void connect() { //Not needed } @Override public void publish(Object message, Map<String, String> dynamicProperties) { Event event = (Event) message; StringBuilder eventBuilder = new StringBuilder("["); if (streamSpecificEvents.size() == queueSize) { streamSpecificEvents.removeFirst(); } eventBuilder.append(event.getTimeStamp()); if (event.getMetaData() != null) { eventBuilder.append(","); Object[] metaData = event.getMetaData(); for (int i = 0; i < metaData.length; i++) { eventBuilder.append("\""); eventBuilder.append(metaData[i]); eventBuilder.append("\""); if (i != (metaData.length - 1)) { eventBuilder.append(","); } } } if (event.getCorrelationData() != null) { Object[] correlationData = event.getCorrelationData(); eventBuilder.append(","); for (int i = 0; i < correlationData.length; i++) { eventBuilder.append("\""); eventBuilder.append(correlationData[i]); eventBuilder.append("\""); if (i != (correlationData.length - 1)) { eventBuilder.append(","); } } } if (event.getPayloadData() != null) { Object[] payloadData = event.getPayloadData(); eventBuilder.append(","); for (int i = 0; i < payloadData.length; i++) { eventBuilder.append("\""); eventBuilder.append(payloadData[i]); eventBuilder.append("\""); if (i != (payloadData.length - 1)) { eventBuilder.append(","); } } } eventBuilder.append("]"); String eventString = eventBuilder.toString(); Object[] eventValues = new Object[UIEventAdapterConstants.INDEX_TWO]; eventValues[UIEventAdapterConstants.INDEX_ZERO] = eventString; eventValues[UIEventAdapterConstants.INDEX_ONE] = System.currentTimeMillis(); streamSpecificEvents.add(eventValues); // fetch all valid sessions checked against any queryParameters provided when subscribing. CopyOnWriteArrayList<WebSocketSessionRequest> validSessions = getValidSessions(event); try { executorService.execute(new WebSocketSender(validSessions, eventString)); } catch (RejectedExecutionException e) { EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, "Job queue is full", e, log, tenantId); } } @Override public void disconnect() { //Not needed } @Override public void destroy() { int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); ConcurrentHashMap<String, String> tenantSpecificAdapterMap = UIEventAdaptorServiceDataHolder .getTenantSpecificOutputEventStreamAdapterMap().get(tenantId); if (tenantSpecificAdapterMap != null && streamId != null) { tenantSpecificAdapterMap.remove(streamId); //Removing outputadapter and streamId } ConcurrentHashMap<String, LinkedBlockingDeque<Object>> tenantSpecificStreamEventMap = UIEventAdaptorServiceDataHolder .getTenantSpecificStreamEventMap().get(tenantId); if (tenantSpecificStreamEventMap != null && streamId != null) { //Removing the streamId and events registered for the output adapter tenantSpecificStreamEventMap.remove(streamId); } } @Override public boolean isPolled() { return true; } /** * Fetch the StreamDefinition corresponding to the given StreamId from the EventStreamService. * * @param streamId the streamId of this UIEventAdaptor. * @return the "StreamDefinition" object corresponding to the streamId of this EventAdaptor. * @throws OutputEventAdapterException if the "EventStreamService" OSGI service is unavailable/unregistered or if * the matching Steam-Definition for the given StreamId cannot be retrieved. */ private StreamDefinition getStreamDefinition(String streamId) throws OutputEventAdapterException { EventStreamService eventStreamService = UIEventAdaptorServiceDataHolder.getEventStreamService(); if (eventStreamService != null) { try { return eventStreamService.getStreamDefinition(streamId); } catch (EventStreamConfigurationException e) { String adaptorType = eventAdapterConfiguration.getType(); String adaptorName = eventAdapterConfiguration.getName(); String errorMsg = "Error while retrieving Stream-Definition for Stream with id [" + streamId + "] " + "for Adaptor [" + adaptorName + "] of type [" + adaptorType + "]."; log.error(errorMsg); throw new OutputEventAdapterException(errorMsg, e); } } throw new OutputEventAdapterException( "Could not retrieve the EventStreamService whilst trying to fetch the Stream-Definition of Stream " + "with Id [" + streamId + "]."); } /** * Fetches all valid web-socket sessions from the entire pool of subscribed sessions. The validity is checked * against any queryString provided when subscribing to the web-socket endpoint. * * @param event the current event received and that which needs to be published to subscribed sessions. * @return a list of all validated web-socket sessions against the queryString values. */ private CopyOnWriteArrayList<WebSocketSessionRequest> getValidSessions(Event event) { CopyOnWriteArrayList<WebSocketSessionRequest> validSessions = new CopyOnWriteArrayList<>(); UIOutputCallbackControllerServiceImpl uiOutputCallbackControllerServiceImpl = UIEventAdaptorServiceDataHolder .getUIOutputCallbackRegisterServiceImpl(); // get all subscribed web-socket sessions. CopyOnWriteArrayList<WebSocketSessionRequest> webSocketSessionUtils = uiOutputCallbackControllerServiceImpl .getSessions(tenantId, streamId); if (webSocketSessionUtils != null) { for (WebSocketSessionRequest webSocketSessionUtil : webSocketSessionUtils) { boolean isValidSession = validateEventAgainstSessionFilters(event, webSocketSessionUtil); if (isValidSession) { validSessions.add(webSocketSessionUtil); } } } return validSessions; } /** * Processes the given session's validity to receive the current "event" against any queryParams that was used at * the time when the web-socket-session is subscribed. This method can be extended to validate the event against * any additional attribute of the given session too. * * @param event the current event received and that which needs to be published to subscribed * sessions. * @param webSocketSessionUtil the session which needs validated for its authenticity to receive this event. * @return "true" if the session is valid to receive the event else "false". */ private boolean validateEventAgainstSessionFilters(Event event, WebSocketSessionRequest webSocketSessionUtil) { // fetch the queryString Key:Value pair map of the given session. Map<String, String> queryParamValuePairs = webSocketSessionUtil.getQueryParamValuePairs(); if (queryParamValuePairs != null) { // fetch the different attribute values received as part of the current event. Object[] eventMetaData = event.getMetaData(); Object[] eventCorrelationData = event.getCorrelationData(); Object[] eventPayloadData = event.getPayloadData(); if (streamMetaAttributes != null) { for (int i = 0; i < streamMetaAttributes.size(); i++) { String attributeName = streamMetaAttributes.get(i).getName(); String queryValue = queryParamValuePairs.get(attributeName); if (queryValue != null && (eventMetaData == null || !eventMetaData[i].toString().equals(queryValue))) { return false; } } } if (streamCorrelationAttributes != null) { for (int i = 0; i < streamCorrelationAttributes.size(); i++) { String attributeName = streamCorrelationAttributes.get(i).getName(); String queryValue = queryParamValuePairs.get(attributeName); if (queryValue != null && (eventCorrelationData == null || !eventCorrelationData[i].toString().equals(queryValue))) { return false; } } } if (streamPayloadAttributes != null) { for (int i = 0; i < streamPayloadAttributes.size(); i++) { String attributeName = streamPayloadAttributes.get(i).getName(); String queryValue = queryParamValuePairs.get(attributeName); if (queryValue != null && (eventPayloadData == null || !eventPayloadData[i].toString().equals(queryValue))) { return false; } } } } return true; } private class WebSocketSender implements Runnable { private String message; private CopyOnWriteArrayList<WebSocketSessionRequest> webSocketSessionUtils; public WebSocketSender(CopyOnWriteArrayList<WebSocketSessionRequest> webSocketSessionUtils, String message) { this.webSocketSessionUtils = webSocketSessionUtils; this.message = message; } /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p/> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see Thread#run() */ @Override public void run() { if (webSocketSessionUtils != null) { doLogDroppedMessage = true; for (WebSocketSessionRequest webSocketSessionUtil : webSocketSessionUtils) { synchronized (WebSocketSessionRequest.class) { try { webSocketSessionUtil.getSession().getBasicRemote().sendText(message); } catch (IOException e) { EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, "Cannot send to endpoint", e, log, tenantId); } } } } else if (doLogDroppedMessage) { EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, "No clients registered", log, tenantId); doLogDroppedMessage = false; } } } }