/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2002
* Copyright by ESO (in the framework of the ALMA collaboration),
* and Cosylab
* All rights reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/
package com.cosylab.acs.laser.dao;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;
import java.util.Set;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import cern.laser.business.LaserObjectNotFoundException;
import cern.laser.business.cache.AlarmCache;
import cern.laser.business.cache.AlarmCacheException;
import cern.laser.business.cache.AlarmCacheListener;
import cern.laser.business.dao.AlarmDAO;
import cern.laser.business.data.Alarm;
import cern.laser.business.data.AlarmImpl;
import cern.laser.business.data.AlarmChange;
import cern.laser.business.data.CategoryActiveList;
import cern.laser.business.data.Category;
import cern.laser.business.data.Location;
import cern.laser.business.data.ResponsiblePerson;
import cern.laser.business.data.Source;
import cern.laser.business.data.StatusImpl;
import cern.laser.business.data.Triplet;
import cern.laser.business.definition.data.SourceDefinition;
import com.cosylab.acs.laser.dao.ACSAlarmDAOImpl;
/**
* Implementation of the {@link AlarmCache} to use within ACS.
* <P>
* <b>Note on the locking mechanism.</b><BR>
* Each method that changes or reads the cache is surrounded by lock/unlock
* to ensure the mutual exclusion and therefore the integrity of the cache.
* If a sequence of operations have to be performed in mutual exclusion then
* the code must initially call <code>acquire()</code> and finally <code>release()</code>.
* The locking mechanism of each method works as follows:
* <UL>
* <LI>if the lock is free then it is acquired at the beginning and released before exiting
* <LI>if the lock is not free then it is acquired only if owned by the same thread that is locking it
* and released before exiting
* </UL>
*
* @author acaproni
*/
public class ACSAlarmCacheImpl implements AlarmCache
{
/**
* The empty String used for undefined String fields
*/
private static final String EMPTY_STRING="";
/**
* The key of the property for undocumented alarms
*/
public static final String alarmServerPropkey="AlarmServerProp";
/**
* The value of the property for undocumented alarms
*/
public static final String undocumentedAlarmProp="UnconfiguredAlarm";
// The cache of the alarms
//
// The key is a string (the same string generated
// by Alarm.getTriplet().toIdentifier())
// The value is an alarm (AlarmImpl)
private Map<String,Alarm> alarms = Collections.synchronizedMap(new HashMap<String,Alarm>());
// The object to access the database
private AlarmDAO dao;
// The listener for the changes in this cache
private AlarmCacheListener listener;
/**
* the lock used to ensure mutual exclusion when accessing the cache.
*
* It is required instead of synchronized because sometimes the mutual exclusion
* spans between calls to a series of methods of the cache.
*/
private final ReentrantLock lock=new ReentrantLock();
/**
* The HashMap with the active alarms per each category
* The map contains a CategoryActiveList per each category
* (the key is the Integer identifying the category)
*
* Policy:
* the CategoryActiveList for a category is created and inserted in the map
* when a new alarm arrives (remember that an alarm has a Set of Categories)
* So it basically happens in the put and replace methods (based on the
* status of the alarm itself)
*/
private HashMap<Integer,CategoryActiveList> activeLists = new HashMap<Integer,CategoryActiveList>();
/**
* The logger
*/
private final Logger logger;
/**
* The category DAO
*/
private final ACSCategoryDAOImpl categoryDAO;
/**
* The constructor
*
* @param alarmDAO The object to access the database
* @param alarmCacheListener The listener
*/
public ACSAlarmCacheImpl(AlarmDAO alarmDAO, ACSCategoryDAOImpl categoryDAO, AlarmCacheListener alarmCacheListener,Logger logger)
{
if (alarmDAO==null || alarmCacheListener==null) {
throw new IllegalArgumentException("The AlarmDAO and the listener can't be null!");
}
this.logger=logger;
this.categoryDAO=categoryDAO;
alarms.clear(); // Redundant
// Store the values in local variables
dao=alarmDAO;
ACSAlarmDAOImpl t = (ACSAlarmDAOImpl)dao;
t.setAlarmCache(this);
listener=alarmCacheListener;
// The alarms should be loaded in the cache in the constructor
// but it is not working because something in the DAO chain has
// not yet been initialized
//
// All the alarms are load when the first request arrives
// in the getReference method
}
public void initializeAlarmCache(Map alarms, Map activeLists) {
lock.lock();
try {
this.alarms.putAll(alarms);
this.activeLists.putAll(activeLists);
} finally {
lock.unlock();
}
}
public Alarm getCopy(String identifier) throws AlarmCacheException {
// This method get the reference to the object first and then
// create a copy to return to the caller
// The Alarm to return a copy of
lock.lock();
Alarm retAl;
try {
retAl = getReference(identifier);
return (Alarm)(((AlarmImpl)retAl).clone());
} finally {
lock.unlock();
}
}
/**
* Get a reference to the alarm with the passed identifier.
* The alarm is searched in the configuration database delegating
* to the {@link ACSAlarmDAOImpl}.
* <P>
* If the alarm is not found then an alarm is built to be sent to the client
* but it is marked with a special property to identify it as a misconfigured
* alarm. In the case of the alarm panel, such alarms will be displayed in a
* dedicated tab.
*
* @param identifier The ID of the alarm to get from the cache
*/
public Alarm getReference(String identifier) throws AlarmCacheException {
lock.lock();
Alarm retAl;
try {
// The Alarm to return a reference of
// Check the integrity of internal data structs
if (alarms==null || dao==null) {
System.err.println("*** ACSAlarmCache internal data corrupted!");
throw new AlarmCacheException("ACSAlarmCache internal data corrupted!");
}
if (!alarms.containsKey(identifier)) {
// Get the alarm from the database
try {
retAl=(Alarm)dao.findAlarm(identifier);
} catch (LaserObjectNotFoundException lonfe) {
// The alarm is not in the configuration database
//
// Built an alarm to be sent to the clients
// with an special property
logger.finer(identifier+" is not in TM/CDB: building an unconfigured alarm");
retAl=buildUnconfiguredAlarm(identifier);
} catch (Throwable t) {
System.err.println("*** Exception reading from CDB "+t.getMessage());
throw new AlarmCacheException(t.getMessage());
}
if (retAl==null) {
System.err.println("*** Alarm not found in database");
throw new AlarmCacheException("Alarm not found in database");
} else {
// Add the alarm to the cache
alarms.put(identifier,retAl);
}
} else {
// Get the alarm from the cache
retAl=alarms.get(identifier);
}
if (retAl==null) {
System.err.println("*** Invalid Alarm");
throw new AlarmCacheException("Invalid Alarm");
}
return retAl;
} finally {
lock.unlock();
}
}
/**
* Update an alarm in the cache without notifying the listener.
*
* NOTE: this is used when alarms are generated on the fly (defaultFM) because in
* that case there i sno need to notify the listener
*
* @param alarm
*/
public void update(Alarm alarm) {
lock.lock();
try {
if (alarm==null) {
throw new IllegalArgumentException("The alarm can't be null");
}
alarms.put(alarm.getAlarmId(), alarm);
dao.updateAlarm(alarm);
} finally {
lock.unlock();
}
}
public void replace(Alarm alarm) throws AlarmCacheException {
lock.lock();
try {
if (alarm==null) {
throw new IllegalArgumentException("Replacing with a null alarm is not allowed");
}
Alarm oldAl=alarms.put(alarm.getTriplet().toIdentifier(),alarm);
dao.updateAlarm(alarm);
sendMsgToListener(alarm,oldAl);
updateCategoryActiveLists(alarm);
//dumpAlarmsCache(false);
} finally {
lock.unlock();
}
}
public void put(Alarm alarm) throws AlarmCacheException {
lock.lock();
try {
if (alarm==null) {
throw new IllegalArgumentException("Inserting a null alarm is not allowed");
}
Alarm oldAl=alarms.put(alarm.getTriplet().toIdentifier(),alarm);
dao.updateAlarm(alarm);
updateCategoryActiveLists(alarm);
sendMsgToListener(alarm,oldAl);
//dumpAlarmsCache(false);
} finally {
lock.unlock();
}
}
public void invalidate(String identifier) throws AlarmCacheException {
lock.lock();
try {
if (identifier==null) {
throw new IllegalArgumentException("Invalidating a null key is not allowed");
}
if (!alarms.containsKey(identifier)) {
throw new AlarmCacheException("The object with the given identifier does not exist");
}
alarms.remove(identifier);
//dumpAlarmsCache(false);
} finally {
lock.unlock();
}
}
public CategoryActiveList getActiveListReference(Integer identifier) throws AlarmCacheException {
lock.lock();
try {
if (activeLists.containsKey(identifier)) {
return activeLists.get(identifier);
} else {
CategoryActiveList catList = new CategoryActiveList(identifier);
activeLists.put(identifier,catList);
return catList;
}
} finally {
lock.unlock();
}
}
public void close() {
lock.lock();
try {
alarms.clear();
} finally {
lock.unlock();
}
}
public void removeActiveList(Integer identifier) throws AlarmCacheException {
lock.lock();
activeLists.remove(identifier);
lock.unlock();
}
/**
* Send the message to the listener
*
* NOTE: If the previous alarm does not exist (for example if the alarm is new)
* then we send as old alarm the actual alarm
* If we send a null then cern.laser.business.pojo.AlarmPublisher:publish
* returns an exception while executing
* Status previous_alarm_status=previous.getStatus();
*
* This policy seems reasonable but could have an impact somewhere
*
* @param actual The actual alarm
* @param old The previous alarm
*/
private void sendMsgToListener(Alarm actual, Alarm old) {
if (old==null) {
listener.onAlarmChange(new AlarmChange(actual,actual));
} else {
listener.onAlarmChange(new AlarmChange(actual,old));
}
}
/**
* Print a copy of the alarms in the cache in the standard output.
* This method is added for debugging
*
* @param verbose If true the details of each alarm found in the
* cache is also displayed
*/
private void dumpAlarmsCache(boolean verbose) {
if (alarms==null) {
return;
}
if (alarms.size()==0) {
return;
}
System.out.println("*** ACSAlarmCacheImpl dumping cache.....");
// Get the keys
Set<String> keys = alarms.keySet();
Iterator<String> iter = keys.iterator();
while ( iter.hasNext() ) {
String key = iter.next();
System.out.print("***\t"+key+" ");
if (verbose) {
Alarm al = alarms.get(key);
System.out.println("active="+al.getStatus().getActive());
} else {
System.out.println();
}
}
System.out.println("ACSAlarmCacheImpl dumping cache..... done");
}
/**
* Return the CategoryActiveList for the specified categoryId.
* If the CategoryActiveList does not exist than a new one is created and
* inserted in the activeList and then returned to the caller
*
* @param categoryId The categoryId identifying the CategoryActiveList
* @return The CategoryActiveList for the specified categoryId
*/
private CategoryActiveList getCategoryList(Integer categoryId) {
if (activeLists.containsKey(categoryId)) {
return activeLists.get(categoryId);
} else {
CategoryActiveList catList = new CategoryActiveList(categoryId);
activeLists.put(categoryId,catList);
return catList;
}
}
/**
* Update the CategoryActiveList for the alarm: all the CategoryActiveLists
* of the categories of the alarm will be updated.
* The alarmId is inserted or removed in the CategoryActiveList depending
* of the status of the alarm
*
* @param alarm The alarm whose id must be inserted or removed for the
* CategoryActiveLists of its categories
*/
private void updateCategoryActiveLists(Alarm alarm) {
if (alarm.getStatus()==null || alarm.getStatus().getActive()==null) {
throw new IllegalArgumentException("Invalid alarm: status and/or status.active null");
}
boolean status = alarm.getStatus().getActive().booleanValue();
String alarmId = alarm.getAlarmId();
Iterator categoryIterator = alarm.getCategories().iterator();
while (categoryIterator.hasNext()) {
Integer categoryId = (Integer)((Category)categoryIterator.next()).getCategoryId();
CategoryActiveList catList = null;
try {
catList = getActiveListReference(categoryId);
} catch (AlarmCacheException ace) {
// In this implementation no exception can be thrown
// by getActiveListReference
// I write a message in case someone changes the things
System.err.println("Exception "+ace.getMessage());
ace.printStackTrace();
continue;
}
if (status) {
catList.addAlarm(alarmId);
} else {
catList.removeAlarm(alarmId);
}
}
}
/**
* Acquire the lock for using the cache.
*/
public void acquire() {
if (lock.isHeldByCurrentThread()) {
System.out.println("===>>>>> The same thread tries to acquire the cache more then once!");
}
lock.lock();
}
/**
* Acquire the lock for using the cache.
*/
public void release() {
lock.unlock();
}
/**
* Build a unconfigured alarm from the passed ID.
*
* A unconfigured alarm has no data but the triplet generated
* from the identifier.
* I associate the lowest priority and a user property
* to be recognized by clients as being generated by the AS instead
* of being retrieved from the configuration database.
* <P>
* The property identifying this alarm as generated by the AS is:
* <Name="AlarmServerProp", Value="UnconfiguredAlarm">
* <P>
* The alarm is associated to the ROOT category.
*
* @param alarmID The ID of the alarm
* @return The default alarm
*/
private Alarm buildUnconfiguredAlarm(String alarmID) {
if (alarmID==null || alarmID.isEmpty()) {
throw new IllegalArgumentException("Invalid null or empty alarm ID!");
}
String[] parts=alarmID.split(":");
if (parts.length!=3) {
throw new IllegalArgumentException("Invalid alarm ID: "+alarmID);
}
String FF=parts[0];
String FM=parts[1];
Integer FC;
try {
FC=Integer.valueOf(parts[2]);
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException("Invalid FC in alarm ID: "+alarmID,nfe);
}
AlarmImpl alarm = new AlarmImpl();
alarm.setMultiplicityChildrenIds(new HashSet());
alarm.setMultiplicityParentIds(new HashSet());
alarm.setNodeChildrenIds(new HashSet());
alarm.setNodeParentIds(new HashSet());
alarm.setAction(EMPTY_STRING);
alarm.setTriplet(new Triplet(FF, FM, FC));
alarm.setCategories(new HashSet<Category>());
alarm.setCause(EMPTY_STRING);
alarm.setConsequence(EMPTY_STRING);
alarm.setProblemDescription(EMPTY_STRING);
alarm.setHelpURL(null);
alarm.setInstant(false);
Location location = new Location("0",EMPTY_STRING,EMPTY_STRING,EMPTY_STRING,EMPTY_STRING);
alarm.setLocation(location);
alarm.setPiquetEmail(EMPTY_STRING);
alarm.setPiquetGSM(EMPTY_STRING);
alarm.setPriority(3);
ResponsiblePerson responsible = new ResponsiblePerson(
1,EMPTY_STRING,EMPTY_STRING,EMPTY_STRING,EMPTY_STRING,EMPTY_STRING);
alarm.setResponsiblePerson(responsible);
SourceDefinition srcDef = new SourceDefinition("ALARM_SYSTEM_SOURCES","SOURCE",EMPTY_STRING,15,1);
Source src = new Source(srcDef,responsible);
alarm.setSource(src);
alarm.setIdentifier(alarm.getTriplet().toIdentifier());
// Build the status in order to associate the property
Properties userProps = new Properties();
userProps.put(alarmServerPropkey, undocumentedAlarmProp);
StatusImpl status = new StatusImpl(
false,false,false, false, false, EMPTY_STRING, new Timestamp(0), new Timestamp(0), new Timestamp(0), userProps);
alarm.setStatus(status);
// Category association
//
// A category for this FF can be already set (this is the case when a new FC/FM is found
// but the FF has already been associated to a category)
// In practice a new category has to be defined only if the FF is unknown.
Category[] categories = categoryDAO.findAllCategories();
Set<Category> categoriesSet=new HashSet<Category>();
for (Category cat: categories) {
if (cat.containsAlarm(alarm)) {
categoriesSet.add(cat);
}
}
if (!categoriesSet.isEmpty()) {
System.out.println("Alarm "+alarm.getAlarmId()+" already associated to categories");
} else {
System.out.println("Alarm "+alarm.getAlarmId()+" has no associated category");
Category root=categoryDAO.findCategoryByPath("ROOT");
categoriesSet.add(root);
alarm.setCategories(categoriesSet);
root.addAlarm(alarm);
}
return alarm;
}
}