/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.jgroups.protocols;
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.apache.commons.codec.binary.Base64;
import org.apereo.portal.jpa.BasePortalJpaDao.PortalTransactional;
import org.apereo.portal.utils.JdbcUtils;
import org.hibernate.annotations.Index;
import org.jgroups.Address;
import org.jgroups.PhysicalAddress;
import org.jgroups.util.Streamable;
import org.jgroups.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
/**
* {@link PingDao} that uses the Spring JDBC APIs to do its work.
*
*/
public class JdbcPingDao implements PingDao, InitializingBean {
/**
* This class is ONLY used to provide for creation of the table/index required by the {@link
* JdbcPingDao}. Due to the JPA -> Hibernate -> Ehcache -> JGroups -> DAO_PING -> JdbcPingDao
* reference chain this class CANNOT directly reference the JPA entity manager or transaction
* manager
*/
@Entity(name = Table.NAME)
public static class Table implements Serializable {
private static final long serialVersionUID = 1L;
static final String NAME = "UP_JGROUPS_PING";
static final String CLASS_COL_SUFFIX = "_CLASS";
static final String DATA_COL_SUFFIX = "_DATA";
static final String COL_CLUSTER_NAME = "CLUSTER_NAME";
static final String COL_MEMBER_ADDRESS = "MEMBER_ADDRESS";
static final String COL_MEMBER_ADDRESS_CLASS = COL_MEMBER_ADDRESS + CLASS_COL_SUFFIX;
static final String COL_MEMBER_ADDRESS_DATA = COL_MEMBER_ADDRESS + DATA_COL_SUFFIX;
static final String COL_PHYSICAL_ADDRESS = "PHYSICAL_ADDRESS";
static final String COL_PHYSICAL_ADDRESS_CLASS = COL_PHYSICAL_ADDRESS + CLASS_COL_SUFFIX;
static final String COL_PHYSICAL_ADDRESS_DATA = COL_PHYSICAL_ADDRESS + DATA_COL_SUFFIX;
@Id
@Column(name = COL_CLUSTER_NAME, length = 100)
@Index(name = "IDX_JGROUPS_PING")
private final String clusterName = null;
@Column(name = COL_MEMBER_ADDRESS, length = 500)
private final String memberAddress = null;
@Id
@Column(name = COL_MEMBER_ADDRESS_CLASS, length = 100)
private final Class<? extends Address> memberAddressClass = null;
@Id
@Column(name = COL_MEMBER_ADDRESS_DATA, length = 550)
private final String memberAddressData = null;
@Column(name = COL_PHYSICAL_ADDRESS, length = 500)
private final String physicalAddressName = null;
@Column(name = COL_PHYSICAL_ADDRESS_CLASS, length = 100)
private final Class<? extends Address> physicalAddressClass = null;
@Column(name = COL_PHYSICAL_ADDRESS_DATA, length = 1000)
private final String physicalAddressData = null;
}
private static final String CLASS_PRM_SUFFIX = "Class";
private static final String DATA_PRM_SUFFIX = "Data";
private static final String PRM_CLUSTER_NAME = "clusterName";
private static final String PRM_MEMBER_ADDRESS = "memberAddress";
private static final String PRM_MEMBER_ADDRESS_CLASS = PRM_MEMBER_ADDRESS + CLASS_PRM_SUFFIX;
private static final String PRM_MEMBER_ADDRESS_DATA = PRM_MEMBER_ADDRESS + DATA_PRM_SUFFIX;
private static final String PRM_PHYSICAL_ADDRESS = "physicalAddress";
private static final String PRM_PHYSICAL_ADDRESS_CLASS =
PRM_PHYSICAL_ADDRESS + CLASS_PRM_SUFFIX;
private static final String PRM_PHYSICAL_ADDRESS_DATA = PRM_PHYSICAL_ADDRESS + DATA_PRM_SUFFIX;
private static final String UPDATE_SQL =
"UPDATE "
+ Table.NAME
+ " "
+ "SET "
+ Table.COL_PHYSICAL_ADDRESS
+ "=:"
+ PRM_PHYSICAL_ADDRESS
+ ", "
+ Table.COL_PHYSICAL_ADDRESS_CLASS
+ "=:"
+ PRM_PHYSICAL_ADDRESS_CLASS
+ ", "
+ Table.COL_PHYSICAL_ADDRESS_DATA
+ "=:"
+ PRM_PHYSICAL_ADDRESS_DATA
+ " "
+ "WHERE "
+ Table.COL_CLUSTER_NAME
+ "=:"
+ PRM_CLUSTER_NAME
+ " AND "
+ Table.COL_MEMBER_ADDRESS_CLASS
+ "=:"
+ PRM_MEMBER_ADDRESS_CLASS
+ " AND "
+ Table.COL_MEMBER_ADDRESS_DATA
+ "=:"
+ PRM_MEMBER_ADDRESS_DATA;
private static final String INSERT_SQL =
"INSERT INTO "
+ Table.NAME
+ " "
+ "("
+ Table.COL_CLUSTER_NAME
+ ", "
+ Table.COL_MEMBER_ADDRESS
+ ", "
+ Table.COL_MEMBER_ADDRESS_CLASS
+ ", "
+ Table.COL_MEMBER_ADDRESS_DATA
+ ", "
+ Table.COL_PHYSICAL_ADDRESS
+ ", "
+ Table.COL_PHYSICAL_ADDRESS_CLASS
+ ", "
+ Table.COL_PHYSICAL_ADDRESS_DATA
+ ") "
+ "values ("
+ ":"
+ PRM_CLUSTER_NAME
+ ", "
+ ":"
+ PRM_MEMBER_ADDRESS
+ ", :"
+ PRM_MEMBER_ADDRESS_CLASS
+ ", :"
+ PRM_MEMBER_ADDRESS_DATA
+ ", "
+ ":"
+ PRM_PHYSICAL_ADDRESS
+ ", :"
+ PRM_PHYSICAL_ADDRESS_CLASS
+ ", :"
+ PRM_PHYSICAL_ADDRESS_DATA
+ ")";
private static final String SELECT_CLUSTER_SQL =
"SELECT "
+ Table.COL_MEMBER_ADDRESS_CLASS
+ ", "
+ Table.COL_MEMBER_ADDRESS_DATA
+ ", "
+ Table.COL_PHYSICAL_ADDRESS_CLASS
+ ", "
+ Table.COL_PHYSICAL_ADDRESS_DATA
+ " "
+ "FROM "
+ Table.NAME
+ " "
+ "WHERE "
+ Table.COL_CLUSTER_NAME
+ "=:"
+ PRM_CLUSTER_NAME;
private static final String DELETE_SQL =
"DELETE FROM "
+ Table.NAME
+ " "
+ "WHERE "
+ Table.COL_CLUSTER_NAME
+ "=:"
+ PRM_CLUSTER_NAME;
protected final Logger logger = LoggerFactory.getLogger(getClass());
private JdbcOperations jdbcOperations;
private NamedParameterJdbcOperations namedParameterJdbcOperations;
private volatile boolean ready = false;
public void setJdbcOperations(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
this.namedParameterJdbcOperations = new NamedParameterJdbcTemplate(this.jdbcOperations);
}
@Override
public void afterPropertiesSet() throws Exception {
DAO_PING.setPingDao(this);
}
@PortalTransactional
@Override
public void addAddress(String clusterName, Address address, PhysicalAddress physicalAddress) {
if (!isReady()) {
return;
}
final Map<String, Object> paramMap = new HashMap<String, Object>();
try {
paramMap.put(PRM_CLUSTER_NAME, clusterName);
setStreamableParam(paramMap, PRM_MEMBER_ADDRESS, address);
setStreamableParam(paramMap, PRM_PHYSICAL_ADDRESS, physicalAddress);
final int rowCount = this.namedParameterJdbcOperations.update(UPDATE_SQL, paramMap);
if (rowCount == 0) {
this.namedParameterJdbcOperations.update(INSERT_SQL, paramMap);
logger.debug("Inserted cluster address: " + paramMap);
} else {
logger.debug("Updated cluster address: " + paramMap);
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.warn("Failed to store cluster address: " + paramMap, e);
} else {
logger.warn("Failed to store cluster address: " + paramMap);
}
}
}
@Override
public Map<Address, PhysicalAddress> getAddresses(String clusterName) {
if (!isReady()) {
return Collections.emptyMap();
}
final Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put(PRM_CLUSTER_NAME, clusterName);
return this.namedParameterJdbcOperations.query(
SELECT_CLUSTER_SQL,
paramMap,
new ResultSetExtractor<Map<Address, PhysicalAddress>>() {
@Override
public Map<Address, PhysicalAddress> extractData(ResultSet rs)
throws SQLException, DataAccessException {
final Map<Address, PhysicalAddress> result =
new HashMap<Address, PhysicalAddress>();
while (rs.next()) {
try {
final Address memberAddress =
getStreamableParam(rs, Table.COL_MEMBER_ADDRESS);
final PhysicalAddress physicalAddress =
getStreamableParam(rs, Table.COL_PHYSICAL_ADDRESS);
result.put(memberAddress, physicalAddress);
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.warn(
"Ignoring address result due to data parsing error", e);
} else {
logger.warn(
"Ignoring address result due to data parsing error");
}
}
}
logger.debug("Found {} addresses in cluster: {}", result.size(), result);
return result;
}
});
}
@PortalTransactional
@Override
public void purgeOtherAddresses(String clusterName, Collection<Address> includedAddresses) {
if (!isReady()) {
return;
}
final Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put(PRM_CLUSTER_NAME, clusterName);
final StringBuilder deleteSqlBuilder = new StringBuilder(DELETE_SQL);
for (final Address address : includedAddresses) {
final String paramPrefix = PRM_MEMBER_ADDRESS + paramMap.size();
deleteSqlBuilder
.append(" AND (")
.append(Table.COL_MEMBER_ADDRESS_CLASS)
.append(" <> :")
.append(paramPrefix)
.append(CLASS_PRM_SUFFIX)
.append(" OR ")
.append(Table.COL_MEMBER_ADDRESS_DATA)
.append(" <> :")
.append(paramPrefix)
.append(DATA_PRM_SUFFIX)
.append(")");
setStreamableParam(paramMap, paramPrefix, address);
}
try {
final int purged =
this.namedParameterJdbcOperations.update(deleteSqlBuilder.toString(), paramMap);
logger.debug(
"Purged {} addresses from '{}' cluster while retaining: {}",
purged,
clusterName,
includedAddresses);
} catch (DataIntegrityViolationException e) {
if (logger.isDebugEnabled()) {
logger.warn("Failed to purge old addresses for cluster '{}'", clusterName, e);
} else {
logger.warn("Failed to purge old addresses for cluster '{}'", clusterName);
}
}
}
protected boolean isReady() {
boolean r = this.ready;
if (!r) {
r = JdbcUtils.doesTableExist(this.jdbcOperations, Table.NAME);
if (r) {
this.ready = r;
}
}
return r;
}
@SuppressWarnings("unchecked")
protected <T extends Streamable> T getStreamableParam(ResultSet rs, String columnPrefix)
throws SQLException {
final String className = rs.getString(columnPrefix + Table.CLASS_COL_SUFFIX);
final Class<? extends Streamable> cl;
try {
cl = (Class<? extends Streamable>) Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to find class '" + className + "'", e);
}
final String streamableData = rs.getString(columnPrefix + Table.DATA_COL_SUFFIX);
try {
final byte[] streamableBytes = Base64.decodeBase64(streamableData);
return (T) Util.streamableFromByteBuffer(cl, streamableBytes);
} catch (Exception e) {
throw new RuntimeException(
"Failed to convert base64 string '"
+ streamableData
+ "' back into '"
+ cl
+ "'",
e);
}
}
protected void setStreamableParam(
Map<String, Object> paramMap, String paramPrefix, Streamable s) {
paramMap.put(paramPrefix + CLASS_PRM_SUFFIX, s.getClass().getName());
try {
final byte[] streamableBytes = Util.streamableToByteBuffer(s);
final String streamableData = Base64.encodeBase64String(streamableBytes);
paramMap.put(paramPrefix + DATA_PRM_SUFFIX, streamableData);
} catch (Exception e) {
throw new RuntimeException(
"Failed to convert '" + s + "' into a base64 string for persistence", e);
}
paramMap.put(paramPrefix, s.toString());
}
}