/* Copyright 2004-2014 Jim Voris * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qumasoft.server; import com.qumasoft.qvcslib.ArchiveInfoInterface; import com.qumasoft.qvcslib.DirectoryCoordinate; import com.qumasoft.qvcslib.QVCSConstants; import com.qumasoft.qvcslib.QVCSException; import com.qumasoft.qvcslib.QVCSServedProjectNamesFilter; import com.qumasoft.qvcslib.ServedProjectProperties; import com.qumasoft.qvcslib.ServerResponseFactory; import com.qumasoft.qvcslib.ServerResponseFactoryInterface; import com.qumasoft.qvcslib.Utility; import com.qumasoft.server.dataaccess.BranchDAO; import com.qumasoft.server.dataaccess.DirectoryDAO; import com.qumasoft.server.dataaccess.FileDAO; import com.qumasoft.server.dataaccess.ProjectDAO; import com.qumasoft.server.dataaccess.impl.BranchDAOImpl; import com.qumasoft.server.dataaccess.impl.DirectoryDAOImpl; import com.qumasoft.server.dataaccess.impl.FileDAOImpl; import com.qumasoft.server.dataaccess.impl.ProjectDAOImpl; import com.qumasoft.server.datamodel.Branch; import com.qumasoft.server.datamodel.Directory; import com.qumasoft.server.datamodel.Project; import com.qumasoft.webserver.WebServer; import java.io.File; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; /** * The QVCS Enterprise server class. This is the main class for the QVCS Enterprise server. * * @author Jim Voris */ public final class QVCSEnterpriseServer { // Capture the version of the server. private static final String VERSION = "3.0.9"; private static final String USER_DIR = "user.dir"; static final int DEFAULT_NON_SECURE_LISTEN_PORT = 9889; static final int DEFAULT_ADMIN_LISTEN_PORT = 9890; static final String WEB_SERVER_PORT = "9080"; static final int WORKER_THREAD_COUNT = 50; private static final int ARGS_LENGTH_WITH_SYNC_OBJECT = 5; private static final int ARGS_SYNC_OBJECT_INDEX = 4; private int nonSecurePort = DEFAULT_NON_SECURE_LISTEN_PORT; private int adminPort = DEFAULT_ADMIN_LISTEN_PORT; private static final long THREAD_POOL_AWAIT_TERMINATION_DELAY = 5; private String qvcsHomeDirectory = null; private final String[] arguments; private static boolean serverIsRunningFlag; private final ExecutorService threadPool = Executors.newCachedThreadPool(); private NonSecureServer nonSecureServer = null; private NonSecureServer adminServer = null; private QVCSWebServer webServer = null; // Server socket listener threads. private Thread nonSecureThread = null; private Thread adminThread = null; // Web server thread. private Thread webServerThread = null; private static QVCSEnterpriseServer qvcsEnterpriseServer; // Create our logger object private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server"); private static final List<ServerResponseFactoryInterface> CONNECTED_USERS_COLLECTION = Collections.synchronizedList(new ArrayList<ServerResponseFactoryInterface>()); /** * Main entry point to the QVCS-Enterprise server. * @param args command line arguments. */ public static void main(String[] args) { qvcsEnterpriseServer = new QVCSEnterpriseServer(args); Object syncObject = null; try { if (args.length == ARGS_LENGTH_WITH_SYNC_OBJECT) { syncObject = args[ARGS_SYNC_OBJECT_INDEX]; } qvcsEnterpriseServer.startServer(syncObject); } catch (SQLException | ClassNotFoundException e) { LOGGER.log(Level.SEVERE, "Failed to initialize the database. " + e.getLocalizedMessage()); if (syncObject != null) { synchronized (syncObject) { syncObject.notifyAll(); } } } catch (QVCSException e) { LOGGER.log(Level.SEVERE, "Caught QVCSException. " + e.getLocalizedMessage()); if (syncObject != null) { synchronized (syncObject) { syncObject.notifyAll(); } } } finally { LOGGER.log(Level.INFO, "Server exit complete."); } } /** * Is the server running. * @return true if the server is running; false if it is not running. */ public static boolean getServerIsRunningFlag() { return serverIsRunningFlag; } /** * Get a collection of client connections. * @return a collection of client connections. */ public static Collection<ServerResponseFactoryInterface> getConnectedUsers() { List<ServerResponseFactoryInterface> collection; synchronized (CONNECTED_USERS_COLLECTION) { collection = new ArrayList<>(); Iterator<ServerResponseFactoryInterface> it = CONNECTED_USERS_COLLECTION.iterator(); while (it.hasNext()) { collection.add(it.next()); } } return collection; } static Collection<ServerResponseFactoryInterface> getConnectedUsersCollection() { return CONNECTED_USERS_COLLECTION; } /** * Set the shutdown in progress flag. The server will stop accepting client connections if the flag is true. * @param flag set to true to shutdown the server. */ public static void setShutdownInProgress(boolean flag) { if (flag) { LOGGER.log(Level.INFO, "QVCS Enterprise Server is exiting."); if ((qvcsEnterpriseServer != null) && (qvcsEnterpriseServer.nonSecureThread != null)) { // Don't accept any more client connection requests on standard client port. qvcsEnterpriseServer.nonSecureServer.closeServerSocket(); } if ((qvcsEnterpriseServer != null) && (qvcsEnterpriseServer.adminThread != null)) { // Don't accept any more client connection requests on admin client port. qvcsEnterpriseServer.adminServer.closeServerSocket(); } } } /** * Creates a new instance of QVCSEnterpriseServer. Only accessible via calls to main(). * * @param args command line arguments. */ private QVCSEnterpriseServer(String[] args) { if (args != null) { String[] localArgs = new String[args.length]; System.arraycopy(args, 0, localArgs, 0, args.length); this.arguments = localArgs; } else { this.arguments = new String[0]; } if (arguments.length > 0) { System.setProperty(USER_DIR, arguments[0]); } qvcsHomeDirectory = System.getProperty(USER_DIR); } private void startServer(Object serverStartCompleteSyncObject) throws SQLException, QVCSException, ClassNotFoundException { try { if (arguments.length > 1) { nonSecurePort = Integer.parseInt(arguments[1]); } } catch (NumberFormatException e) { nonSecurePort = DEFAULT_NON_SECURE_LISTEN_PORT; } try { if (arguments.length > 2) { adminPort = Integer.parseInt(arguments[2]); } } catch (NumberFormatException e) { adminPort = DEFAULT_ADMIN_LISTEN_PORT; } // Init the logging properties. initLoggingProperties(); // Report the System info. reportSystemInfo(); LOGGER.log(Level.INFO, "QVCS Enterprise Server Version: '" + VERSION + "'."); LOGGER.log(Level.INFO, "QVCS Enterprise Server running with " + Runtime.getRuntime().availableProcessors() + " available processors."); // Define the database location. DatabaseManager.getInstance().setDerbyHomeDirectory(getDerbyHomeDirectory()); // Initialize the role privileges manager RolePrivilegesManager.getInstance().initialize(); // Initialize the role manager. RoleManager.getRoleManager().initialize(); // Initialize the authentication manager. AuthenticationManager.getAuthenticationManager().initialize(); // Initialize the archive digest manager. ArchiveDigestManager.getInstance().initialize(QVCSConstants.QVCS_SERVED_PROJECT_TYPE); // Initialize the file ID manager. FileIDManager.getInstance().initialize(); // See if we need to scan for fileID, etc. if (FileIDManager.getInstance().getFileIDResetRequiredFlag()) { // Wipe out the database. wipeDatabase(getDerbyHomeDirectory()); // Initialize the database. DatabaseManager.getInstance().initializeDatabase(); // Reset the directory id dictionary store. DirectoryIDDictionary.getInstance().resetStore(); DirectoryIDDictionary.getInstance().initialize(); // Reset the directoryID store. DirectoryIDManager.getInstance().resetStore(); DirectoryIDManager.getInstance().initialize(); // Reset the file id dictionary store. FileIDDictionary.getInstance().resetStore(); FileIDDictionary.getInstance().initialize(); // Remove the view manager store. ViewManager.getInstance().resetStore(); ViewManager.getInstance().initialize(); // We need to reset all file IDs. This actually sets the files ids in the archive files. resetFileIDs(); // Reset the directory IDs. This actually clears all the directory ids. They get assigned later. resetDirectoryIDs(); // Initialize our DirectoryContents objects. initializeDirectoryContentsObjects(); FileIDManager.getInstance().setFileIDResetRequiredFlag(false); } else { // Initialize the database. DatabaseManager.getInstance().initializeDatabase(); // Initialize the DirectoryIDDictionary. DirectoryIDDictionary.getInstance().initialize(); // Initialize the FileIDDictionary. FileIDDictionary.getInstance().initialize(); // Initialize the directoryID manager. DirectoryIDManager.getInstance().initialize(); // Initialize the ViewManager. ViewManager.getInstance().initialize(); } // Validate/Update database to match what exists on the trunk. DatabaseVerificationManager.getInstance().verifyTrunkToDatabase(getServedProjectPropertiesList()); // Register our shutdown thread. Runtime.getRuntime().addShutdownHook(new QVCSEnterpriseServer.ShutdownThread()); // Initialize the Activity Journal Manager. ActivityJournalManager.getInstance().initialize(); ActivityJournalManager.getInstance().addJournalEntry("QVCS-Enterprise Server is starting. Server Version: " + VERSION + "."); // Launch three separate listener threads // one for non-secure requests, // one for secure requests, and a 3rd for admin messages. nonSecureServer = new NonSecureServer(nonSecurePort); adminServer = new NonSecureServer(adminPort); webServer = new QVCSWebServer(arguments); nonSecureThread = new Thread(nonSecureServer, "non secure server"); adminThread = new Thread(adminServer, "admin server"); webServerThread = new Thread(webServer, "web server"); webServerThread.setDaemon(true); nonSecureThread.start(); adminThread.start(); webServerThread.start(); // This will notify the TestHelper that the server is ready for use. if (serverStartCompleteSyncObject != null) { synchronized (serverStartCompleteSyncObject) { serverStartCompleteSyncObject.notifyAll(); } } serverIsRunningFlag = true; try { nonSecureThread.join(); adminThread.join(); // Kill the web server. webServerThread.interrupt(); // Shut down the thread pool and wait for all the worker threads to exit. threadPool.shutdown(); // Disable new tasks from being submitted LOGGER.log(Level.INFO, "Threadpool shutdown called."); try { // Wait a while for existing tasks to terminate if (!threadPool.awaitTermination(THREAD_POOL_AWAIT_TERMINATION_DELAY, TimeUnit.SECONDS)) { threadPool.shutdownNow(); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if (!threadPool.awaitTermination(THREAD_POOL_AWAIT_TERMINATION_DELAY, TimeUnit.SECONDS)) { LOGGER.log(Level.WARNING, "Thread pool did not terminate"); } } } catch (InterruptedException e) { // (Re-)Cancel if current thread also interrupted threadPool.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } } catch (InterruptedException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { DatabaseManager.getInstance().shutdownDatabase(); ActivityJournalManager.getInstance().addJournalEntry("QVCS-Enterprise Server is shutting down."); ArchiveDigestManager.getInstance().writeStore(); DirectoryIDManager.getInstance().writeStore(); FileIDManager.getInstance().writeStore(); DirectoryIDDictionary.getInstance().writeStore(); FileIDDictionary.getInstance().writeStore(); ActivityJournalManager.getInstance().closeJournal(); LOGGER.log(Level.INFO, "QVCS Enterprise Server exit complete."); System.out.println("QVCS Enterprise Server exit complete."); serverIsRunningFlag = false; if (serverStartCompleteSyncObject != null) { synchronized (serverStartCompleteSyncObject) { serverStartCompleteSyncObject.notifyAll(); } } } } private void initLoggingProperties() { try { String logConfigFile = qvcsHomeDirectory + File.separator + "serverLogging.properties"; System.setProperty("java.util.logging.config.file", logConfigFile); LogManager.getLogManager().readConfiguration(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Caught exception: " + e.getClass().toString() + " : " + e.getLocalizedMessage()); System.out.println("Caught exception: " + e.getClass().toString() + " : " + e.getLocalizedMessage()); } } /** * Figure out where we put the derby database. * * @return the directory name for the root of the derby database. */ private String getDerbyHomeDirectory() { return qvcsHomeDirectory + File.separator + QVCSConstants.QVCS_DERBY_DB_DIRECTORY; } /** * Report the system's information to the log file.... basically all the system properties. */ private void reportSystemInfo() { java.util.Properties systemProperties = System.getProperties(); java.util.Set keys = systemProperties.keySet(); java.util.Iterator it = keys.iterator(); StringBuilder messageString = new StringBuilder(); messageString.append("\nSystem properties:\n"); while (it.hasNext()) { String key = (String) it.next(); String message = key + " = " + System.getProperty(key); messageString.append(message); messageString.append("\n"); } LOGGER.log(Level.INFO, messageString.toString()); // Log what charset is the platform default LOGGER.log(Level.INFO, "Default charset: " + Charset.defaultCharset().displayName()); } /** * This method is called <i>before</i> the server opens any ports to listen for client connections... The goal is to reset the * file ids for all files in all existing projects. We do <i>not</i> use the ArchiveDirManager class here as that is * heavier-weight than what we want/need to do here. */ private void resetFileIDs() { LOGGER.log(Level.INFO, "QVCSEnterpriseServer: resetting all file id's for all projects."); try { // Get a list of all the projects that this server serves... ServedProjectProperties[] projectPropertiesList = getServedProjectPropertiesList(); for (ServedProjectProperties projectPropertiesList1 : projectPropertiesList) { String archiveLocation = projectPropertiesList1.getArchiveLocation(); File projectBaseDirectory = new File(archiveLocation); resetFileIDsForProjectDirectoryTree(projectBaseDirectory); resetCemeteryFilenames(projectBaseDirectory); // Remove any branch archive files, since they are now worthless since we // have destroyed the directory contents, and the ability to see branches and/or views. removeProjectBranchArchiveFiles(projectBaseDirectory); } } finally { LOGGER.log(Level.INFO, "QVCSEnterpriseServer: reset file id's completed."); } } /** * <p>This method is called <i>before</i> the server opens any ports to listen for client connections. This method is only * called when resetting the file id's and has the task of renaming the cemetery files of a project so that the cemetery file's * name matches the file id that has be re-assigned to that cemetery file. This method should be called <i>after</i> the * resetFileIDs.</p> <p>Note that we have to iterate over the cemetery twice; first to rename the files to a name that is * guaranteed not to collide with any existing cemetery file name; and a 2nd time to rename them to their correct cemetery file * name.</p> * * @param projectBaseDirectory the base directory of a project's archive files. */ private void resetCemeteryFilenames(File projectBaseDirectory) { File cemeteryDirectory = new File(projectBaseDirectory.getAbsolutePath() + File.separator + QVCSConstants.QVCS_CEMETERY_DIRECTORY); LOGGER.log(Level.INFO, "Renaming cemetery files for directory: [" + cemeteryDirectory.getAbsolutePath() + "]"); File[] fileList = cemeteryDirectory.listFiles(); if (fileList != null) { for (File fileList1 : fileList) { if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_CACHE_NAME) == 0) { // Get rid of the cache file, since we may be changing things here that // would make the cache out of date. if (fileList1.delete()) { LOGGER.log(Level.INFO, "Deleting " + QVCSConstants.QVCS_CACHE_NAME + " file from directory: " + cemeteryDirectory.getAbsolutePath()); } continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_JOURNAL_NAME) == 0) { continue; } if (fileList1.isDirectory()) { // Do not traverse a directory tree in the cemetery. It should be flat. continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_DIRECTORYID_FILENAME) == 0) { continue; } if (fileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_TEMPFILE_SUFFIX)) { continue; } if (fileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_OLDFILE_SUFFIX)) { continue; } if (Utility.isMacintosh() && fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_MAC_DS_STORE_FILENAME) == 0) { continue; } LogFile logfile = new LogFile(fileList1.getPath()); if (logfile.readInformation()) { try { int fileId = logfile.getFileID(); String transientCemeteryFilename = cemeteryDirectory.getCanonicalPath() + File.separator + "TRANSIENT_QVCS_CEMETERY_NAME_" + fileId; File transientCemeteryFile = new File(transientCemeteryFilename); fileList1.renameTo(transientCemeteryFile); } catch (IOException ex) { Logger.getLogger(QVCSEnterpriseServer.class.getName()).log(Level.SEVERE, null, ex); } } else { LOGGER.log(Level.WARNING, "Failed to read logfile information for: [" + fileList1.getPath() + "]"); LOGGER.log(Level.WARNING, "Deleting corrupt logfile from cemetery: [" + fileList1.getPath() + "]"); fileList1.delete(); } } // Go through the files in the cemetery again, this time, rename to the name they should have. File[] renamedFileList = cemeteryDirectory.listFiles(); if (renamedFileList != null) { for (File renamedFileList1 : renamedFileList) { if (renamedFileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_CACHE_NAME) == 0) { // Get rid of the cache file, since we may be changing things here that // would make the cache out of date. if (renamedFileList1.delete()) { LOGGER.log(Level.INFO, "Deleting " + QVCSConstants.QVCS_CACHE_NAME + " file from directory: " + cemeteryDirectory.getAbsolutePath()); } continue; } if (renamedFileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_JOURNAL_NAME) == 0) { continue; } if (renamedFileList1.isDirectory()) { // Do not traverse a directory tree in the cemetery. It should be flat. continue; } if (renamedFileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_DIRECTORYID_FILENAME) == 0) { continue; } if (renamedFileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_TEMPFILE_SUFFIX)) { continue; } if (renamedFileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_OLDFILE_SUFFIX)) { continue; } if (Utility.isMacintosh() && renamedFileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_MAC_DS_STORE_FILENAME) == 0) { continue; } LogFile logfile = new LogFile(renamedFileList1.getPath()); if (logfile.readInformation()) { try { int fileId = logfile.getFileID(); String permanentCemeteryFilename = cemeteryDirectory.getCanonicalPath() + File.separator + Utility.createCemeteryShortArchiveName(fileId); File permanentCemeteryFile = new File(permanentCemeteryFilename); renamedFileList1.renameTo(permanentCemeteryFile); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to get the canonical path for [" + cemeteryDirectory.getAbsolutePath() + "]", e); } } else { LOGGER.log(Level.WARNING, "Failed to read logfile information for: [" + renamedFileList1.getPath() + "]"); LOGGER.log(Level.WARNING, "Deleting corrupt logfile from cemetery: [" + renamedFileList1.getPath() + "]"); renamedFileList1.delete(); } } } } LOGGER.log(Level.INFO, "Completed renaming cemetery files for directory: [" + cemeteryDirectory.getAbsolutePath() + "]"); } /** * Remove the project's branch archive files. This are archive files that capture archive history for files that exist * <i>only</i> on a branch -- i.e. the file was never 'promoted' to the trunk. Since this method is called only if/when we are * resetting the file id's -- we have to throw these archives away since they are now orphaned. (since the branch will be gone * after the reset of the file ids). * * <p><b>For use before the server accepts clients connections only!!</b></p> * * @param projectBaseDirectory the base directory of a project's archive files. */ private void removeProjectBranchArchiveFiles(File projectBaseDirectory) { File branchArchiveDirectory = new File(projectBaseDirectory.getAbsolutePath() + File.separator + QVCSConstants.QVCS_BRANCH_ARCHIVES_DIRECTORY); LOGGER.log(Level.INFO, "Deleting branch archive files for directory: [" + branchArchiveDirectory.getAbsolutePath() + "]"); File[] fileList = branchArchiveDirectory.listFiles(); if (fileList != null) { for (File fileList1 : fileList) { LOGGER.log(Level.INFO, "Deleting branch archive file: [" + fileList1.getAbsolutePath() + "]"); fileList1.delete(); } } } /** * Get the list of projects that are 'served' by this server application. <p><b>For use before the server accepts clients * connections only!!</b></p> */ private ServedProjectProperties[] getServedProjectPropertiesList() { ServedProjectProperties[] servedProjectsProperties = new ServedProjectProperties[0]; // Where all the property files can be found... File propertiesDirectory = new File(System.getProperty(USER_DIR) + System.getProperty("file.separator") + QVCSConstants.QVCS_PROPERTIES_DIRECTORY); QVCSServedProjectNamesFilter servedProjectNamesFilter = new QVCSServedProjectNamesFilter(); File[] servedProjectFiles = propertiesDirectory.listFiles(servedProjectNamesFilter); if (servedProjectFiles != null) { servedProjectsProperties = new ServedProjectProperties[servedProjectFiles.length]; for (int i = 0; i < servedProjectFiles.length; i++) { String projectName = servedProjectNamesFilter.getProjectName(servedProjectFiles[i].getName()); try { servedProjectsProperties[i] = new ServedProjectProperties(projectName); } catch (QVCSException e) { LOGGER.log(Level.WARNING, "Error finding served project names for project: '" + projectName + "'."); } } } return servedProjectsProperties; } /** * For use by the resetFileIDs() method only!! Reset fileIDs for the given directory tree. */ @SuppressWarnings("LoggerStringConcat") private void resetFileIDsForProjectDirectoryTree(File directory) { LOGGER.log(Level.INFO, "Resetting file id's for directory: " + directory.getAbsolutePath()); File[] fileList = directory.listFiles(); if (fileList != null) { for (File fileList1 : fileList) { if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_CACHE_NAME) == 0) { // Get rid of the cache file, since we may be changing things here that // would make the cache out of date. if (fileList1.delete()) { LOGGER.log(Level.INFO, "Deleting " + QVCSConstants.QVCS_CACHE_NAME + " file from directory: " + directory.getAbsolutePath()); } continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_JOURNAL_NAME) == 0) { continue; } if (fileList1.isDirectory()) { // Recurse through the directory tree... resetFileIDsForProjectDirectoryTree(fileList1); continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_DIRECTORYID_FILENAME) == 0) { continue; } if (fileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_TEMPFILE_SUFFIX)) { continue; } if (fileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_OLDFILE_SUFFIX)) { continue; } if (Utility.isMacintosh() && fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_MAC_DS_STORE_FILENAME) == 0) { continue; } LogFile logfile = new LogFile(fileList1.getPath()); if (logfile.readInformation()) { // Update the file's file ID. This will cause a re-write of the archive file with an updated // header (supplemental info) that contains the assigned file id. int newFileId = FileIDManager.getInstance().getNewFileID(); // Reset the file id and discard all branch and view labels that may have been applied to the archive files. // we need to do this because we are starting over, and all branches and views have been discarded. logfile.setFileIDAndRemoveViewAndBranchLabels(newFileId); LOGGER.log(Level.INFO, "Reset file id for [" + logfile.getFullArchiveFilename() + "] to [" + newFileId + "]"); } else { LOGGER.log(Level.WARNING, "Failed to read logfile information for: [" + fileList1.getPath() + "]"); } } } } /** * This method is called <i>before</i> the server opens any ports to listen for client connections... The goal is to reset the * directory ids for all existing projects. */ private void resetDirectoryIDs() { LOGGER.log(Level.INFO, "QVCSEnterpriseServer: resetting all directory ids."); try { // Get a list of all the projects that this server serves... ServedProjectProperties[] projectPropertiesList = getServedProjectPropertiesList(); for (ServedProjectProperties projectPropertiesList1 : projectPropertiesList) { String archiveLocation = projectPropertiesList1.getArchiveLocation(); File projectBaseDirectory = new File(archiveLocation); removeDirectoryContentsArchiveFiles(projectPropertiesList1.getProjectName()); resetDirectoryIDsForDirectoryTree(projectBaseDirectory); } DirectoryIDManager.getInstance().setMaximumDirectoryID(0); } catch (Exception e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { LOGGER.log(Level.INFO, "QVCSEnterpriseServer: reset all directory ids complete."); } } /** * For use by the resetDirectoryIDs() method only!! Reset the given directory ids for the given directory tree. */ private void resetDirectoryIDsForDirectoryTree(File directory) { LOGGER.log(Level.INFO, "Resetting directory id for directory: [" + directory.getAbsolutePath() + "]"); File[] fileList = directory.listFiles(); if (fileList != null) { for (File fileList1 : fileList) { if (fileList1.isDirectory()) { // Recurse through the directory tree... resetDirectoryIDsForDirectoryTree(fileList1); continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_DIRECTORYID_FILENAME) == 0) { try { // Delete the directory ID file... It will get re-created when we create the directory // contents object for this directory... fileList1.delete(); } catch (Exception e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } } } } /** * For use only when resetting all directory contents!! * * @param projectRootDirectory the root directory of the project. */ private void removeDirectoryContentsArchiveFiles(String projectName) { String fullArchiveDirectory = System.getProperties().getProperty(USER_DIR) + File.separator + QVCSConstants.QVCS_PROJECTS_DIRECTORY + File.separator + projectName + File.separator + QVCSConstants.QVCS_DIRECTORY_METADATA_DIRECTORY; File directoryFile = new File(fullArchiveDirectory); File[] fileList = directoryFile.listFiles(); // Delete all the files in the directory used for storing directory contents archive files. if (fileList != null) { for (File fileList1 : fileList) { if (fileList1.isFile()) { fileList1.delete(); } } } } /** * Used once only when upgrading, or running the server for the very first time. This method should only be run before the * server accepts requests from client. */ private void initializeDirectoryContentsObjects() throws SQLException, QVCSException { LOGGER.log(Level.INFO, "QVCSEnterpriseServer: Initializing DirectoryContents objects..."); // Delete any/all existing directory contents objects... we're starting // from scratch here. deleteExistingDirectoryContentsObjects(); try { // Get a list of all the projects that this server serves... ServedProjectProperties[] projectPropertiesList = getServedProjectPropertiesList(); for (ServedProjectProperties projectPropertiesList1 : projectPropertiesList) { // Wrap this work in a server transaction so the DirectoryContents // stuff will behave in a useful way... ServerResponseFactoryInterface bogusResponseObject = new BogusResponseObject(); // Keep track that we're in a transaction. ServerTransactionManager.getInstance().clientBeginTransaction(bogusResponseObject); String archiveLocation = projectPropertiesList1.getArchiveLocation(); File projectBaseDirectory = new File(archiveLocation); // Create the project and its Trunk branch in the database. int branchId = createProjectAndTrunkDbRecords(projectPropertiesList1); // And initialize the directory contents objects for this project tree. initializeDirectoryContentsObjectForDirectory(projectBaseDirectory, projectPropertiesList1, branchId, -1, bogusResponseObject); // Keep track that we ended this transaction. ServerTransactionManager.getInstance().clientEndTransaction(bogusResponseObject); } // Throw away any archive dir managers we built since we now need // to built them again so they will discard (i.e. move) any obsolete // files. ArchiveDirManagerFactoryForServer.getInstance().resetDirectoryMap(); } catch (SQLException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); throw e; } } /** * Create the project record in the database. This method should only be called when starting the server for the first time, or * when rebuilding the database. * * @returns the branchId of the Trunk branch for the given project. * * @throws SQLException if the project already exists. */ private int createProjectAndTrunkDbRecords(ServedProjectProperties servedProjectProperties) throws SQLException { Project project = new Project(); project.setProjectName(servedProjectProperties.getProjectName()); ProjectDAO projectDAO = new ProjectDAOImpl(); projectDAO.insert(project); Project foundProject = projectDAO.findByProjectName(servedProjectProperties.getProjectName()); Branch branch = new Branch(); branch.setBranchName(QVCSConstants.QVCS_TRUNK_VIEW); branch.setBranchTypeId(1); branch.setProjectId(foundProject.getProjectId()); BranchDAO branchDAO = new BranchDAOImpl(); branchDAO.insert(branch); Branch foundBranch = branchDAO.findByProjectIdAndBranchName(foundProject.getProjectId(), QVCSConstants.QVCS_TRUNK_VIEW); return foundBranch.getBranchId(); } /** * Used only during an upgrade or when product is run for the 1st time. */ private void deleteExistingDirectoryContentsObjects() { try { // Get a list of all the projects that this server serves... ServedProjectProperties[] projectPropertiesList = getServedProjectPropertiesList(); for (ServedProjectProperties projectPropertiesList1 : projectPropertiesList) { String projectName = projectPropertiesList1.getProjectName(); String fullArchiveDirectory = System.getProperties().getProperty(USER_DIR) + File.separator + QVCSConstants.QVCS_PROJECTS_DIRECTORY + File.separator + projectName + File.separator + QVCSConstants.QVCS_DIRECTORY_METADATA_DIRECTORY; File directoryFile = new File(fullArchiveDirectory); if (!directoryFile.exists()) { if (!directoryFile.mkdirs()) { continue; } } // Delete all the files in the QVCS_DIRECTORY_METADATA_DIRECTORY File[] fileList = directoryFile.listFiles(); if (fileList != null) { for (File fileList1 : fileList) { if (fileList1.isDirectory()) { continue; } fileList1.delete(); } } } } catch (Exception e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } /** * Used only during an upgrade or when product is run for the 1st time. */ private void initializeDirectoryContentsObjectForDirectory(File directory, ServedProjectProperties servedProjectProperties, int branchId, int rootDirectoryId, ServerResponseFactoryInterface bogusResponseObject) throws SQLException, QVCSException { LOGGER.log(Level.INFO, "Populating database for directory: [" + directory.getAbsolutePath() + "]"); String projectName = servedProjectProperties.getProjectName(); String viewName = QVCSConstants.QVCS_TRUNK_VIEW; String appendedPath = ServerUtility.deduceAppendedPath(directory, servedProjectProperties); File[] fileList = directory.listFiles(); if (fileList != null) { try { // Create the archiveDirManager for this directory... DirectoryCoordinate directoryCoordinate = new DirectoryCoordinate(projectName, viewName, appendedPath); ArchiveDirManager archiveDirManager = (ArchiveDirManager) ArchiveDirManagerFactoryForServer.getInstance() .getDirectoryManager(QVCSConstants.QVCS_SERVER_SERVER_NAME, directoryCoordinate, QVCSConstants.QVCS_SERVED_PROJECT_TYPE, QVCSConstants.QVCS_SERVER_USER, bogusResponseObject, false); if (rootDirectoryId == -1) { rootDirectoryId = archiveDirManager.getDirectoryID(); } populateDatabaseFromArchiveDirManager(archiveDirManager, branchId, rootDirectoryId); } catch (QVCSException | SQLException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); throw e; } for (File fileList1 : fileList) { if (fileList1.isDirectory()) { // Recurse through the directory tree... initializeDirectoryContentsObjectForDirectory(fileList1, servedProjectProperties, branchId, rootDirectoryId, bogusResponseObject); } } } } /** * Create a Directory row, and the File rows for the given archive directory manager. This method should only be called during * initialization when the server is run for the first time, or if the server has been 'reset'. * * @param archiveDirManager the archive directory manager that we're looking at. * @param branchId the branchId for the current project's Trunk branch. * @param rootDirectoryId the root directory id for this directory tree. * @throws SQLException if there is some problem with the database. */ private void populateDatabaseFromArchiveDirManager(ArchiveDirManager archiveDirManager, int branchId, int rootDirectoryId) throws SQLException { // Wrap this in a transaction. DatabaseManager.getInstance().getConnection().setAutoCommit(false); DirectoryDAO directoryDAO = new DirectoryDAOImpl(); FileDAO fileDAO = new FileDAOImpl(); try { // Create the Directory row. Directory directory = new Directory(); directory.setDirectoryId(archiveDirManager.getDirectoryID()); directory.setRootDirectoryId(rootDirectoryId); if (archiveDirManager.getParent() != null) { directory.setParentDirectoryId(archiveDirManager.getParent().getDirectoryID()); } directory.setBranchId(branchId); directory.setAppendedPath(archiveDirManager.getAppendedPath()); directory.setDeletedFlag(false); directoryDAO.insert(directory); if (LOGGER.isLoggable(Level.INFO)) { LOGGER.log(Level.INFO, "Project: [" + archiveDirManager.getProjectName() + "] -- Created directory record for: [" + directory.getAppendedPath() + "]"); } // Insert all the files in this directory. Collection<ArchiveInfoInterface> archiveInfoCollection = archiveDirManager.getArchiveInfoCollection().values(); for (ArchiveInfoInterface archiveInfo : archiveInfoCollection) { com.qumasoft.server.datamodel.File file = new com.qumasoft.server.datamodel.File(); file.setFileId(archiveInfo.getFileID()); file.setBranchId(branchId); file.setDeletedFlag(false); file.setDirectoryId(archiveDirManager.getDirectoryID()); file.setFileName(archiveInfo.getShortWorkfileName()); fileDAO.insert(file); if (LOGGER.isLoggable(Level.INFO)) { LOGGER.log(Level.INFO, "Created file record for: [" + file.getFileName() + "]"); } } DatabaseManager.getInstance().getConnection().commit(); } finally { DatabaseManager.getInstance().getConnection().setAutoCommit(true); } } /** * Wipe out the derby database. This is only called via the reset path, i.e. when we are starting from scratch. * * @param derbyHomeDirectory the full path of the derby home directory. */ private void wipeDatabase(String derbyHomeDirectory) { File derbyDirectory = new File(derbyHomeDirectory); File[] files = derbyDirectory.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { File[] subFiles = file.listFiles(); for (File subFile : subFiles) { if (subFile.isDirectory()) { File[] subSubFiles = subFile.listFiles(); for (File subSubFile : subSubFiles) { subSubFile.delete(); } } subFile.delete(); } } file.delete(); } } } /** * This is the class that runs at server exit time. */ static class ShutdownThread extends Thread { @Override public void run() { try { DatabaseManager.getInstance().shutdownDatabase(); ActivityJournalManager.getInstance().addJournalEntry("QVCS-Enterprise Server: shutdown thread called to shutdown."); ArchiveDigestManager.getInstance().writeStore(); DirectoryIDManager.getInstance().writeStore(); FileIDManager.getInstance().writeStore(); DirectoryIDDictionary.getInstance().writeStore(); FileIDDictionary.getInstance().writeStore(); ActivityJournalManager.getInstance().closeJournal(); } catch (Exception e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { LOGGER.log(Level.INFO, "QVCS Enterprise Server exit complete."); } } } class NonSecureServer implements Runnable { private final int localPort; private ServerSocket nonSecureServerSocket = null; NonSecureServer(int port) { this.localPort = port; } void closeServerSocket() { if (nonSecureServerSocket != null) { try { nonSecureServerSocket.close(); } catch (IOException e) { LOGGER.log(Level.FINE, "QVCS Enterprise IOException when closing server socket:" + e.getLocalizedMessage()); } finally { nonSecureServerSocket = null; } } } @Override public void run() { try { nonSecureServerSocket = new ServerSocket(localPort); LOGGER.log(Level.INFO, "Non secure server is listening on port: [" + localPort + "]"); while (!ServerResponseFactory.getShutdownInProgress()) { Socket socket = nonSecureServerSocket.accept(); socket.setTcpNoDelay(true); socket.setKeepAlive(true); LOGGER.log(Level.INFO, "QVCSEnterpriseServer: got non-secure connect"); LOGGER.log(Level.INFO, "local socket port: [" + socket.getLocalPort() + "]"); LOGGER.log(Level.INFO, "remote socket port: [" + socket.getPort() + "]"); LOGGER.log(Level.INFO, "Launching worker thread for non-secure connection"); ServerWorker ws = new ServerWorker(socket); threadPool.execute(ws); } } catch (RejectedExecutionException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); // <editor-fold> LOGGER.log(Level.WARNING, e.getLocalizedMessage() + " cause: " + e.getCause() != null ? e.getCause().getLocalizedMessage() : ""); // </editor-fold> } catch (java.net.SocketException e) { LOGGER.log(Level.INFO, "Server non-secure accept thread is exiting for port [" + localPort + "]"); } catch (java.io.IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { closeServerSocket(); LOGGER.log(Level.INFO, "QVCSEnterpriseServer: closing listener thread for port: [" + localPort + "]"); } } } static class QVCSWebServer implements Runnable { private final String[] webServerArguments; QVCSWebServer(String[] args) { // <editor-fold> if (args != null && args.length > 0) { webServerArguments = new String[2]; webServerArguments[0] = args[0]; if (args.length > 3) { webServerArguments[1] = args[3]; } else { webServerArguments[1] = WEB_SERVER_PORT; } } else { webServerArguments = new String[2]; webServerArguments[0] = System.getProperty(USER_DIR); webServerArguments[1] = WEB_SERVER_PORT; } // </editor-fold> } @Override public void run() { try { WebServer.start(webServerArguments); } catch (IOException e) { LOGGER.log(Level.INFO, "Web server exiting due to exception: " + e.toString()); } } } }