/* ==================================================================== * Limited Evaluation License: * * This software is open source, but licensed. The license with this package * is an evaluation license, which may not be used for productive systems. If * you want a full license, please contact us. * * The exclusive owner of this work is the OpenRate project. * This work, including all associated documents and components * is Copyright of the OpenRate project 2006-2015. * * The following restrictions apply unless they are expressly relaxed in a * contractual agreement between the license holder or one of its officially * assigned agents and you or your organisation: * * 1) This work may not be disclosed, either in full or in part, in any form * electronic or physical, to any third party. This includes both in the * form of source code and compiled modules. * 2) This work contains trade secrets in the form of architecture, algorithms * methods and technologies. These trade secrets may not be disclosed to * third parties in any form, either directly or in summary or paraphrased * form, nor may these trade secrets be used to construct products of a * similar or competing nature either by you or third parties. * 3) This work may not be included in full or in part in any application. * 4) You may not remove or alter any proprietary legends or notices contained * in or on this work. * 5) This software may not be reverse-engineered or otherwise decompiled, if * you received this work in a compiled form. * 6) This work is licensed, not sold. Possession of this software does not * imply or grant any right to you. * 7) You agree to disclose any changes to this work to the copyright holder * and that the copyright holder may include any such changes at its own * discretion into the work * 8) You agree not to derive other works from the trade secrets in this work, * and that any such derivation may make you liable to pay damages to the * copyright holder * 9) You agree to use this software exclusively for evaluation purposes, and * that you shall not use this software to derive commercial profit or * support your business or personal activities. * * This software is provided "as is" and any expressed or impled warranties, * including, but not limited to, the impled warranties of merchantability * and fitness for a particular purpose are disclaimed. In no event shall * The OpenRate Project or its officially assigned agents be liable to any * direct, indirect, incidental, special, exemplary, or consequential damages * (including but not limited to, procurement of substitute goods or services; * Loss of use, data, or profits; or any business interruption) however caused * and on theory of liability, whether in contract, strict liability, or tort * (including negligence or otherwise) arising in any way out of the use of * this software, even if advised of the possibility of such damage. * This software contains portions by The Apache Software Foundation, Robert * Half International. * ==================================================================== */ package OpenRate.cache; import OpenRate.CommonConfig; import OpenRate.OpenRate; import OpenRate.configurationmanager.ClientManager; import OpenRate.db.DBUtil; import OpenRate.exception.InitializationException; import OpenRate.lang.ProductList; import OpenRate.logging.LogUtil; import OpenRate.utils.ConversionUtils; import OpenRate.utils.PropertyUtils; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.text.ParseException; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; /** * This class implements a cache of customer information for use in simple * JBilling rating cases. It uses the Item Description field which is * parsed to extract the information out of the jbilling database. * * The data should be recovered from the database like this: * - OrderId (integer) * - CustId (integer) * - OrderLineId (integer) * - Description (string) * - StartDate (yyyy-MM-dd HH:mm:ss.S) * - EndDate (yyyy-MM-dd HH:mm:ss.S) * - Quantity (integer) * * The "invalidate on duplicate" configuration will remove a whole alias key * from the cache in the case that a duplicate is found. This will mean that all * rating for the alias key will be rejected until the contention is removed. */ public abstract class JBCustomerCache extends AbstractSyncLoaderCache { // Used to allow alias maps - takes an alias and maps to a poid. private ConcurrentHashMap<String, validityNode> aliasCache; // The CustIDCache holds the aliases for the account private ConcurrentHashMap<Integer, CustInfo> custIDCache; // Conversion cache private ConversionUtils conv = new ConversionUtils(); /** * these are the statements that we have to prepare to be able to get records * once and only once */ protected String CustomerDataSelectQuery; /** * these are the prepared statements */ protected PreparedStatement StmtCustomerDataSelectQuery; /** * The internal date format is used to translate the dates from the * database into internal readable date formats */ protected String internalDateFormat = "yyyy-MM-dd HH:mm:ss.S"; // Setting if we invalidate existing value if a duplicate is found private boolean invalidateOnDuplicate = false; // List of Services that this Client supports private final static String SERVICE_INVALIDATE_DUPLICATE = "InvalidateDuplicates"; private final static String SERVICE_DATE_FORMAT = "DateFormat"; /** * The CustInfo structure holds the information about the customer account, * including the validity dates, the product list and the balance group * reference. Note that we are using the dates as long integers to reduce * the total amount of storage that is required. */ private class CustInfo { private long UTCValidFrom; private long UTCValidTo; private ArrayList<CustProductInfo> CPI = null; private int productCount = 0; private int balanceGroup = 0; } /** * The CustProductInfo structure holds the information about the products the, * customer has, including the validity dates. Note that we are using long integers * for the dates to reduce storage requirements. */ private class CustProductInfo { private String ProductID=null; private String Service=null; private String Subscription=null; private long UTCValidFrom; private long UTCValidTo; private int OrderId; private int OrderLineId; private int Quantity; } /** * A ValidityNode is a segment of validity of a resource. These are chained * together in a sorted linked list. The sorting is done at insertion time * into the list, meaning that lookups at run time can be optimised. */ private class validityNode { long ID; // Required for managing updates long TimeFrom; long TimeTo; Integer Result = 0; String SubId = ""; validityNode child = null; } /** Constructor * Creates a new instance of the Customer Cache. The Cache * contains all of the Customer IDs that have been cached. */ public JBCustomerCache() { super(); custIDCache = new ConcurrentHashMap<>(5000); aliasCache = new ConcurrentHashMap<>(5000); } // ----------------------------------------------------------------------------- // -------------------- Start of custom Plug In functions ---------------------- // ----------------------------------------------------------------------------- /** * Recover a customer ID from the cache using the alias. * * @param custID The custID to lookup * @return True if we know the customer ID */ public boolean getCustIdExists(int custID) { return custIDCache.containsKey(custID); } /** * Recover a customer ID from the cache using the alias. * * @param alias The alias to lookup * @param CDRDate The date to get the alias for * @return The internal customer ID */ public Integer getCustId(String alias, long CDRDate) { Integer custID = null; validityNode tmpValidityNode; // See if we already have AuditSegID for this customer if (aliasCache.containsKey(alias)) { // get the start of the search tree tmpValidityNode = aliasCache.get(alias); // Now that we have the Validity Map, get the entry while (tmpValidityNode != null) { if ((tmpValidityNode.TimeFrom <= CDRDate) & (tmpValidityNode.TimeTo > CDRDate)) { custID = tmpValidityNode.Result; break; } // Move down the map tmpValidityNode = tmpValidityNode.child; } } // return the id return custID; } /** * Get the products that are attached to the customer account, using the * alias to locate the account * * @param alias The alias to the customer account * @param service The service * @param subscription The subscription to get products for * @param CDRDate The date to retrieve the products for * @return The product list */ public ProductList getProducts(String alias, String service, String subscription,long CDRDate) { int custID; // See if we already have ID for this customer if (aliasCache.containsKey(alias)) { // Get the poid from the Alias custID = getCustId(alias, CDRDate); // Get the products return getProducts(custID, service, subscription, CDRDate); } else { // We could find no alias OpenRate.getOpenRateFrameworkLog().error("Alias <" + alias + "> not found. Lookup failed."); // Just return null return null; } } /** * Get the products that are attached to the customer account, using the * alias to locate the account * * @param custID The customer ID we want to retireve for * @param service The service * @param subscription The subscription to get products for * @param CDRDate The date to retrieve the products for * @return The product list */ public ProductList getProducts(int custID, String service, String subscription,long CDRDate) { ProductList tmpProductList; CustInfo tmpCustInfo; CustProductInfo tmpCPI; boolean firstProduct = true; // Prepare the result tmpProductList = new ProductList(); // Get the product information tmpCustInfo = custIDCache.get(custID); // See if the CDR is within the period of validity if (tmpCustInfo != null) { if ( tmpCustInfo.UTCValidFrom <= CDRDate ) { if (tmpCustInfo.UTCValidTo > CDRDate) { // We have validity, get back the product list for ( int i = 0 ; i < tmpCustInfo.productCount ; i ++ ) { tmpCPI = tmpCustInfo.CPI.get(i); if ((tmpCPI.Service.equals(service)) & (tmpCPI.Subscription.equals(subscription))) { if ( tmpCPI.UTCValidFrom <= CDRDate ) { if ( tmpCPI.UTCValidTo > CDRDate ) { if (firstProduct) { tmpProductList.addProduct(0,tmpCPI.ProductID,null,tmpCPI.Service,tmpCPI.UTCValidFrom,tmpCPI.UTCValidTo,tmpCPI.Quantity); firstProduct = false; } else { tmpProductList.addProduct(0,tmpCPI.ProductID,null,tmpCPI.Service,tmpCPI.UTCValidFrom,tmpCPI.UTCValidTo,tmpCPI.Quantity); } } } } } } tmpProductList.setBalanceGroup(tmpCustInfo.balanceGroup); return tmpProductList; } } return null; } /** * Return the value of the balance group so that we are able to update * it during the logic processing. This is to make sure that we have all * the information necessary when we start the calculation. * * @param CustId The customer ID * @return The balance group */ public int getBalanceGroup(int CustId) { CustInfo tmpCustInfo; // See if we already have ID for this customer if (custIDCache.containsKey(CustId)) { // Get the product information tmpCustInfo = custIDCache.get(CustId); return tmpCustInfo.balanceGroup; } else { // Otherwise write an error and ignore it OpenRate.getOpenRateFrameworkLog().error("Customer ID <" + CustId + "> not found. Lookup failed."); } return 0; } // ----------------------------------------------------------------------------- // -------------------- Start of data loading functions ------------------------ // ----------------------------------------------------------------------------- /** * loadCache is called automatically on startup of the * cache factory, as a result of implementing the CacheLoader * interface. This should be used to load any data that needs loading, and * to set up variables. * * @param ResourceName The name of the resource to load for * @param CacheName The name of the cache to load for * @throws InitializationException */ @Override public void loadCache(String ResourceName, String CacheName) throws InitializationException { String tmpHelper; // load the value of the invalidate duplicates setting tmpHelper = PropertyUtils.getPropertyUtils().getDataCachePropertyValueDef(ResourceName, CacheName, SERVICE_INVALIDATE_DUPLICATE, "False"); // process it processControlEvent(SERVICE_INVALIDATE_DUPLICATE,true,tmpHelper); // load the value of the invalidate duplicates setting tmpHelper = PropertyUtils.getPropertyUtils().getDataCachePropertyValueDef(ResourceName, CacheName, SERVICE_DATE_FORMAT, internalDateFormat); // Initialise the conversion object conv.setInputDateFormat(tmpHelper); // Do the parent processing super.loadCache(ResourceName, CacheName); } /** * load the data from a file - not supported for this cache. */ @Override public void loadDataFromFile() throws InitializationException { throw new InitializationException("File loading not supported",getSymbolicName()); } /** * Load the data from the defined Data Source DB */ @Override public void loadDataFromDB() throws InitializationException { int custId; String orderId; String orderLineId; String alias; String description; String prodName; String service; String subscription; int custLoaded = 0; String tmpStartDate; String tmpEndDate; long startDate = 0; long endDate = 0; String tmpQuantity; int quantity; // Log that we are starting the loading OpenRate.getOpenRateFrameworkLog().info("Starting Customer Cache Loading from DB for <" + getSymbolicName() + ">"); // Try to open the DS JDBCcon = DBUtil.getConnection(cacheDataSourceName); // Now prepare the statements prepareStatements(); // Execute the query try { mrs = StmtCustomerDataSelectQuery.executeQuery(); } catch (SQLException ex) { message = "Error performing SQL for retieving Customer data. message <" + ex.getMessage() + ">"; throw new InitializationException(message,ex,getSymbolicName()); } // loop through the results for the customer alias cache try { mrs.beforeFirst(); while (mrs.next()) { custLoaded++; orderId = mrs.getString(1); custId = mrs.getInt(2); orderLineId = mrs.getString(3); description = mrs.getString(4); tmpStartDate = mrs.getString(5); tmpEndDate = mrs.getString(6); tmpQuantity = mrs.getString(7); // parse the description prodName = getProduct(description); alias = getAlias(description); service = getService(description); subscription = getSubscription(description); if ((prodName == null) | (alias == null) | (service == null) | (subscription == null)) { OpenRate.getOpenRateFrameworkLog().warning("Record <" + description + "> skipped for customer <" + custId + "> order <" + orderId + ">." ); continue; } // parse the start date try { if (tmpStartDate == null) { startDate = CommonConfig.LOW_DATE; } else { startDate = conv.convertInputDateToUTC(tmpStartDate); } } catch (ParseException ex) { OpenRate.getOpenRateFrameworkLog().error("Start Date format for record <" + custLoaded + "> are not correct. Date <" + tmpStartDate + ">, format <" + conv.getInputDateFormat() + "> order <" + orderId + ">. Data discarded." ); } // parse the end date try { if (tmpEndDate == null) { endDate = CommonConfig.HIGH_DATE; } else { endDate = conv.convertInputDateToUTC(tmpEndDate); } } catch (ParseException ex) { OpenRate.getOpenRateFrameworkLog().error("End Date format for record <" + custLoaded + "> are not correct. Date <" + tmpEndDate + ">, format <" + conv.getInputDateFormat() + "> order <" + orderId + ">. Data discarded." ); } // parse the Quantity quantity = Integer.parseInt(tmpQuantity); // print the information to the log OpenRate.getOpenRateFrameworkLog().info("Adding service ID <" + alias + "> to account <" + custId + "> with product <" + prodName + "> validity <" + tmpStartDate + " (" + startDate + ") - " + tmpEndDate + " (" + endDate + ")>, Qty: <" + quantity + "> order <" + orderId + ">."); // Add the map addAlias(alias,custId,startDate,endDate); addCustId(custId,CommonConfig.LOW_DATE,CommonConfig.HIGH_DATE,0); addCPI(custId, service, subscription, prodName, startDate, endDate, Integer.parseInt(orderId), Integer.parseInt(orderLineId),quantity); } } catch (SQLException ex) { message = "Error opening Customer Data for <" + cacheDataSourceName + ">"; throw new InitializationException(message,ex,getSymbolicName()); } // Close down stuff try { mrs.close(); StmtCustomerDataSelectQuery.close(); JDBCcon.close(); } catch (SQLException ex) { message = "Error closing Result Set for Customer information from <" + cacheDataSourceName + ">"; throw new InitializationException(message,ex,getSymbolicName()); } OpenRate.getOpenRateFrameworkLog().info( "Customer Cache Data Loading completed from <" + cacheDataSourceName + ">"); OpenRate.getOpenRateFrameworkLog().info("Products Loaded: " + custLoaded); } /** * Load the data from the defined Data Source Method */ @Override public void loadDataFromMethod() throws InitializationException { throw new InitializationException("Not implemented yet",getSymbolicName()); } /** * Reset the cache */ @Override public void clearCacheObjects() { custIDCache.clear(); aliasCache.clear(); } /** * Add an alias to the customer cache. An alias is a representation of any * identifier that can be used to locate the account. Note that we exclude * the last second of the validity period to avoid issues with overlapping * periods that end and start on the same second (e.g. old validity ends on * 1st Sept @ 00:00 and the new one starts on 1st Sept @ 00:00) * * @param alias The identifier that should be used to locate the account * @param custId The account that should be located * @param validFrom The start date of the validity * @param validTo The end date of the validity */ public void addAlias(String alias, int custId,long validFrom,long validTo) { validityNode NewNode; validityNode tmpValidityNode; validityNode tmpValidityNextNode; boolean insertedEntry = false; // Check that the valid to is after the valid from if (validFrom > validTo) { // Otherwise write an error and ignore it OpenRate.getOpenRateFrameworkLog().error("Alias ID <" + alias + "> validity period from <" + validFrom + "> is after validity period to <" + validTo + ">. Ignoring."); return; } // Now add the validity segment into the ArrayList if (!aliasCache.containsKey(alias)) { // We do not know this alias - Create the new ArrayList tmpValidityNode = new validityNode(); //tmpValidityNode.ID = ID; tmpValidityNode.TimeFrom = validFrom; tmpValidityNode.TimeTo = validTo - 1; // Exlude the last second tmpValidityNode.Result = custId; tmpValidityNode.child = null; //NewNode.SubId = subID; // Add in the new node aliasCache.put(alias, tmpValidityNode); // mark that we have done the work insertedEntry = true; } else { // Recover the validity map that there is tmpValidityNode = aliasCache.get(alias); // now run down the validity periods until we find the right position while (tmpValidityNode != null) { tmpValidityNextNode = tmpValidityNode.child; // Search for the place that will accommodate the start of the segment if (validFrom > tmpValidityNode.TimeTo) { if (tmpValidityNextNode == null) { // Insert at the end of the list NewNode = new validityNode(); //tmpValidityNode.ID = ID; tmpValidityNode.child = NewNode; NewNode.TimeFrom = validFrom; NewNode.TimeTo = validTo - 1; // Exclude the last second NewNode.Result = custId; //NewNode.SubId = subID; // mark that we have done the work insertedEntry = true; } else { if (validTo < tmpValidityNextNode.TimeFrom) { // insert into the middle of the list NewNode = new validityNode(); NewNode.child = tmpValidityNode.child; tmpValidityNode.child = NewNode; //tmpValidityNode.ID = ID; NewNode.TimeFrom = validFrom; NewNode.TimeTo = validTo; NewNode.Result = custId; //NewNode.SubId = subID; // mark that we have done the work insertedEntry = true; } } } // Move down the map tmpValidityNode = tmpValidityNode.child; } } // see if we inserted correctly if (!insertedEntry) { if (invalidateOnDuplicate) { // remove the whole key that we couldn't add. aliasCache.remove(alias); OpenRate.getOpenRateFrameworkLog().error("Alias ID <" + alias + "> already exists for time <" + validFrom + "-" + validTo + ">. Removed key."); } else { // Otherwise write an error and ignore it OpenRate.getOpenRateFrameworkLog().error("Alias ID <" + alias + "> already exists for time <" + validFrom + "-" + validTo + ">"); } } } /** * Add a Customer object into the CustomerCache. This is allowed to fail in the * case that we find a duplicate. It is important that we have the customer, * but it does not matter that we have it twice. * * @param custId The customer identifier * @param validFrom Valid from date of the customer relationship * @param validTo Valid to date of the customer relationship * @param balanceGroup The ID of the counter balance group */ public void addCustId(int custId,long validFrom,long validTo,int balanceGroup) { CustInfo tmpCustInfo; // See if we already have ID for this customer if (!custIDCache.containsKey(custId)) { // Create the new entry for the customer ID tmpCustInfo = new CustInfo(); tmpCustInfo.CPI = new ArrayList<>(); tmpCustInfo.UTCValidFrom = validFrom; tmpCustInfo.UTCValidTo = validTo; tmpCustInfo.balanceGroup = balanceGroup; custIDCache.put(custId,tmpCustInfo); } } /** * Add a CPI (CustomerProductInstance) value into the CustomerCache * * @param custId The customer ID to add the product to * @param service The service of the product * @param subscription The subscription ID * @param prodID The product identifier * @param validFrom The start of the product validity * @param validTo The end of the product validity * @param orderId The JBilling order ID * @param orderLineId The JBilling order line ID * @param quantity The order line quantity */ public void addCPI(int custId, String service, String subscription, String prodID, long validFrom, long validTo, int orderId, int orderLineId, int quantity) { CustInfo tmpCustInfo; CustProductInfo tmpCPI; // See if we already have ID for this customer if (custIDCache.containsKey(custId)) { // Create the new entry for the customer ID tmpCustInfo = custIDCache.get(custId); tmpCPI = new CustProductInfo(); tmpCPI.Service = service; tmpCPI.Subscription = subscription; tmpCPI.ProductID = prodID; tmpCPI.UTCValidFrom = validFrom; tmpCPI.UTCValidTo = validTo - 1; // exclude the last second tmpCPI.OrderId = orderId; tmpCPI.OrderLineId = orderLineId; tmpCPI.Quantity = quantity; tmpCustInfo.CPI.add(tmpCustInfo.productCount,tmpCPI); tmpCustInfo.productCount++; } else { // Otherwise write an error and ignore it OpenRate.getOpenRateFrameworkLog().error("Customer ID <" + custId + "> not found. Add CPI failed."); } } // ----------------------------------------------------------------------------- // ---------------- Start of abstract implementation functions ----------------- // ----------------------------------------------------------------------------- /** * Get the Product Name from the descriptor string * * @param descriptionString The string to parse * @return The product name * @throws InitializationException */ public abstract String getProduct(String descriptionString) throws InitializationException; /** * Get the Alias from the descriptor string * * @param descriptionString * @return The alias * @throws InitializationException */ public abstract String getAlias(String descriptionString) throws InitializationException; /** * Get the Service from the descriptor string * * @param descriptionString * @return The Service * @throws InitializationException */ public abstract String getService(String descriptionString) throws InitializationException; /** * Get the Subscription from the descriptor string * * @param descriptionString * @return The Subscription * @throws InitializationException */ public abstract String getSubscription(String descriptionString) throws InitializationException; // ----------------------------------------------------------------------------- // ---------------- Start of data base data layer functions -------------------- // ----------------------------------------------------------------------------- @Override protected boolean getDataStatements(String ResourceName, String CacheName) throws InitializationException { CustomerDataSelectQuery = PropertyUtils.getPropertyUtils().getDataCachePropertyValueDef(ResourceName, CacheName, "CustomerSelectStatement", "None"); if (CustomerDataSelectQuery.equals("None")) { return false; } else { return true; } } /** * PrepareStatements creates the statements from the SQL expressions * so that they can be run as needed. */ @Override protected void prepareStatements() throws InitializationException { try { // prepare the SQL for the TestStatement StmtCustomerDataSelectQuery = JDBCcon.prepareStatement(CustomerDataSelectQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); } catch (SQLException ex) { message = "Error preparing the statement <" + CustomerDataSelectQuery + ">"; throw new InitializationException(message,ex,getSymbolicName()); } } // ----------------------------------------------------------------------------- // ------------- Start of inherited IEventInterface functions ------------------ // ----------------------------------------------------------------------------- /** * registerClientManager registers the client module to the ClientManager class * which manages all the client modules available in this OpenRate Application. * * registerClientManager registers this class as a client of the ECI listener * and publishes the commands that the plug in understands. The listener is * responsible for delivering only these commands to the plug in. * */ @Override public void registerClientManager() throws InitializationException { // Set the client reference and the base services first super.registerClientManager(); //Register services for this Client ClientManager.getClientManager().registerClientService(getSymbolicName(), SERVICE_INVALIDATE_DUPLICATE, ClientManager.PARAM_NONE); } /** * processControlEvent is the method that will be called when an event * is received for a module that has registered itself as a client of the * External Control Interface * * @param Command - command that is understand by the client module * @param Init - we are performing initial configuration if true * @param Parameter - parameter for the command * @return The result string of the operation */ @Override public String processControlEvent(String Command, boolean Init, String Parameter) { int ResultCode = -1; // Set the status of the invalidate duplicates if (Command.equalsIgnoreCase(SERVICE_INVALIDATE_DUPLICATE)) { if (Parameter.equalsIgnoreCase("true")) { // Set the value invalidateOnDuplicate = true; // done ResultCode = 0; } else if (Parameter.equalsIgnoreCase("false")) { // Set the value invalidateOnDuplicate = false; // done ResultCode = 0; } else if (Parameter.equals("")) { // return the current state if (invalidateOnDuplicate) { return "true"; } else { return "false"; } } } if (ResultCode == 0) { OpenRate.getOpenRateFrameworkLog().debug(LogUtil.LogECICacheCommand(getSymbolicName(), Command, Parameter)); return "OK"; } else { return super.processControlEvent(Command,Init,Parameter); } } }