/** * 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()); } }