Java tutorial
/* * 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.sling.discovery.impl.topology.connector; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; import java.util.Date; import java.util.Iterator; import java.util.UUID; import java.util.zip.GZIPOutputStream; import javax.servlet.http.HttpServletResponse; import org.apache.commons.httpclient.Credentials; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.PutMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.sling.commons.json.JSONException; import org.apache.sling.discovery.ClusterView; import org.apache.sling.discovery.InstanceDescription; import org.apache.sling.discovery.impl.Config; import org.apache.sling.discovery.impl.cluster.ClusterViewService; import org.apache.sling.discovery.impl.topology.announcement.Announcement; import org.apache.sling.discovery.impl.topology.announcement.AnnouncementFilter; import org.apache.sling.discovery.impl.topology.announcement.AnnouncementRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A topology connector client is used for sending (pinging) a remote topology * connector servlet and exchanging announcements with it */ public class TopologyConnectorClient implements TopologyConnectorClientInformation { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** the endpoint url **/ private final URL connectorUrl; /** the cluster view service **/ private final ClusterViewService clusterViewService; /** the config service to user **/ private final Config config; /** the id of this connection **/ private final UUID id; /** the announcement registry **/ private final AnnouncementRegistry announcementRegistry; /** the last inherited announcement **/ private Announcement lastInheritedAnnouncement; /** the time when the last announcement was inherited - for webconsole use only **/ private long lastPingedAt; /** the information about this server **/ private final String serverInfo; /** the status code of the last post **/ private int lastStatusCode = -1; /** SLING-3316: whether or not this connector was auto-stopped **/ private boolean autoStopped = false; /** more details about connection failures **/ private String statusDetails = null; /** SLING-2882: whether or not to suppress ping warnings **/ private boolean suppressPingWarnings_ = false; private TopologyRequestValidator requestValidator; /** value of Content-Encoding of the last request **/ private String lastRequestEncoding; /** value of Content-Encoding of the last repsonse **/ private String lastResponseEncoding; /** SLING-3382: unix-time at which point the backoff-period ends and pings can be sent again **/ private long backoffPeriodEnd = -1; TopologyConnectorClient(final ClusterViewService clusterViewService, final AnnouncementRegistry announcementRegistry, final Config config, final URL connectorUrl, final String serverInfo) { if (clusterViewService == null) { throw new IllegalArgumentException("clusterViewService must not be null"); } if (announcementRegistry == null) { throw new IllegalArgumentException("announcementRegistry must not be null"); } if (config == null) { throw new IllegalArgumentException("config must not be null"); } if (connectorUrl == null) { throw new IllegalArgumentException("connectorUrl must not be null"); } this.requestValidator = new TopologyRequestValidator(config); this.clusterViewService = clusterViewService; this.announcementRegistry = announcementRegistry; this.config = config; this.connectorUrl = connectorUrl; this.serverInfo = serverInfo; this.id = UUID.randomUUID(); } /** ping the server and pass the announcements between the two **/ void ping(final boolean force) { if (autoStopped) { // then we suppress any further pings! logger.debug("ping: autoStopped=true, hence suppressing any further pings."); return; } if (force) { backoffPeriodEnd = -1; } else if (backoffPeriodEnd > 0) { if (System.currentTimeMillis() < backoffPeriodEnd) { logger.debug("ping: not issueing a heartbeat due to backoff instruction from peer."); return; } else { logger.debug("ping: backoff period ended, issuing another ping now."); } } final String uri = connectorUrl.toString() + "." + clusterViewService.getSlingId() + ".json"; if (logger.isDebugEnabled()) { logger.debug("ping: connectorUrl=" + connectorUrl + ", complete uri=" + uri); } HttpClient httpClient = new HttpClient(); final PutMethod method = new PutMethod(uri); Announcement resultingAnnouncement = null; try { String userInfo = connectorUrl.getUserInfo(); if (userInfo != null) { Credentials c = new UsernamePasswordCredentials(userInfo); httpClient.getState() .setCredentials(new AuthScope(method.getURI().getHost(), method.getURI().getPort()), c); } Announcement topologyAnnouncement = new Announcement(clusterViewService.getSlingId()); topologyAnnouncement.setServerInfo(serverInfo); final ClusterView clusterView = clusterViewService.getClusterView(); topologyAnnouncement.setLocalCluster(clusterView); if (force) { logger.debug("ping: sending a resetBackoff"); topologyAnnouncement.setResetBackoff(true); } announcementRegistry.addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() { public boolean accept(final String receivingSlingId, final Announcement announcement) { // filter out announcements that are of old cluster instances // which I dont really have in my cluster view at the moment final Iterator<InstanceDescription> it = clusterViewService.getClusterView().getInstances() .iterator(); while (it.hasNext()) { final InstanceDescription instance = it.next(); if (instance.getSlingId().equals(receivingSlingId)) { // then I have the receiving instance in my cluster view // all fine then return true; } } // looks like I dont have the receiving instance in my cluster view // then I should also not propagate that announcement anywhere return false; } }); final String p = requestValidator.encodeMessage(topologyAnnouncement.asJSON()); if (logger.isDebugEnabled()) { logger.debug("ping: topologyAnnouncement json is: " + p); } requestValidator.trustMessage(method, p); if (config.isGzipConnectorRequestsEnabled()) { // tell the server that the content is gzipped: method.addRequestHeader("Content-Encoding", "gzip"); // and gzip the body: final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final GZIPOutputStream gzipOut = new GZIPOutputStream(baos); gzipOut.write(p.getBytes("UTF-8")); gzipOut.close(); final byte[] gzippedEncodedJson = baos.toByteArray(); method.setRequestEntity(new ByteArrayRequestEntity(gzippedEncodedJson, "application/json")); lastRequestEncoding = "gzip"; } else { // otherwise plaintext: method.setRequestEntity(new StringRequestEntity(p, "application/json", "UTF-8")); lastRequestEncoding = "plaintext"; } // independent of request-gzipping, we do accept the response to be gzipped, // so indicate this to the server: method.addRequestHeader("Accept-Encoding", "gzip"); DefaultHttpMethodRetryHandler retryhandler = new DefaultHttpMethodRetryHandler(0, false); httpClient.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, retryhandler); httpClient.getHttpConnectionManager().getParams() .setConnectionTimeout(1000 * config.getConnectionTimeout()); httpClient.getHttpConnectionManager().getParams().setSoTimeout(1000 * config.getSoTimeout()); method.getParams().setSoTimeout(1000 * config.getSoTimeout()); httpClient.executeMethod(method); if (logger.isDebugEnabled()) { logger.debug("ping: done. code=" + method.getStatusCode() + " - " + method.getStatusText()); } lastStatusCode = method.getStatusCode(); lastResponseEncoding = null; if (method.getStatusCode() == HttpServletResponse.SC_OK) { final Header contentEncoding = method.getResponseHeader("Content-Encoding"); if (contentEncoding != null && contentEncoding.getValue() != null && contentEncoding.getValue().contains("gzip")) { lastResponseEncoding = "gzip"; } else { lastResponseEncoding = "plaintext"; } String responseBody = requestValidator.decodeMessage(method); // limiting to 16MB, should be way enough if (logger.isDebugEnabled()) { logger.debug("ping: response body=" + responseBody); } if (responseBody != null && responseBody.length() > 0) { Announcement inheritedAnnouncement = Announcement.fromJSON(responseBody); final long backoffInterval = inheritedAnnouncement.getBackoffInterval(); if (backoffInterval > 0) { // then reset the backoffPeriodEnd: /* minus 1 sec to avoid slipping the interval by a few millis */ this.backoffPeriodEnd = System.currentTimeMillis() + (1000 * backoffInterval) - 1000; logger.debug("ping: servlet instructed to backoff: backoffInterval=" + backoffInterval + ", resulting in period end of " + new Date(backoffPeriodEnd)); } else { logger.debug("ping: servlet did not instruct any backoff-ing at this stage"); this.backoffPeriodEnd = -1; } if (inheritedAnnouncement.isLoop()) { if (logger.isDebugEnabled()) { logger.debug( "ping: connector response indicated a loop detected. not registering this announcement from " + inheritedAnnouncement.getOwnerId()); } if (inheritedAnnouncement.getOwnerId().equals(clusterViewService.getSlingId())) { // SLING-3316 : local-loop detected. Check config to see if we should stop this connector if (config.isAutoStopLocalLoopEnabled()) { inheritedAnnouncement = null; // results in connected -> false and representsloop -> true autoStopped = true; // results in isAutoStopped -> true } } } else { inheritedAnnouncement.setInherited(true); if (announcementRegistry.registerAnnouncement(inheritedAnnouncement) == -1) { if (logger.isDebugEnabled()) { logger.debug( "ping: connector response is from an instance which I already see in my topology" + inheritedAnnouncement); } statusDetails = "receiving side is seeing me via another path (connector or cluster) already (loop)"; return; } } resultingAnnouncement = inheritedAnnouncement; statusDetails = null; } else { statusDetails = "no response body received"; } } else { statusDetails = "got HTTP Status-Code: " + lastStatusCode; } // SLING-2882 : reset suppressPingWarnings_ flag in success case suppressPingWarnings_ = false; } catch (URIException e) { logger.warn("ping: Got URIException: " + e + ", uri=" + uri); statusDetails = e.toString(); } catch (IOException e) { // SLING-2882 : set/check the suppressPingWarnings_ flag if (suppressPingWarnings_) { if (logger.isDebugEnabled()) { logger.debug("ping: got IOException: " + e + ", uri=" + uri); } } else { suppressPingWarnings_ = true; logger.warn("ping: got IOException [suppressing further warns]: " + e + ", uri=" + uri); } statusDetails = e.toString(); } catch (JSONException e) { logger.warn("ping: got JSONException: " + e); statusDetails = e.toString(); } catch (RuntimeException re) { logger.warn("ping: got RuntimeException: " + re, re); statusDetails = re.toString(); } finally { method.releaseConnection(); lastInheritedAnnouncement = resultingAnnouncement; lastPingedAt = System.currentTimeMillis(); } } public int getStatusCode() { return lastStatusCode; } public URL getConnectorUrl() { return connectorUrl; } public boolean representsLoop() { if (autoStopped) { return true; } if (lastInheritedAnnouncement == null) { return false; } else { return lastInheritedAnnouncement.isLoop(); } } public boolean isConnected() { if (autoStopped) { return false; } if (lastInheritedAnnouncement == null) { return false; } else { return announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId()); } } public String getStatusDetails() { if (autoStopped) { return "auto-stopped"; } if (lastInheritedAnnouncement == null) { return statusDetails; } else { if (announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId())) { // still active - so no status details return null; } else { return "received announcement has expired (it was last renewed " + new Date(lastPingedAt) + ") - consider increasing heartbeat timeout"; } } } public long getLastHeartbeatSent() { return lastPingedAt; } public int getNextHeartbeatDue() { final long absDue; if (backoffPeriodEnd > 0) { absDue = backoffPeriodEnd; } else { absDue = lastPingedAt + 1000 * config.getHeartbeatInterval(); } final int relDue = (int) ((absDue - System.currentTimeMillis()) / 1000); if (relDue < 0) { return -1; } else { return relDue; } } public boolean isAutoStopped() { return autoStopped; } public String getLastRequestEncoding() { return lastRequestEncoding == null ? "" : lastRequestEncoding; } public String getLastResponseEncoding() { return lastResponseEncoding == null ? "" : lastResponseEncoding; } public String getRemoteSlingId() { if (lastInheritedAnnouncement == null) { return null; } else { return lastInheritedAnnouncement.getOwnerId(); } } public String getId() { return id.toString(); } /** Disconnect this connector **/ public void disconnect() { final String uri = connectorUrl.toString() + "." + clusterViewService.getSlingId() + ".json"; if (logger.isDebugEnabled()) { logger.debug("disconnect: connectorUrl=" + connectorUrl + ", complete uri=" + uri); } if (lastInheritedAnnouncement != null) { announcementRegistry.unregisterAnnouncement(lastInheritedAnnouncement.getOwnerId()); } HttpClient httpClient = new HttpClient(); final DeleteMethod method = new DeleteMethod(uri); try { String userInfo = connectorUrl.getUserInfo(); if (userInfo != null) { Credentials c = new UsernamePasswordCredentials(userInfo); httpClient.getState() .setCredentials(new AuthScope(method.getURI().getHost(), method.getURI().getPort()), c); } requestValidator.trustMessage(method, null); httpClient.executeMethod(method); if (logger.isDebugEnabled()) { logger.debug("disconnect: done. code=" + method.getStatusCode() + " - " + method.getStatusText()); } // ignoring the actual statuscode though as there's little we can // do about it after this point } catch (URIException e) { logger.warn("disconnect: Got URIException: " + e); } catch (IOException e) { logger.warn("disconnect: got IOException: " + e); } catch (RuntimeException re) { logger.error("disconnect: got RuntimeException: " + re, re); } finally { method.releaseConnection(); } } }