/** * Copyright (c) <2013> <Radware Ltd.> and others. All rights reserved. * * This program and the accompanying materials are made available under the terms of the Eclipse Public License * v1.0 which accompanies this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * @author Gera Goft * @version 0.1 */ package org.opendaylight.defense4all.framework.core.impl; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import org.opendaylight.defense4all.framework.core.Asserter; import org.opendaylight.defense4all.framework.core.EM; import org.opendaylight.defense4all.framework.core.ExceptionControlApp; import org.opendaylight.defense4all.framework.core.ExceptionEntityExists; import org.opendaylight.defense4all.framework.core.HealthTracker; import org.opendaylight.defense4all.framework.core.RepoCD; import org.opendaylight.defense4all.framework.core.RepoFactory; import org.opendaylight.defense4all.framework.core.SerializersSerializer; import org.opendaylight.defense4all.framework.core.FrameworkMain.ResetLevel; import me.prettyprint.cassandra.service.CassandraHostConfigurator; import me.prettyprint.cassandra.service.ThriftKsDef; import me.prettyprint.hector.api.Cluster; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.Serializer; import me.prettyprint.hector.api.ddl.ColumnFamilyDefinition; import me.prettyprint.hector.api.ddl.ComparatorType; import me.prettyprint.hector.api.ddl.KeyspaceDefinition; import me.prettyprint.hector.api.exceptions.HectorException; import me.prettyprint.hector.api.factory.HFactory; public class RepoFactoryImpl extends FrameworkModule implements RepoFactory { // Local Cassandra server connectivity protected static final String LOCAL_HOST = "localhost"; protected short cassandraServerPort; // Port number on which local Cassandra server listens for client requests // Cassandra cluster protected Cluster ctrlAppsCluster; // Hector object representing the Cassandra cluster protected String clusterName; // Hector-Cassandra DB (keyspace) protected String dbName; // Cassandra DB (keyspace) name for this app. Multiple DBs (for multiple control apps) can coexist in a cassandra cluster protected KeyspaceDefinition ctrlAppsKSDef; protected Keyspace ctrlAppsKS; // Cassandra DB protected int ctrlAppsKSReplLevel; /** * Name space allocation of Repo factory REPO minor IDs (Repo factory itself places its state in these REPOs) */ public enum RepoMinor { INVALID("RFactoryInvalid"), REPO_DESCRIPTIONS("RepoDescriptions"), EM_DESCRIPTIONS("EMDescriptions"); private String value; private RepoMinor(String value) {this.value = value;} public String val() {return value;} } // All allocated Repo and EM objects to ensure singleton allocation for each Repo or EM description protected Hashtable<String,RepoImpl<?>> repos = null; protected Hashtable<String, EM> eMs = null; /* Constructor for Spring */ public RepoFactoryImpl() { super(); eMs = new Hashtable<String, EM>(); repos = new Hashtable<String, RepoImpl<?>>(); } // Setters for Spring public void setClusterName(String clusterName) {this.clusterName = clusterName;} public void setDbName(String dbName) {this.dbName = dbName;} public void setCassandraServerPort(short port) {cassandraServerPort = port;} public void setCtrlAppsKSReplLevel(int ctrlAppsKSReplLevel) {this.ctrlAppsKSReplLevel = ctrlAppsKSReplLevel;} protected static String aggregateRepoName(String repoMajor, String repoMinor) { return (repoMajor + "_" + repoMinor); } @Override public void init() throws ExceptionControlApp { super.init(); try { // Connect to the cassandra cluster. We assume the local machine runs a Cassandra cluster member. // We connect to it and ask it to discover all other Cassandra instances in this cluster CassandraHostConfigurator hostConfigurator = new CassandraHostConfigurator(LOCAL_HOST + ":" + cassandraServerPort); // comment out for single host installation //hostConfigurator.setAutoDiscoverHosts(true); ctrlAppsCluster = HFactory.getOrCreateCluster(clusterName, hostConfigurator); // get or create the DB (keyspace), with default (simple) replication strategy ctrlAppsKSDef = ctrlAppsCluster.describeKeyspace(dbName); if (ctrlAppsKSDef == null) { // not created yet ArrayList<ColumnFamilyDefinition> emptyArr = new ArrayList<ColumnFamilyDefinition>(); ctrlAppsKSDef = HFactory.createKeyspaceDefinition(dbName, ThriftKsDef.DEF_STRATEGY_CLASS, ctrlAppsKSReplLevel, emptyArr); ctrlAppsCluster.addKeyspace(ctrlAppsKSDef); } // get a Hector object that represents the DB (keyspace). This method does NOT create a new keyspace in Cassandra! ctrlAppsKS = HFactory.createKeyspace(dbName, ctrlAppsCluster); } catch (Throwable e) { log.error("Failed to initialize RepoFactory." + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to initialize RepoFactory.", e); } } @Override public void finit() { try { // Flush all Entity Managers Iterator<Map.Entry<String,EM>> emIter = eMs.entrySet().iterator(); EM em; while(emIter.hasNext()) { em = emIter.next().getValue(); em.flush(); // Today HOM entity manager does nothing, but as a preparation for future... } // Flush all repos Iterator<Map.Entry<String, RepoImpl<?>>> repoIter = repos.entrySet().iterator(); RepoImpl<?> repo; while(repoIter.hasNext()) { repo = repoIter.next().getValue(); repo.finit(); } super.finit(); } catch (Throwable e) { log.error("Failed to flush entity managers or repos." + e.getLocalizedMessage()); } } @Override public void reset(ResetLevel resetLevel) throws ExceptionControlApp { super.reset(resetLevel); if(resetLevel == ResetLevel.factory) { try { ctrlAppsCluster.dropKeyspace(dbName); // Remove all tables in this DB repos.clear(); } catch (HectorException e) { log.error("Failed to remove db " + dbName + ". "+ e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to remove db " + dbName, e); } } } /** * Get EM instance to persist objects of classes in the provided class paths. * The same entity manager is returned per all requests of an entity manager for a given stateClassPaths * RepoFactoryImpl will scan the classpath looking for all classes annotated with "@Entity" and "@Table(name=repoName)". * Then a request to persist an object to or load an object from the repo "repoName" will allow RepoFactoryImpl to map * the object into the appropriate repo (according to its class). Provided class paths are package names beneath * /src/main/java - e.g., a class path can be "mypackagewithstate". * There are two primary requests to be invoked against the entity manager object * Persist request: "entityManager.persist(stateObject);" The object will be persisted in a repo named as the object class, * in a row whose key is the key field value in the stateObject. * Find request: "stateObject = mEntityManager.find(StateObjectClass.class, rowKey);" The row number "rowKey" will be loaded * from the repo named as StateObjectClass.class, and populated into stateObject. * @param emId Id (name) of the requested entity manager * @return return Entity manager through which state objects can be persisted and loaded from corresponding repo. * Null if failed to retrieve or instantiate it. * @throws ExceptionControlApp */ @Override public EM getEM(String emId) throws IllegalArgumentException, ExceptionControlApp { Asserter.assertNonEmptyStringParam(emId, "emId", log); EM em = eMs.get(emId); if(em != null) return em; EMDescription emDescription; try { emDescription = fMainImpl.frameworkEM.find(EMDescription.class, emId); } catch (Throwable e) { log.error("Failed to find entity manager." + emId + ". " + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to find entity manager." + emId + ". ", e); } if (emDescription == null) return null; // The user has not yet introduced the EM description try { em = EMImpl.getEM(fMainImpl, this, ctrlAppsKS, emDescription); // Create the EM instance using its emDescription } catch (Throwable e) { log.error("Failed to create EMImpl." + emId + ". " + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to create EMImpl." + emId + ". ", e); } eMs.put(emId, em); return em; } /** * Get or create EM instance to persist objects of classes in the provided class paths. * Once instantiated, the same entity manager instance is returned per all requests of an entity manager for a given * stateClassPaths. If not yet instantiated, check if its definition has already been provided in a previous lifecycle. * If so use it. If not, record the definition passed in as the parameter, and use it to instantiate the EntityManager * for this lifecycle. * @param emId Id (name) of the requested entity manager * @param stateClassPaths Class paths containing classes of objects to be persisted, delimited by colon (":"), * for example: class.path1:classpath2:also.class.path3 * @return return Entity manager through which state objects can be persisted and loaded from corresponding repo. If * failed * @throws IllegalArgumentException * @throws ExceptionControlApp */ @Override public EM getOrCreateEM(String emId, String stateClassPaths) throws IllegalArgumentException, ExceptionControlApp { Asserter.assertNonEmptyStringParam(emId, "emId", log); Asserter.assertNonEmptyStringParam(stateClassPaths, "stateClassPaths", log); EM em = eMs.get(emId); if (em != null) return em; // It has already been instantiated in this lifecycle. // Need to instantiate the requested EntityManager. If its description has already been defined in a previous lifecycle // use it, otherwise create the EMDescription, record it and use the created one to create an EntityManager instance. EMDescription emDesc; try { emDesc = fMainImpl.frameworkEM.find(EMDescription.class, emId); } catch (Throwable e) { log.error("Failed to find entity manager." + emId + ", " + stateClassPaths + ". " + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.MODERATE_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to find entity manager." + emId + ". ", e); } if (emDesc == null) { // Need to create and record the description try { emDesc = new EMDescription(emId, stateClassPaths); // Create the EMDescription fMainImpl.frameworkEM.persist(emDesc); } catch (Throwable e) { log.error("Failed to create or persist EMDescription."+emId+", "+stateClassPaths+". "+e.getLocalizedMessage()); throw new ExceptionControlApp("Failed to create or persist EMDescription." + emId + ". ", e); } } try { em = EMImpl.getEM(fMainImpl, this, ctrlAppsKS, emDesc); // Create EM instance using its emDesc } catch (Exception e) { log.error("Failed to instantiate EMImpl."+emDesc+", "+stateClassPaths+". "+e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to instantiate EMImpl."+emDesc+", "+stateClassPaths+". ", e); } eMs.put(emId, em); return em; } /** * Create an EM instance to persist objects of classes in the provided class paths. Once instantiated, * the same entity manager instance is returned per all requests of an entity manager for a given stateClassPaths. * If not yet instantiated, create its EMDescription and use it to instantiate the EntityManager for this lifecycle. * This method does not record the EMDescription in Cassandra (cannot use self to instantiate self). * @param emId Id (name) of the requested entity manager * @param stateClassPaths Class paths containing classes of objects to be persisted, delimited by colon (":"), * for example: class.path1:classpath2:also.class.path3 * @return return Entity manager through through which state objects can be persisted and loaded from corresponding repo * @throws IllegalArgumentException * @throws ExceptionControlApp */ @Override public EM createFrameworkMainEM(String emId, String stateClassPaths) throws IllegalArgumentException, ExceptionControlApp { Asserter.assertNonEmptyStringParam(emId, "emId", log); Asserter.assertNonEmptyStringParam(stateClassPaths, "stateClassPaths", log); /* If corresponding column family has not yet been created in Cassandra - do it now. Column names are strings (UTF8TYPE). * Need to check for EM_DESCRIPTIONS repo and REPO_DESCRIPTIONS repo. */ EM em; try { ColumnFamilyDefinition columnFamilyDefinition; String fEMDsRepoName = FrameworkMainImpl.RepoMajor.FWORK_REPO_FACTORY.name() + "_" + RepoFactoryImpl.RepoMinor.EM_DESCRIPTIONS.val(); if(!columnFamilyExists(fEMDsRepoName)) { columnFamilyDefinition = HFactory.createColumnFamilyDefinition(dbName, fEMDsRepoName, ComparatorType.UTF8TYPE); ctrlAppsCluster.addColumnFamily(columnFamilyDefinition); } String fREPODsRepoName = FrameworkMainImpl.RepoMajor.FWORK_REPO_FACTORY.name()+"_"+RepoFactoryImpl.RepoMinor.REPO_DESCRIPTIONS.val(); if(!columnFamilyExists(fREPODsRepoName)) { columnFamilyDefinition = HFactory.createColumnFamilyDefinition(dbName,fREPODsRepoName,ComparatorType.UTF8TYPE); ctrlAppsCluster.addColumnFamily(columnFamilyDefinition); } EMDescription emDescription = new EMDescription(emId, stateClassPaths); // Create the EMDescription em = EMImpl.getEM(fMainImpl, this, ctrlAppsKS, emDescription); eMs.put(emId, em); } catch (Throwable e) { log.error("Failed to create frameworkMainEM."+emId+", "+stateClassPaths+". "+e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to instantiate EMImpl."+emId+", "+stateClassPaths+". ", e); } return em; } /** * Get an object representing an already created repo * @param RepoNameMajor a name uniquely identifying the component using this repo. * For framework or application global repos the component is the framework or the application respectively. * @param RepoNameMinor a name uniquely identifying the repo among other repos the component uses. * @return requested repo object, or null if this repo has not yet been created * @throws ExceptionControlApp */ @SuppressWarnings("rawtypes") @Override public RepoImpl<?> getRepo(String RepoNameMajor, String RepoNameMinor) throws IllegalArgumentException, ExceptionControlApp { Asserter.assertNonEmptyStringParam(RepoNameMajor, "RepoNameMajor", log); Asserter.assertNonEmptyStringParam(RepoNameMinor, "RepoNameMinor", log); String repoName = aggregateRepoName(RepoNameMajor, RepoNameMinor); RepoImpl<?> repo = repos.get(repoName); if (repo != null) // Java object for this repo has already been instantiated in this lifecycle. Reuse it as singleton. return repo; if (!columnFamilyExists(repoName)) return null; // The repo has not been created yet /* Java repo object has not been instantiated yet in this lifecycle */ RepoDescription repoDescription; try { repoDescription = fMainImpl.frameworkEM.find(RepoDescription.class, repoName); } catch (Exception e) { log.error("Failed to find repo description for " + repoName + ". " + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to find repo description for " + repoName + ". ", e); } if (repoDescription == null) return null; repo = new RepoImpl(fMainImpl, repoDescription); // Create the repo java object representing the CF created in Cassandra repos.put(repoName, repo); // and add it to the list of repos return repo; } /** * Create a repo and return an object representing it * @param RepoNameMajor a name uniquely identifying the component using this repo. * For framework or application global repos the component is the framework or the application respectively. * @param RepoNameMinor a name uniquely identifying the repo among other repos the component uses. * @param repoColumnDescriptions a set of column descriptions containing column characteristics, such as type of serialization and validation. * @return requested repo object * @throws ExceptionControlApp If there is an inconsistency between internal RepoFactoryImpl data structures * @throws ExceptionEntityExists If the repo already exists */ @Override public <K> RepoImpl<K> createRepo(String RepoNameMajor, String RepoNameMinor, Serializer<K> keySerializer, boolean immediateFlush, List<RepoCD> columnDescriptions ) throws IllegalArgumentException, ExceptionEntityExists, ExceptionControlApp { Asserter.assertNonEmptyStringParam(RepoNameMajor, "RepoNameMajor", log); Asserter.assertNonEmptyStringParam(RepoNameMinor, "RepoNameMinor", log); Asserter.assertNonNullObjectParam(keySerializer, "keySerializer", log); String repoName = aggregateRepoName(RepoNameMajor, RepoNameMinor); /* If the repo description already exists in cassandra - use it. Otherwise create and persist one to repoDescriptions repo. */ RepoDescription repoDesc; try { repoDesc = fMainImpl.frameworkEM.find(RepoDescription.class, repoName); } catch (Exception e) { log.error("Failed to find repo description for " + repoName + ". " + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to find repo description for " + repoName + ". ", e); } if(repoDesc == null) { SerializersSerializer sSerializer = SerializersSerializer.getInstance(); repoDesc = new RepoDescription(repoName, sSerializer.toString(keySerializer),immediateFlush,columnDescriptions); try { fMainImpl.frameworkEM.persist(repoDesc); } catch (Exception e) { log.error("Failed to persist repo description for " + repoName + ". " + e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.MODERATE_HEALTH_ISSUE); throw new ExceptionControlApp("Failed to persist repo description for " + repoName + ". ", e); } } // Create the repo java object representing the column family created in cassandra, and add it to the list of repos RepoImpl<K> repo; try { // If not yet created, create the column family in Cassandra for repo "repoName", with column types string (UTF8TYPE). if (!columnFamilyExists(repoName)) { ColumnFamilyDefinition columnFamilyDefinition = HFactory.createColumnFamilyDefinition(dbName, repoName, ComparatorType.UTF8TYPE); ctrlAppsCluster.addColumnFamily(columnFamilyDefinition); } repo = new RepoImpl<K>(fMainImpl, repoDesc); repos.put(repoName, repo); } catch (HectorException e) { log.error("Failed to create CF in cassandra or instantiate RepoImpl for"+repoName+". "+e.getLocalizedMessage()); fMainImpl.healthTrackerImpl.reportHealthIssue(HealthTracker.SIGNIFICANT_HEALTH_ISSUE); //fMainImpl.frImpl.logRecord(FrameworkMain.FR_FRAMEWORK_FAILURE,"Failed to create Repo "+repoDesc.repoName); throw new ExceptionControlApp("Failed to create CF in cassandra or instantiate RepoImpl for"+repoName+". ",e); } return repo; } /** * Create a repo and return an object representing it * @param RepoNameMajor a name uniquely identifying the component using this repo. * For framework or application global repos the component is the framework or the application respectively. * @param RepoNameMinor a name uniquely identifying the repo among other repos the component uses. * @param repoColumnDescriptions a set of column descriptions containing column characteristics, such as type of serialization and validation. * @return requested repo object * @throws ExceptionControlApp If there is an inconsistency between internal RepoFactoryImpl data structures * @throws ExceptionEntityExists If the repo already exists */ public <K> RepoImpl<K> getCreateTestRepo(String repoName, Serializer<K> keySerializer, boolean immediateFlush, List<RepoCD> columnDescs ) throws Exception { SerializersSerializer sSer = SerializersSerializer.getInstance(); RepoDescription repoDesc = new RepoDescription(repoName, sSer.toString(keySerializer), immediateFlush, columnDescs); // If not yet created, create the column family in Cassandra for repo "repoName", with column types string (UTF8TYPE). if (!columnFamilyExists(repoName)) { ColumnFamilyDefinition cfDef = HFactory.createColumnFamilyDefinition(dbName, repoName, ComparatorType.UTF8TYPE); ctrlAppsCluster.addColumnFamily(cfDef); } // Create the repo java object representing the column family created in cassandra, and add it to the list of repos RepoImpl<K> repo = new RepoImpl<K>(this, repoDesc); repos.put(repoName, repo); return repo; } /** * Get a Java object representing a repo. Create the repo if non-existent. Create the java object if has not been * instantiated yet in this life-cycle. * @param RepoNameMajor a name uniquely identifying the component using this repo. * For framework or application global repos the component is the framework or the application respectively. * @param RepoNameMinor a name uniquely identifying the repo among other repos the component uses. * @param repoColumnDescriptions a set of column descriptions containing column characteristics, such as type of serialization and validation. * @return requested repo object * @throws ExceptionControlApp If there is an inconsistency between internal RepoFactoryImpl data structures * @throws ExceptionEntityExists If the repo already exists */ @Override public <K> RepoImpl<K> getOrCreateRepo(String RepoNameMajor, String RepoNameMinor, Serializer<K> keySerializer, boolean immediateFlush, List<RepoCD> columnDescriptions ) throws IllegalArgumentException, ExceptionControlApp { Asserter.assertNonEmptyStringParam(RepoNameMajor, "RepoNameMajor", log); Asserter.assertNonEmptyStringParam(RepoNameMinor, "RepoNameMinor", log); Asserter.assertNonNullObjectParam(keySerializer, "keySerializer", log); @SuppressWarnings("unchecked") RepoImpl<K> repo = (RepoImpl<K>) getRepo(RepoNameMajor, RepoNameMinor); if (repo == null) try { repo = (RepoImpl<K>) createRepo(RepoNameMajor,RepoNameMinor,keySerializer,immediateFlush,columnDescriptions); } catch (ExceptionEntityExists e) {/* Should not happen after failed get*/} return repo; } protected boolean columnFamilyExists(String cfName) { if(cfName == null) return false; Iterator<ColumnFamilyDefinition> iter = ctrlAppsKSDef.getCfDefs().iterator(); ColumnFamilyDefinition cfDef; while(iter.hasNext()) { cfDef = iter.next(); if(cfDef.getName().equals(cfName)) return true; } return false; } /** * The table needs to be declared prior to persisting or finding the annotated state class, because * the corresponding Column Family needs to be created in Cassandra in advance. This method is idempotent. * @param tableName * @throws ExceptionControlApp */ public void declareAnnotationTable(String tableName) throws ExceptionControlApp { // If corresponding column family has not been created yet in Cassandra - do it now. Column names are strings (UTF8TYPE). if(!columnFamilyExists(tableName)) { ColumnFamilyDefinition columnFamilyDefinition = HFactory.createColumnFamilyDefinition(dbName, tableName, ComparatorType.UTF8TYPE); try { ctrlAppsCluster.addColumnFamily(columnFamilyDefinition); } catch (Throwable e) { log.error("Failed to add CF for " + tableName+". " + e.getLocalizedMessage()); throw new ExceptionControlApp("Failed to add CF for " + tableName+". ",e); } } } @Override protected void actionSwitcher(int actionCode, Object param) { // TODO: check if decoupled execution is needed } }