/*
* This file is part of Mockey, a tool for testing application
* interactions over HTTP, with a focus on testing web services,
* specifically web applications that consume XML, JSON, and HTML.
*
* Copyright (C) 2009-2010 Authors:
*
* chad.lafontaine (chad.lafontaine AT gmail DOT com)
* neil.cronin (neil AT rackle DOT com)
* lorin.kobashigawa (lkb AT kgawa DOT com)
* rob.meyer (rob AT bigdis DOT com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.mockey.storage.xml;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.log4j.Logger;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import com.google.common.io.CharStreams;
import com.mockey.model.ProxyServerModel;
import com.mockey.model.Scenario;
import com.mockey.model.Service;
import com.mockey.model.ServicePlan;
import com.mockey.model.ServiceRef;
import com.mockey.model.TwistInfo;
import com.mockey.model.Url;
import com.mockey.storage.IMockeyStorage;
import com.mockey.storage.StorageRegistry;
import com.mockey.ui.ServiceMergeResults;
/**
* Consumes an XML file and configures Mockey services.
*
* <pre>
* + mock_service_definitions.xml
* + mockey_def_depot
* ++ <SERVICE NAME>
* ++ <SERVICE NAME>
* +++ <NAME>.xml
* +++ scenarios
* ++++ 1.xml
* ++++ 2.xml
*
* </pre>
*
* @author Chad.Lafontaine
*
*/
public class MockeyXmlFileManager {
private static Logger logger = Logger.getLogger(MockeyXmlFileManager.class);
private static final char[] VALID_FILE_NAME_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
.toCharArray();
private File basePathFile = new File(System.getProperty("user.dir"));
private static IMockeyStorage store = StorageRegistry.MockeyStorage;
private static MockeyXmlFileManager mockeyXmlFileManagerInstance = null;
public static final String MOCK_SERVICE_DEFINITION = "mock_service_definitions.xml";
protected static final String MOCK_SERVICE_FOLDER = "mockey_def_depot";
protected static final String MOCK_SERVICE_SCENARIO_FOLDER = "scenarios";
public static final String FILESEPERATOR = System.getProperty("file.separator");
/**
* Basic constructor. Will create a folder on the file system to store XML
* definitions.
*
*/
private MockeyXmlFileManager(String path) {
if (path != null && path.trim().length() > 0) {
this.basePathFile = new File(path);
if (!this.basePathFile.exists()) {
this.basePathFile.mkdir();
}
} else {
this.basePathFile = new File(System.getProperty("user.dir"));
}
File fileDepot = new File(this.getBasePathFile(), MOCK_SERVICE_FOLDER);
if (!fileDepot.exists()) {
boolean success = fileDepot.mkdirs();
if (!success) {
logger.fatal("Unable to create a folder called " + MOCK_SERVICE_FOLDER);
} else {
logger.info("Created directory: " + fileDepot.getAbsolutePath());
}
}
}
public static MockeyXmlFileManager getInstance() {
if (MockeyXmlFileManager.mockeyXmlFileManagerInstance == null) {
MockeyXmlFileManager.createInstance(System.getProperty("user.dir"));
}
return MockeyXmlFileManager.mockeyXmlFileManagerInstance;
}
/**
*
* @param path
*/
public static void createInstance(String path) {
MockeyXmlFileManager.mockeyXmlFileManagerInstance = new MockeyXmlFileManager(path);
}
/**
*
* @return location of Mockey definitions.
*/
public File getBasePathFile() {
return this.basePathFile;
}
/**
*
* @param file
* - xml configuration file for Mockey
* @throws IOException
* @throws SAXException
* @throws SAXParseException
*/
public String getFileContentAsString(InputStream fstream) throws IOException, SAXParseException, SAXException {
String inputStreamString = CharStreams.toString(new InputStreamReader(fstream, "UTF-8"));
return inputStreamString;
}
/**
* Loads from default file definition file.
*
* @return results of loading configuration, includes additions and possible
* conflicts.
*
* @throws SAXParseException
* @throws IOException
*/
public ServiceMergeResults loadConfiguration() throws SAXParseException, IOException {
File n = new File(this.getBasePathFile(), MOCK_SERVICE_DEFINITION);
logger.debug("Loading configuration from " + MOCK_SERVICE_DEFINITION);
try {
return loadConfigurationWithXmlDef(getFileContentAsString(new FileInputStream(n)), null);
} catch (SAXException e) {
logger.error("Ouch, unable to parse" + n.getAbsolutePath(), e);
}
return new ServiceMergeResults();
}
/**
*
* @param strXMLDefintion
* @param tagArguments
* @return results (conflicts and additions).
* @throws IOException
* @throws SAXParseException
* @throws SAXException
*/
public ServiceMergeResults loadConfigurationWithXmlDef(String strXMLDefintion, String tagArguments)
throws IOException, SAXParseException, SAXException {
ServiceMergeResults mergeResults = new ServiceMergeResults();
// ***** REMEMBER *****
// Every time a saveOrUpdateXXXX is made, the entire STORE is written to
// the file system.
// If the STORE has many definitions, then each SAVE will loop over
// every file and write.
//
// NOT GOOD FOR PERFORMANCE
//
// Solution: put the store in a temporary transient state
// (memory-mode-only), then revert to original transient setting,
// which could have been in memory-only or write-to-file in the
// first place.
//
// *********************
Boolean originalTransientState = store.getReadOnlyMode();
store.setReadOnlyMode(true);
// STEP #1. CREATE A TEMP STORE
// Read the incoming XML file, and create a new/temporary store for the
// need to ensure current store doesn't get overridden
//
MockeyXmlFileConfigurationReader msfr = new MockeyXmlFileConfigurationReader();
IMockeyStorage mockServiceStoreTemporary = msfr.readDefinition(strXMLDefintion);
// STEP #2. PROXY SETTINGS
// If the proxy settings are _empty_, then set the incoming
// proxy settings. Otherwise, call out a merge conflict.
//
ProxyServerModel proxyServerModel = store.getProxy();
if (proxyServerModel.hasSettings()) {
mergeResults.addConflictMsg("Proxy settings NOT set from incoming file.");
} else {
store.setProxy(mockServiceStoreTemporary.getProxy());
mergeResults.addAdditionMsg("Proxy settings set.");
}
// STEP #3. BUILD SERVICE REFERENCES
// Why is this needed?
// We are adding _new_ services into the Store, and that means that the
// store's state is always changing. We need references as a saved
// snapshot list of store state prior to adding new services.
// **********
// I forget why we really need this though...
// **********
List<Service> serviceListFromRefs = new ArrayList<Service>();
for (ServiceRef serviceRef : mockServiceStoreTemporary.getServiceRefs()) {
try {
String mockServiceDefinition = getFileContentAsString(new FileInputStream(serviceRef.getFileName()));
// HACK:
// I tried to find an easier way to use XML ENTITY and let
// Digester
// to the work of slurping up the XML but was unsuccessful.
// Hence, the brute force.
// YYYYY
//
List<Service> tmpList = msfr.readServiceDefinition(mockServiceDefinition);
for (Service tmpService : tmpList) {
serviceListFromRefs.add(tmpService);
}
} catch (SAXParseException spe) {
logger.error("Unable to parse file of name " + serviceRef.getFileName(), spe);
mergeResults.addConflictMsg("File not parseable: " + serviceRef.getFileName());
} catch (FileNotFoundException fnf) {
logger.error("File not found: " + serviceRef.getFileName());
mergeResults.addConflictMsg("File not found: " + serviceRef.getFileName());
}
}
addServicesToStore(mergeResults, serviceListFromRefs, tagArguments);
// STEP #4. MERGE SERVICES AND SCENARIOS
// Since this gets complicated, logic was moved to it's own method.
mergeResults = addServicesToStore(mergeResults, mockServiceStoreTemporary.getServices(), tagArguments);
// STEP #5. UNIVERSAL RESPONSE SETTINGS
// Important: usage of the temporary-store's Scenario reference
// information is used to set the primary in-memory store. The primary
// store has all the information and the TEMP store only needs to pass
// the references, e.g. Service 1, Scenario 2.
if (store.getUniversalErrorScenario() != null
&& mockServiceStoreTemporary.getUniversalErrorScenarioRef() != null) {
mergeResults.addConflictMsg("Universal error message already defined with name '"
+ store.getUniversalErrorScenario().getScenarioName() + "'");
} else if (store.getUniversalErrorScenario() == null
&& mockServiceStoreTemporary.getUniversalErrorScenarioRef() != null) {
store.setUniversalErrorScenarioRef(mockServiceStoreTemporary.getUniversalErrorScenarioRef());
mergeResults.addAdditionMsg("Universal error response defined.");
}
// STEP #6. MERGE SERVICE PLANS
for (ServicePlan servicePlan : mockServiceStoreTemporary.getServicePlans()) {
if (tagArguments != null) {
servicePlan.addTagToList(tagArguments);
}
store.saveOrUpdateServicePlan(servicePlan);
}
// STEP #7. TWIST CONFIGURATION
for (TwistInfo twistInfo : mockServiceStoreTemporary.getTwistInfoList()) {
store.saveOrUpdateTwistInfo(twistInfo);
}
// STEP #8. DEFAULT Service Plan ID
// Only set a default service plan ID from the incoming XML file
// if one is not already set in the current store.
ServicePlan servicePlan = mockServiceStoreTemporary.getServicePlanById(mockServiceStoreTemporary
.getDefaultServicePlanIdAsLong());
if (servicePlan != null && store.getDefaultServicePlanIdAsLong() == null) {
// OK, we have a 'default' service plan from the incoming file AND
// the current store does not. Let's update the current store.
store.setDefaultServicePlanId(mockServiceStoreTemporary.getDefaultServicePlanId());
store.setServicePlan(servicePlan);
}
// Don't forget to set state back to original state.
// NOTE: if transient state (read only) is false, then this method will
// write to STORE to the file system.
// Yeah!
// *********************
store.setReadOnlyMode(originalTransientState);
// *********************
return mergeResults;
}
// Let's Merge!
private ServiceMergeResults addServicesToStore(ServiceMergeResults mergeResults, List<Service> serviceListToAdd,
String tagArguments) {
// When loading a definition file, by default, we should
// compare the uploaded Service list mock URL to what's currently
// in memory.
//
// 1) MATCHING MOCK URL
// If there is an existing/matching mockURL, then this isn't
// a new service and we DON'T want to overwrite. But, we
// want new Scenarios if they exist. See Scenario.equals()
//
// 2) NO MATCHING MOCK URL
// If there is no matching service URL, then we want to create a new
// service and associated scenarios. But here's an odd case. What if
// we are merging two same-name Services, each with empty matching URL
// lists?
//
for (Service uploadedServiceBean : serviceListToAdd) {
List<Service> serviceBeansInMemory = store.getServices();
Iterator<Service> inMemoryServiceIter = serviceBeansInMemory.iterator();
boolean existingService = false;
Service inMemoryServiceBean = null;
while (inMemoryServiceIter.hasNext()) {
inMemoryServiceBean = (Service) inMemoryServiceIter.next();
// Same name?
if (uploadedServiceBean.getServiceName().trim().toLowerCase()
.equals(inMemoryServiceBean.getServiceName().trim().toLowerCase())) {
existingService = true;
mergeResults.addConflictMsg("Service '" + uploadedServiceBean.getServiceName()
+ "' not created because one with the same name already defined. '"
+ inMemoryServiceBean.getServiceName() + "' ");
}
}
if (!existingService) {
// YES, no in-store matching Name.
// We null ID, to not write-over on any in-store
// services with same ID
uploadedServiceBean.setId(null);
// #TAG HANDLING - BEGIN
// Ensure Service, and all it's child scenarios have
// incoming/uploaded tag arguments
uploadedServiceBean.addTagToList(tagArguments);
for (Scenario scenarioTmp : uploadedServiceBean.getScenarios()) {
scenarioTmp.addTagToList(tagArguments);
}
// #TAG HANDLING - END
// Save to the IN-MEMORY STORE
store.saveOrUpdateService(uploadedServiceBean);
mergeResults.addAdditionMsg("Uploaded Service '" + uploadedServiceBean.getServiceName()
+ "' created with scenarios.");
} else {
// We have an existing Service
// Just merge scenarios per matching services
mergeResults = this.mergeServices(uploadedServiceBean, inMemoryServiceBean, mergeResults, tagArguments);
}
}
return mergeResults;
}
/**
* This method will make an effort to take things that exist in the
*
* @param uploadedService
* @param inMemoryService
* @param readResults
* @return
*/
public ServiceMergeResults mergeServices(Service uploadedService, Service inMemoryService
, ServiceMergeResults readResults, String tagArguments) {
Boolean originalMode = store.getReadOnlyMode();
store.setReadOnlyMode(true);
if (uploadedService != null && inMemoryService != null
&& uploadedService.getServiceName().trim().equalsIgnoreCase(inMemoryService.getServiceName().trim())
) {
// ********************** TAG - BEGIN ***********************
// #TAG HANDLING for the Service - BEGIN
// Ensure Service gets incoming/uploaded-file tag arguments
inMemoryService.addTagToList(tagArguments);
// Ensure Service gets uploaded Service tag arguments
inMemoryService.addTagToList(uploadedService.getTag());
// #TAG HANDLING for the Service - END
// ********************** TAG - END *****************
// ********************* SCENARIOS BEGIN *******************
if (readResults == null) {
readResults = new ServiceMergeResults();
}
Iterator<Scenario> uploadedListIter = uploadedService.getScenarios().iterator();
Iterator<Scenario> inMemListIter = inMemoryService.getScenarios().iterator();
while (uploadedListIter.hasNext()) {
Scenario uploadedScenario = (Scenario) uploadedListIter.next();
boolean inMemScenarioExistTemp = false;
Scenario inMemScenarioTemp = null;
while (inMemListIter.hasNext()) {
inMemScenarioTemp = (Scenario) inMemListIter.next();
if (inMemScenarioTemp.hasSameNameAndResponse(uploadedScenario)) {
inMemScenarioExistTemp = true;
break;
}
}
if (!inMemScenarioExistTemp) {
// Hey, we have a new scenario.
// NOTE: incoming/uploaded scenario has an ID.
// We MUST nullify it, to ensure there's no common Service's
// scenario's ID
uploadedScenario.setId(null);
uploadedScenario.setServiceId(inMemoryService.getId());
// Tag for Service:Scenario
uploadedScenario.addTagToList(tagArguments);
inMemoryService.saveOrUpdateScenario(uploadedScenario);
readResults.addAdditionMsg("Scenario name '" + uploadedScenario.getScenarioName()
+ "' from uploaded service named '" + uploadedService.getServiceName()
+ "' was merged into service '" + inMemoryService.getServiceName() + "' ");
} else {
// OK, we have a MATCHING Scenario.
// Be sure to add the uploaded-file tags
inMemScenarioTemp.addTagToList(tagArguments);
// Be sure to add the uploaded-scenario tags
inMemScenarioTemp.addTagToList(uploadedScenario.getTag());
// Save the scenario to the Service
inMemoryService.saveOrUpdateScenario(inMemScenarioTemp);
// Although we still need to
readResults.addConflictMsg("Uploaded Scenario '" + uploadedScenario.getScenarioName()
+ "' not added, already defined in in-memory service '" + inMemoryService.getServiceName()
+ "' ");
}
}
// ********************* SCENARIOS - END ******************
// ********************* REAL URLS - BEGIN *******************
for (Url uploadedUrl : uploadedService.getRealServiceUrls()) {
inMemoryService.saveOrUpdateRealServiceUrl(uploadedUrl);
}
// ********************* REAL URLS - END *******************
store.saveOrUpdateService(inMemoryService);
}
store.setReadOnlyMode(originalMode);
return readResults;
}
/**
*
* @param service
* @return
*/
protected File getServiceFile(Service service) {
// Ensure the name is good.
String serviceFileName = service.getServiceName();
if (serviceFileName != null) {
serviceFileName = getSafeForFileSystemName(serviceFileName);
File serviceDirectoryFile = new File(this.getBasePathFile(), MockeyXmlFileManager.MOCK_SERVICE_FOLDER
+ FILESEPERATOR + serviceFileName);
// depot directory/<service ID> directory
if (!serviceDirectoryFile.exists()) {
serviceDirectoryFile.mkdir();
}
// depot directory/<service ID> directory/scenario directory/
File serviceScenarioListDirectory = new File(serviceDirectoryFile.getPath() + FILESEPERATOR
+ MOCK_SERVICE_SCENARIO_FOLDER);
if (!serviceScenarioListDirectory.exists()) {
serviceScenarioListDirectory.mkdir();
}
// depot directory/<service ID> directory/<service ID> file
File serviceFile = new File(serviceDirectoryFile.getPath() + FILESEPERATOR + serviceFileName + ".xml");
return serviceFile;
} else {
return null;
}
}
protected File[] getServiceScenarioFileNames(Service service) {
File serviceScenarioDir = new File(this.getBasePathFile(), getSafeForFileSystemName(service.getServiceName())
+ FILESEPERATOR + MOCK_SERVICE_SCENARIO_FOLDER);
return serviceScenarioDir.listFiles();
}
private File getServiceScenarioDirectoryAbsolutePath(Service service, Scenario scenario) {
// mockey_def_depot/<service ID>/scenarios/<scenario_name>.xml
File serviceScenarioFolder = new File(this.getBasePathFile(), MockeyXmlFileManager.MOCK_SERVICE_FOLDER
+ FILESEPERATOR + getSafeForFileSystemName(service.getServiceName()) + FILESEPERATOR
+ MOCK_SERVICE_SCENARIO_FOLDER);
return serviceScenarioFolder;
}
protected File getServiceScenarioFileAbsolutePath(Service service, Scenario scenario) {
// mockey_def_depot/<service ID>/scenarios/<scenario_name>.xml
File serviceScenarioFolder = getServiceScenarioDirectoryAbsolutePath(service, scenario);
File serviceScenarioFile = new File(serviceScenarioFolder.getPath() + FILESEPERATOR
+ getScenarioXmlFileName(scenario));
return serviceScenarioFile;
}
/**
* Example if file is here:
* <pre>
* // /Users/<User>/Work/Mockey/dist/mockey_def_depot/<ServiceName>/scenarios/<ScenarioName>.xml
* </pre>
* then returns
* <pre>
* mockey_def_depot/<ServiceName>/scenarios/<ScenarioName>.xml
* </pre>
* @param service
* @param scenario
* @return a path name relative to the root mockey depot folder.
*
*/
protected String getServiceScenarioFileRelativePathToDepotFolder(Service service, Scenario scenario) {
String relativePath = MockeyXmlFileManager.MOCK_SERVICE_FOLDER + FILESEPERATOR
+ getSafeForFileSystemName(service.getServiceName()) + FILESEPERATOR + MOCK_SERVICE_SCENARIO_FOLDER
+ FILESEPERATOR + getSafeForFileSystemName(scenario.getScenarioName()) + ".xml";
return relativePath;
}
/**
*
* @param scenario
* @return
*/
public String getScenarioResponseFileName(Scenario scenario) {
return getSafeForFileSystemName(scenario.getScenarioName()) + ".txt";
}
/**
*
* @param scenario
* @return
*/
public String getScenarioXmlFileName(Scenario scenario) {
return getSafeForFileSystemName(scenario.getScenarioName()) + ".xml";
}
/**
*
* @param arg
* @return a file name safe for a file system.
* @see MockeyXmlFileManager#VALID_FILE_NAME_CHARS
*
*/
public static String getSafeForFileSystemName(String arg) {
// Let's make sure we only accept valid characters (AlphaNumberic +
// '_').
StringBuffer safe = new StringBuffer();
for (int x = 0; x < arg.length(); x++) {
boolean valid = false;
for (int i = 0; i < VALID_FILE_NAME_CHARS.length; i++) {
if (arg.charAt(x) == VALID_FILE_NAME_CHARS[i]) {
valid = true;
break;
}
}
if (valid) {
safe.append(arg.charAt(x));
}
}
return safe.toString().toLowerCase();
//
}
/**
* @param baseFile
* @param fileArg
* @return the RELATIVE path of the fileArg if a child of base file.
* Otherwise, returns the absolute path of the fileArg.
*/
public String getRelativePath(File fileArg) {
String relativePath = "";
String basePath = this.getBasePathFile().getAbsolutePath();
String filePath = fileArg.getAbsolutePath();
int index = filePath.indexOf(basePath);
if (index > -1) {
try {
//
relativePath = fileArg.getAbsolutePath().substring(index + basePath.length() + 1);
} catch (Exception e) {
logger.error("Unable to retrive a relative path from: " + fileArg.getAbsolutePath()
+ "' with base path '" + basePath + "'", e);
relativePath = "ERROR";
}
} else {
relativePath = "ERROR";
logger.error("Unable to retrive a relative path from: " + fileArg.getAbsolutePath() + "' with base path '"
+ basePath + "'");
}
return relativePath;
}
}