package org.apereo.cas.ticket.registry; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IMap; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.ticket.Ticket; import org.apereo.cas.ticket.TicketCatalog; import org.apereo.cas.ticket.TicketDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PreDestroy; import java.io.Closeable; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; /** * Hazelcast-based implementation of a {@link TicketRegistry}. * <p>This implementation just wraps the Hazelcast's {@link IMap} * which is an extension of the standard Java's {@code ConcurrentMap}.</p> * <p>The heavy lifting of distributed data partitioning, network cluster discovery and * join, data replication, etc. is done by Hazelcast's Map implementation.</p> * * @author Dmitriy Kopylenko * @author Jonathan Johnson * @since 4.1.0 */ public class HazelcastTicketRegistry extends AbstractTicketRegistry implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(HazelcastTicketRegistry.class); private final HazelcastInstance hazelcastInstance; private final TicketCatalog ticketCatalog; private final int pageSize; /** * Instantiates a new Hazelcast ticket ticketGrantingTicketsRegistry. * * @param hz An instance of {@code HazelcastInstance} * @param plan the plan * @param pageSize the page size */ public HazelcastTicketRegistry(final HazelcastInstance hz, final TicketCatalog plan, final int pageSize) { this.hazelcastInstance = hz; this.pageSize = pageSize; this.ticketCatalog = plan; LOGGER.info("Setting up Hazelcast Ticket Registry instance [{}]", this.hazelcastInstance); } @Override public Ticket updateTicket(final Ticket ticket) { addTicket(ticket); return ticket; } @Override public void addTicket(final Ticket ticket) { final long ttl = ticket.getExpirationPolicy().getTimeToLive(); if (ttl < 0) { throw new IllegalArgumentException("The expiration policy of ticket " + ticket.getId() + "is set to use a negative ttl"); } LOGGER.debug("Adding ticket [{}] with ttl [{}s]", ticket.getId(), ttl); final Ticket encTicket = encodeTicket(ticket); final TicketDefinition metadata = this.ticketCatalog.find(ticket); final IMap<String, Ticket> ticketMap = getTicketMapInstanceByMetadata(metadata); ticketMap.set(encTicket.getId(), encTicket, ttl, TimeUnit.SECONDS); LOGGER.debug("Added ticket [{}] with ttl [{}s]", encTicket.getId(), ttl); } private IMap<String, Ticket> getTicketMapInstanceByMetadata(final TicketDefinition metadata) { final String mapName = metadata.getProperties().getStorageName(); LOGGER.debug("Locating map name [{}] for ticket definition [{}]", mapName, metadata); return getTicketMapInstance(mapName); } @Override public Ticket getTicket(final String ticketId) { final String encTicketId = encodeTicketId(ticketId); if (StringUtils.isNotBlank(encTicketId)) { final TicketDefinition metadata = this.ticketCatalog.find(ticketId); if (metadata != null) { final Ticket ticket = getTicketMapInstanceByMetadata(metadata).get(encTicketId); return decodeTicket(ticket); } LOGGER.warn("No ticket definition could be found in the catalog to match [{}]", ticketId); } return null; } @Override public boolean deleteSingleTicket(final String ticketId) { final String encTicketId = encodeTicketId(ticketId); final TicketDefinition metadata = this.ticketCatalog.find(ticketId); final IMap<String, Ticket> map = getTicketMapInstanceByMetadata(metadata); return map.remove(encTicketId) != null; } @Override public long deleteAll() { final Collection<TicketDefinition> metadata = this.ticketCatalog.findAll(); final AtomicLong count = new AtomicLong(); metadata.forEach(r -> { final IMap<String, Ticket> instance = getTicketMapInstanceByMetadata(r); if (instance != null) { count.addAndGet(instance.size()); instance.evictAll(); instance.clear(); } }); return count.get(); } @Override public Collection<Ticket> getTickets() { final Collection<Ticket> tickets = new HashSet<>(); try { final Collection<TicketDefinition> metadata = this.ticketCatalog.findAll(); metadata.forEach(t -> { final IMap<String, Ticket> map = getTicketMapInstanceByMetadata(t); tickets.addAll(map.values().stream().limit(this.pageSize).collect(Collectors.toList())); }); return tickets; } catch (final Exception e) { LOGGER.warn(e.getMessage(), e); } return decodeTickets(tickets); } /** * Make sure we shutdown HazelCast when the context is destroyed. */ @PreDestroy public void shutdown() { try { LOGGER.info("Shutting down Hazelcast instance [{}]", this.hazelcastInstance.getConfig().getInstanceName()); this.hazelcastInstance.shutdown(); } catch (final Throwable e) { LOGGER.debug(e.getMessage()); } } @Override public void close() throws IOException { shutdown(); } private IMap<String, Ticket> getTicketMapInstance(final String mapName) { try { final IMap<String, Ticket> inst = hazelcastInstance.getMap(mapName); LOGGER.debug("Located Hazelcast map instance [{}] for [{}]", inst, mapName); return inst; } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return null; } }